symbiot 0.55.0 → 0.55.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.55.1
6
+
7
+ - **No more stray Symbiot windows.** 0.55.0's smoke test called Away like every other endpoint, so each full test run (yours, or an agent's in the symbiot repo) opened Away windows on your screen. Away now opens only when asked to by name, and never while windows are turned off (as in tests).
8
+ - **One Symbiot window, not a pile.** Opening Symbiot again (its icon, `symbiot app`) used to add a window each time. Now the newest window wins and older ones close themselves; a tab that can't close itself says Symbiot is open in a newer window, with **Use this one**. An update's restart that can't get its port bows out instead of lingering unseen.
9
+ - **Away is Super+`** (Super and the key above Tab). Super+S stacks windows on COSMIC, so it was the wrong key; `symbiot away --shortcut` sets up Super+` and clears the old one.
10
+
5
11
  ## 0.55.0
6
12
 
7
13
  - **Away: Super+S.** Symbiot full screen while you're away from the desk: the orb at rest, the time, the agents at work and what each is doing, how many things only you can do, and how much waits on what you watch. Counts only, never anyone's words. Any key or click brings you back. On X11 with more than one screen the orb bounces across all of them; under Wayland (COSMIC, GNOME, KDE) it's one window on your screen, because no app may place windows there. `symbiot away --shortcut` sets up Super+S (on COSMIC it's done for you). Super+Esc still locks.
package/README.md CHANGED
@@ -643,21 +643,21 @@ of this: it adapts to you.
643
643
  Everything it learns stays on this computer: `adapt.json` and `mind.json` in
644
644
  Symbiot's config folder, readable by you only.
645
645
 
646
- ### Away: Super+S
646
+ ### Away: Super+`
647
647
 
648
- Stepping away? **Super+S** (or `symbiot away`) puts Symbiot full screen: the orb at
648
+ Stepping away? **Super+`** (Super and the key above Tab), or `symbiot away`, puts Symbiot full screen: the orb at
649
649
  rest, the time, and what's going on. That means the agents at work and what each is
650
650
  doing, how many things only you can do, and how much waits on what you watch. It shows
651
651
  counts only, never anyone's words, so it's fine on a screen across the room. Any key
652
- or click (or Super+S again) brings you back.
652
+ or click (or Super+` again) brings you back.
653
653
 
654
654
  On one screen the orb rests in the middle. On X11 desktops with more screens it
655
655
  bounces across all of them, one window per screen, with the orb crossing from one to
656
656
  the next. Under Wayland (COSMIC, GNOME, KDE) no app may place a window on a screen, so
657
657
  it's one window on the screen you're on.
658
658
 
659
- `symbiot away --shortcut` sets up Super+S. On COSMIC that's done for you (it replaces
660
- COSMIC's own Super+S, which stacks windows); elsewhere it tells you what to bind.
659
+ `symbiot away --shortcut` sets up Super+`. On COSMIC that's done for you; elsewhere
660
+ it tells you what to bind.
661
661
  Away isn't a lock: **Super+Esc** still locks the computer.
662
662
 
663
663
  ## Link your work
package/away.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // symbiot — Away: Symbiot full screen on every screen while you're away from the
2
- // desk (Super+S, or `symbiot away`): the orb at rest, the time, and what's going on
2
+ // desk (Super+`, or `symbiot away`): the orb at rest, the time, and what's going on
3
3
  // (agents at work and what they're doing, how much waits on you), counts only, no
4
4
  // one's words. One screen: the orb in the middle. More: it bounces across all of
5
5
  // them, one kiosk window per screen, each working out where the orb is from the
@@ -11,7 +11,7 @@
11
11
  // own move-to-screen shortcut). So under Wayland it's one window, full screen where
12
12
  // you are, the orb in the middle. Each window's profile lets Symbiot's address see the
13
13
  // screens and go full screen without a click, so nothing asks.
14
- // Any key or click, or Super+S again, closes every window. Not a lock: Super+Esc
14
+ // Any key or click, or Super+` again, closes every window. Not a lock: Super+Esc
15
15
  // locks the computer.
16
16
  import { spawn, spawnSync } from "node:child_process";
17
17
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
@@ -65,6 +65,7 @@ function grantProfile(dir, origin) {
65
65
  const placeable = (env = process.env) => (env.XDG_SESSION_TYPE || "").toLowerCase() === "x11" || (!env.WAYLAND_DISPLAY && !!env.DISPLAY);
66
66
  function openAway(url, { chrome = chromeBinary(), scr = placeable() ? screens() : { list: [], display: "" }, now = Date.now() } = {}) {
67
67
  if (!chrome) return { error: "Away needs Chrome or Chromium on this computer." };
68
+ if (process.env.SYMBIOT_NO_OPEN === "1") return { open: false, note: "Windows are off here (SYMBIOT_NO_OPEN): tests, and runs that mustn't open any." };
68
69
  closeAway();
69
70
  const qs = awayQueries(scr.list, now);
70
71
  const origin = (String(url).match(/^https?:\/\/[^/?#]+/) || [""])[0];
@@ -77,31 +78,36 @@ function openAway(url, { chrome = chromeBinary(), scr = placeable() ? screens()
77
78
  });
78
79
  return { open: KIDS.length > 0, screens: Math.max(1, scr.list.length) };
79
80
  }
80
- // Open, close (open: false) or, with neither said, the other way round.
81
+ // open: true opens, false closes, "toggle" goes the other way; anything else only
82
+ // says whether it's open (a call that names nothing never opens windows: the smoke
83
+ // test calls every endpoint, and it opened Away on the owner's screen every run).
81
84
  function toggleAway(url, open, deps) {
82
- const want = open === undefined || open === null ? !awayOpen() : !!open;
85
+ if (open !== true && open !== false && open !== "toggle") return { open: awayOpen() };
86
+ const want = open === "toggle" ? !awayOpen() : open;
83
87
  if (!want) { closeAway(); return { open: false }; }
84
88
  return openAway(url, deps);
85
89
  }
86
90
 
87
91
  // ---- the shortcut -------------------------------------------------------------
88
- // Super+S runs `symbiot away`. COSMIC: a custom shortcut in its config (it reloads
89
- // it as it changes; one for Super+S takes over the default there, which stacks
90
- // windows). Elsewhere it says how to add one.
92
+ // Super+` (the key above Tab) runs `symbiot away`: free on COSMIC, where Super+S
93
+ // stacks windows. COSMIC: a custom shortcut in its config (it reloads it as it
94
+ // changes). Elsewhere it says how to add one.
91
95
  const COSMIC_CUSTOM = (home = homedir()) => join(home, ".config", "cosmic", "com.system76.CosmicSettings.Shortcuts", "v1", "custom");
92
- function shortcutLine(cmd) { return ` (modifiers: [Super], key: "s"): Spawn(${JSON.stringify(cmd)}),`; }
93
- // The new custom file: whatever was there, minus any Super+S, plus ours.
96
+ const KEY = "grave";
97
+ function shortcutLine(cmd) { return ` (modifiers: [Super], key: "${KEY}"): Spawn(${JSON.stringify(cmd)}),`; }
98
+ // The new custom file: whatever was there, minus any Super+` and any earlier one of
99
+ // ours (on another key), plus ours.
94
100
  function withShortcut(text, cmd) {
95
101
  const body = String(text || "").trim().replace(/^\{/, "").replace(/\}\s*$/, "");
96
- const kept = body.split("\n").filter((l) => l.trim() && !/\(modifiers:\s*\[\s*Super\s*,?\s*\],\s*key:\s*"s"\s*\)/.test(l));
102
+ const kept = body.split("\n").filter((l) => l.trim() && !/\(modifiers:\s*\[\s*Super\s*,?\s*\],\s*key:\s*"grave"\s*\)/.test(l) && !/Spawn\("[^"]*symbiot[^"]* away"\)/.test(l));
97
103
  return "{\n" + [...kept.map((l) => l.replace(/\s*$/, "")), shortcutLine(cmd)].join("\n") + "\n}\n";
98
104
  }
99
105
  function installShortcut({ cmd, home = homedir(), desktop = process.env.XDG_CURRENT_DESKTOP || "" } = {}) {
100
- if (!/cosmic/i.test(desktop)) return { manual: true, note: `Add a keyboard shortcut in your desktop's settings: Super+S runs ${cmd}` };
106
+ if (!/cosmic/i.test(desktop)) return { manual: true, note: `Add a keyboard shortcut in your desktop's settings: Super+\` runs ${cmd}` };
101
107
  const f = COSMIC_CUSTOM(home);
102
108
  let had = ""; try { had = existsSync(f) ? readFileSync(f, "utf8") : ""; } catch {}
103
109
  try { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, withShortcut(had, cmd)); } catch (e) { return { error: "Couldn't write " + f + ": " + ((e && e.message) || e) }; }
104
- return { ok: true, file: f, replaced: /key:\s*"s"/.test(had) };
110
+ return { ok: true, file: f, replaced: /key:\s*"grave"/.test(had) };
105
111
  }
106
112
 
107
113
  export { placeable, grantProfile, parseMonitors, screens, awayQueries, openAway, closeAway, awayOpen, toggleAway, withShortcut, installShortcut, COSMIC_CUSTOM };
package/index.mjs CHANGED
@@ -329,16 +329,16 @@ async function viaApp(path, body) {
329
329
  } catch (e) { return { error: "Lost Symbiot's app mid-way: " + ((e && e.message) || e) }; }
330
330
  }
331
331
  // symbiot away: Symbiot full screen on every screen (again: closes it). --shortcut:
332
- // Super+S runs it (on COSMIC it's set up; elsewhere it says how).
332
+ // Super+` runs it (on COSMIC it's set up; elsewhere it says how).
333
333
  async function cmdAway() {
334
334
  if (has("shortcut")) {
335
335
  const { installShortcut } = await import("./away.mjs");
336
336
  const r = installShortcut({ cmd: `${process.execPath} ${process.argv[1]} away` });
337
337
  if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; return; }
338
- console.log(r.manual ? r.note : `${c.g("✓")} Super+S now opens Away${r.replaced ? " (it replaces what Super+S did before)" : ""}. ${c.d(r.file)}`);
338
+ console.log(r.manual ? r.note : `${c.g("✓")} Super+\` (the key above Tab) now opens Away. ${c.d(r.file)}`);
339
339
  return;
340
340
  }
341
- const r = await viaApp("/api/away", {});
341
+ const r = await viaApp("/api/away", { open: "toggle" });
342
342
  if (!r) { console.log(c.y("Symbiot's app isn't running.") + c.d(" Start it with symbiot app, then Away works.")); process.exitCode = 1; return; }
343
343
  if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; }
344
344
  }
@@ -768,7 +768,7 @@ ${c.b("Usage")}
768
768
  symbiot todo what's still on your plate
769
769
  symbiot app open the visual app in your browser
770
770
  symbiot away [--shortcut] Symbiot full screen on every screen while you're away;
771
- again closes it. --shortcut: Super+S does it
771
+ again closes it. --shortcut: Super+\` does it
772
772
  symbiot app --fresh [--keep] try it as someone new: a brand-new Symbiot in
773
773
  a throwaway home, none of your data or accounts
774
774
  symbiot drift [--fetch] what's out of sync / at risk across repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.55.0",
3
+ "version": "0.55.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 \u2014 Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -214,6 +214,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
214
214
  const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
215
215
  const screenOut =(s) => (s && s.id ? { ...s, blueprint: blueprint(s), ...(s.page ? { trusted: isTrusted(s.page.url) } : {}) } : s && s.screens ? { ...s, screens: s.screens.map(screenOut) } : s);
216
216
  let UPDATING = null; // an update in flight: { target, attempt, retrying? }
217
+ let NEWEST_WIN = ""; // the Symbiot window opened last: older ones close themselves (one window, not a pile)
217
218
  process.on("exit", () => closeAway()); // Away's windows go with the app (a quit, an update's restart)
218
219
  const server = createServer(async (req, res) => {
219
220
  const u = new URL(req.url, "http://127.0.0.1");
@@ -418,7 +419,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
418
419
  if (u.pathname === "/api/run" && req.method === "POST") { const b = await readBody(req); const cmd = ["week", "standup", "todo"].includes(b.cmd) ? b.cmd : "week"; return json(res, cmd === "week" ? await runWeekly(writeup, { notify: false }) : await writeup(cmd)); }
419
420
  if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
420
421
  // a sandbox (symbiot app --fresh) never offers an update: it would replace your real install
421
- if (u.pathname === "/api/ping") { if (u.searchParams.get("fresh") === "1" && !SANDBOX) await checkLatest(); return json(res, { version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION, newer: !SANDBOX && semverGt(LATEST_VERSION, VERSION), ...(UPDATING && UPDATING.retrying ? { retrying: UPDATING } : {}), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
422
+ if (u.pathname === "/api/ping") { if (u.searchParams.get("fresh") === "1" && !SANDBOX) await checkLatest(); const w = u.searchParams.get("w") || ""; if (/^[a-z0-9]{6,20}$/.test(w) && u.searchParams.get("new") === "1") NEWEST_WIN = w; return json(res, { ...(NEWEST_WIN ? { window: NEWEST_WIN } : {}), version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION, newer: !SANDBOX && semverGt(LATEST_VERSION, VERSION), ...(UPDATING && UPDATING.retrying ? { retrying: UPDATING } : {}), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
422
423
  // What's new: after an update, since the version you last saw (until you click Got it);
423
424
  // ?latest=1, what the update on offer brings, from its package on npm
424
425
  if (u.pathname === "/api/whatsnew") {
@@ -473,7 +474,10 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
473
474
  server.on("error", (e) => {
474
475
  // Stable port busy (an older instance still exiting during an update, or a
475
476
  // second app): retry briefly, then fall back to a random port.
476
- if (e && e.code === "EADDRINUSE" && tries < 8) { tries++; setTimeout(() => { try { server.listen(PORT, "127.0.0.1"); } catch {} }, 500); }
477
+ if (e && e.code === "EADDRINUSE" && tries < (RELAUNCH ? 40 : 8)) { tries++; setTimeout(() => { try { server.listen(PORT, "127.0.0.1"); } catch {} }, 500); }
478
+ // after an update, the window only ever looks for the stable port: a copy on another
479
+ // port would serve nobody and linger (one did, for hours), so it bows out
480
+ else if (RELAUNCH) process.exit(0);
477
481
  else { try { server.listen(0, "127.0.0.1"); } catch {} }
478
482
  });
479
483
  server.listen(PORT, "127.0.0.1");
package/ui.mjs CHANGED
@@ -818,7 +818,7 @@ body.lq-light .lqatt img{border-color:rgba(21,26,33,.15)}body.lq-light .lqatt bu
818
818
  #lqform.drop{outline:2px dashed rgba(242,165,65,.7);outline-offset:4px}
819
819
  .lqmimgs{display:flex;gap:6px;margin-bottom:6px}.lqmimgs img{width:120px;max-height:90px;object-fit:cover;border-radius:8px}
820
820
 
821
- /* Away (Super+S): the orb, the time and what's going on, on every screen; nothing else */
821
+ /* Away (Super and the key above Tab): the orb, the time and what's going on, on every screen; nothing else */
822
822
  #lqaway{display:none}
823
823
  body.lq-away{cursor:none;overflow:hidden}
824
824
  body.lq-away #lq{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;display:block!important}
@@ -2280,8 +2280,17 @@ document.getElementById('autostart').addEventListener('change',saveAutostart);
2280
2280
  (function(){var h='';for(var i=0;i<24;i++)h+="<option value='"+i+"'>"+(i<10?'0':'')+i+":00</option>";document.getElementById('weeklyhour').innerHTML=h;})();
2281
2281
  var SRV_STARTED=null,srvDown=false,updBusy=false;
2282
2282
  function ubar(){return document.getElementById('updatebar');}
2283
+ // One Symbiot window, not a pile: each window has an id, says so the first time it
2284
+ // asks, and the server remembers the newest. An older one closes itself; where the
2285
+ // browser won't let it (a tab you opened), it says so and can take over. Away's
2286
+ // windows don't count.
2287
+ var WIN_ID=/[?&]away=1(&|$)/.test(location.search||'')?'':Math.random().toString(36).slice(2,12),WIN_NEW=true; // read from the address: AWAY is set further down
2288
+ function winTakeOver(){WIN_NEW=true;var o=$('winold');if(o)o.remove();heartbeat(false);}
2289
+ function winOld(){try{window.close();}catch(e){}setTimeout(function(){if($('winold')||!document.body)return;var o=document.createElement('div');o.id='winold';o.className='updatebar reconnect show';o.innerHTML="Symbiot is open in a newer window. <button class='ghost' id='wintake'>Use this one</button>";document.body.appendChild(o);var t=$('wintake');if(t)t.onclick=winTakeOver;},400);}
2283
2290
  function heartbeat(fresh){
2284
- api('/api/ping'+(fresh?'?fresh=1':'')).then(function(p){
2291
+ var q=[];if(fresh)q.push('fresh=1');if(WIN_ID){q.push('w='+WIN_ID);if(WIN_NEW)q.push('new=1');}
2292
+ api('/api/ping'+(q.length?'?'+q.join('&'):'')).then(function(p){
2293
+ if(WIN_ID){WIN_NEW=false;if(p&&p.window&&p.window!==WIN_ID){if(!$('winold'))winOld();return;}}
2285
2294
  var b=ubar();
2286
2295
  var ve=document.getElementById('ver');if(ve&&p.version){ve.textContent='v'+p.version+(p.sandbox?' · sandbox':'');if(p.sandbox){ve.title='A brand-new Symbiot to try first run in (symbiot app --fresh): its own empty home, none of your data, accounts or memory. It is deleted when you quit.';ve.style.color='var(--amber)';}}
2287
2296
  if(SRV_STARTED===null){SRV_STARTED=p.started;appBar(p);}
@@ -2817,7 +2826,7 @@ var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,slow?0:t);gl.uniform1f(U.uD
2817
2826
  gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);
2818
2827
  gl.drawArrays(gl.TRIANGLES,0,3);}
2819
2828
 
2820
- // ---- Away (Super+S) ------------------------------------------------------------
2829
+ // ---- Away (Super and the key above Tab) ------------------------------------------------------------
2821
2830
  // ?away=1 from "symbiot away": this window is one of the screens. One screen: the
2822
2831
  // orb rests in the middle. More: it bounces across their shared area (gw x gh) at
2823
2832
  // the same pace in every window from the same start (t0), each drawing it while it's