brane 0.3.2 → 0.3.3

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
@@ -6,6 +6,27 @@ page, which is also what triggers its publish (see
6
6
  [docs/releasing.md](docs/releasing.md)). This file is the summary that
7
7
  travels in the npm tarball.
8
8
 
9
+ ## 0.3.3 - 2026-10-14
10
+
11
+ - A fresh `npm install brane` audits clean. The math syntax extension
12
+ is now vendored (`src/math-syntax.ts`) instead of depending on
13
+ `micromark-extension-math`, which pinned a second, older katex into
14
+ every install (GHSA-238p-pmpm-9mq7) and led `npm audit fix --force` to
15
+ propose brane@0.1.1 ([#608](https://github.com/benjamn/brane/pull/608)).
16
+ - In the web app, "All narratives" opens the list in a new tab, and a
17
+ link opened in a new tab lands on the daemon it came from rather than
18
+ the browser's default one ([#607](https://github.com/benjamn/brane/pull/607)).
19
+ - The 膜 glyph carries a caption — a quip or a tip about brane — that
20
+ changes on each tap and nothing else; the empty state's words move
21
+ above it, and it no longer says "No narratives yet" when they are all
22
+ archived ([#606](https://github.com/benjamn/brane/pull/606)).
23
+ - The README, which is also the npm page, opens with a new
24
+ introduction: what brane is for, the LIVE loop, and terminal
25
+ mirroring ([#610](https://github.com/benjamn/brane/pull/610)).
26
+
27
+ Published while the repository is private, so like 0.3.0–0.3.2 it
28
+ carries no npm provenance attestation.
29
+
9
30
  ## 0.3.2 - 2026-10-13
10
31
 
11
32
  - Archive is offered only once a narrative has ended, and the daemon
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # brane
2
2
 
3
- A TypeScript CLI and userspace daemon for managing narrative agent sessions.
3
+ Brane provides a [web-based remote management interface](https://brane.organelle.dev/) for running long-lived Terminal UI (TUI) applications on any remote machine, securely and directly, without a central server, supporting not only full shells like `zsh` and `fish` and utilities like `lazygit` or `htop`, but also most of the popular _agentic_ TUI commands: `claude`, `codex`, `gemini`, `crush`, `copilot`, `goose`, and so on.
4
+
5
+ The "intelligent" TUI applications demonstrate their intelligence by using brane's local MCP tools: `whoami`, `narrate`, `listen` and the rest, to implement a protocol for ongoing, purposeful conversation (the "LIVE loop") recorded automatically to a permanent, fully agent-searchable record that remains completely private to the host machine.
6
+
7
+ Unlike a lot of other remote-coding tools, which let you chat with your agents and accept/deny permissions prompts, brane also _mirrors the terminal of the running agent_, so you can effortlessly observe your agents working from any mobile device, with the full power of a software keyboard and touch inputs for interacting with the terminal from afar, whenever you need that level of control.
8
+
9
+ ## Etymology and economics
4
10
 
5
11
  "Brane" is short for **membrane**, evoking a [_eukaryotic_](https://en.wikipedia.org/wiki/Eukaryote) metaphor for agent management, inherited from [organelle](https://github.com/benjamn/organelle): the daemon is the nucleus (which has a membrane), agents are organelles (which in turn have their own membranes), and narrative event logs trace everything each organelle has encountered and done (biologically implausible, but very useful).
6
12
 
@@ -12,24 +18,6 @@ If you find brane useful, perhaps because your tastes align with mine and I have
12
18
 
13
19
  [![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-ea4aaa?logo=githubsponsors)](https://github.com/sponsors/benjamn)
14
20
 
15
- ## What it does
16
-
17
- Brane runs long-lived coding agents (Claude Code, Codex, Gemini, Goose and
18
- others, or any command) in tmux on your own machine, keeps everything they
19
- say and hear in one SQLite database, and lets you read, steer and search
20
- them from a phone or any browser on your tailnet.
21
-
22
- Four ideas cover most of it:
23
-
24
- - **Fold**: a directory you've registered as a place agents may be spawned.
25
- - **Narrative**: one agent session's append-only log of everything it said
26
- and everything said to it. Every message has a stable address
27
- (`claude-4c7289d1:42`) you can link to from anywhere.
28
- - **The LIVE loop**: the protocol agents follow to talk to you through an
29
- MCP server brane injects into each session: listen, interpret, vocalize,
30
- execute.
31
- - **Meld**: a QR code that signs a phone or browser into your daemon.
32
-
33
21
  ## Quick start
34
22
 
35
23
  0. **Install [Tailscale](https://tailscale.com/download)** and join your
package/dist/export.js CHANGED
@@ -4,7 +4,7 @@ import { micromark } from "micromark";
4
4
  import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
5
5
  import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
6
6
  import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
7
- import { math } from "micromark-extension-math";
7
+ import { math } from "./math-syntax.js";
8
8
  import { NID_PATTERN, NID_REF_PATTERN } from "./shared/markdown-nids.js";
9
9
  import { isInlinePayload, INLINE_PAYLOAD_MIME } from "./shared/payload-inline.js";
10
10
  import { readEvents, readEventsBySeqs } from "./db/events.js";
@@ -32,7 +32,7 @@ import { micromark } from "micromark";
32
32
  import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
33
33
  import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
34
34
  import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
35
- import { math } from "micromark-extension-math";
35
+ import { math } from "./math-syntax.js";
36
36
  import { mathHtmlExtension } from "./math.js";
37
37
  import { fromMarkdown } from "mdast-util-from-markdown";
38
38
  import { inspectDiagram } from "./diagram/index.js";
@@ -0,0 +1,354 @@
1
+ /** micromark syntax extension for math: `$…$` (text) and `$$` fences (flow).
2
+ *
3
+ * Vendored from `micromark-extension-math` 3.1.0 (`dev/lib/syntax.js`,
4
+ * `math-flow.js`, `math-text.js`), the syntax half only, so the published
5
+ * package no longer depends on it. That package's index also loads its
6
+ * `mathHtml()`, which imports `katex` at the range it pins (`^0.16.0`). Our
7
+ * root `overrides` dedupe that onto our own katex in this checkout, but npm
8
+ * ignores a dependency's `overrides`, so every `npm install brane` got a
9
+ * second, older katex: loaded, never called (`src/math.ts` is our `mathHtml`),
10
+ * and flagged by `npm audit` (GHSA-238p-pmpm-9mq7). The tokenizers are
11
+ * unchanged apart from TypeScript annotations and dropping the `devlop`
12
+ * assertions, which upstream's production build drops too. The web app still
13
+ * imports the package itself: it ships prebuilt, under `expo/`'s overrides.
14
+ *
15
+ * Upstream: https://github.com/micromark/micromark-extension-math
16
+ *
17
+ * (The MIT License)
18
+ *
19
+ * Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
20
+ *
21
+ * Permission is hereby granted, free of charge, to any person obtaining
22
+ * a copy of this software and associated documentation files (the
23
+ * 'Software'), to deal in the Software without restriction, including
24
+ * without limitation the rights to use, copy, modify, merge, publish,
25
+ * distribute, sublicense, and/or sell copies of the Software, and to
26
+ * permit persons to whom the Software is furnished to do so, subject to
27
+ * the following conditions:
28
+ *
29
+ * The above copyright notice and this permission notice shall be
30
+ * included in all copies or substantial portions of the Software.
31
+ *
32
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
33
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
35
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
36
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
37
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
38
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
39
+ */
40
+ import { factorySpace } from "micromark-factory-space";
41
+ import { markdownLineEnding } from "micromark-util-character";
42
+ import { codes, constants, types } from "micromark-util-symbol";
43
+ /** Create an extension for `micromark` to enable math syntax. */
44
+ export function math(options) {
45
+ return {
46
+ flow: { [codes.dollarSign]: mathFlow },
47
+ text: { [codes.dollarSign]: mathText(options) },
48
+ };
49
+ }
50
+ // --- math (flow) ------------------------------------------------------------
51
+ const mathFlow = {
52
+ tokenize: tokenizeMathFenced,
53
+ concrete: true,
54
+ name: "mathFlow",
55
+ };
56
+ const nonLazyContinuation = {
57
+ tokenize: tokenizeNonLazyContinuation,
58
+ partial: true,
59
+ };
60
+ function tokenizeMathFenced(effects, ok, nok) {
61
+ const self = this;
62
+ const tail = self.events[self.events.length - 1];
63
+ const initialSize = tail && tail[1].type === types.linePrefix
64
+ ? tail[2].sliceSerialize(tail[1], true).length
65
+ : 0;
66
+ let sizeOpen = 0;
67
+ return start;
68
+ /** Start of math: `$$`, at the first `$`. */
69
+ function start(code) {
70
+ effects.enter("mathFlow");
71
+ effects.enter("mathFlowFence");
72
+ effects.enter("mathFlowFenceSequence");
73
+ return sequenceOpen(code);
74
+ }
75
+ /** In opening fence sequence. */
76
+ function sequenceOpen(code) {
77
+ if (code === codes.dollarSign) {
78
+ effects.consume(code);
79
+ sizeOpen++;
80
+ return sequenceOpen;
81
+ }
82
+ if (sizeOpen < 2) {
83
+ return nok(code);
84
+ }
85
+ effects.exit("mathFlowFenceSequence");
86
+ return factorySpace(effects, metaBefore, types.whitespace)(code);
87
+ }
88
+ /** In opening fence, before meta: `$$asciimath`, at `a`. */
89
+ function metaBefore(code) {
90
+ if (code === codes.eof || markdownLineEnding(code)) {
91
+ return metaAfter(code);
92
+ }
93
+ effects.enter("mathFlowFenceMeta");
94
+ effects.enter(types.chunkString, { contentType: constants.contentTypeString });
95
+ return meta(code);
96
+ }
97
+ /** In meta. */
98
+ function meta(code) {
99
+ if (code === codes.eof || markdownLineEnding(code)) {
100
+ effects.exit(types.chunkString);
101
+ effects.exit("mathFlowFenceMeta");
102
+ return metaAfter(code);
103
+ }
104
+ if (code === codes.dollarSign) {
105
+ return nok(code);
106
+ }
107
+ effects.consume(code);
108
+ return meta;
109
+ }
110
+ /** After meta. */
111
+ function metaAfter(code) {
112
+ // Guaranteed to be eol/eof.
113
+ effects.exit("mathFlowFence");
114
+ if (self.interrupt) {
115
+ return ok(code);
116
+ }
117
+ return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
118
+ }
119
+ /** After eol/eof in math, at a non-lazy closing fence or content. */
120
+ function beforeNonLazyContinuation(code) {
121
+ return effects.attempt({ tokenize: tokenizeClosingFence, partial: true }, after, contentStart)(code);
122
+ }
123
+ /** Before math content, definitely not before a closing fence. */
124
+ function contentStart(code) {
125
+ return (initialSize
126
+ ? factorySpace(effects, beforeContentChunk, types.linePrefix, initialSize + 1)
127
+ : beforeContentChunk)(code);
128
+ }
129
+ /** Before math content, after optional prefix. */
130
+ function beforeContentChunk(code) {
131
+ if (code === codes.eof) {
132
+ return after(code);
133
+ }
134
+ if (markdownLineEnding(code)) {
135
+ return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
136
+ }
137
+ effects.enter("mathFlowValue");
138
+ return contentChunk(code);
139
+ }
140
+ /** In math content. */
141
+ function contentChunk(code) {
142
+ if (code === codes.eof || markdownLineEnding(code)) {
143
+ effects.exit("mathFlowValue");
144
+ return beforeContentChunk(code);
145
+ }
146
+ effects.consume(code);
147
+ return contentChunk;
148
+ }
149
+ /** After math (ha!). */
150
+ function after(code) {
151
+ effects.exit("mathFlow");
152
+ return ok(code);
153
+ }
154
+ function tokenizeClosingFence(effects, ok, nok) {
155
+ let size = 0;
156
+ // Before closing fence, at optional whitespace.
157
+ return factorySpace(effects, beforeSequenceClose, types.linePrefix, self.parser.constructs.disable.null.includes("codeIndented")
158
+ ? undefined
159
+ : constants.tabSize);
160
+ /** In closing fence, after optional whitespace, at sequence. */
161
+ function beforeSequenceClose(code) {
162
+ effects.enter("mathFlowFence");
163
+ effects.enter("mathFlowFenceSequence");
164
+ return sequenceClose(code);
165
+ }
166
+ /** In closing fence sequence. */
167
+ function sequenceClose(code) {
168
+ if (code === codes.dollarSign) {
169
+ size++;
170
+ effects.consume(code);
171
+ return sequenceClose;
172
+ }
173
+ if (size < sizeOpen) {
174
+ return nok(code);
175
+ }
176
+ effects.exit("mathFlowFenceSequence");
177
+ return factorySpace(effects, afterSequenceClose, types.whitespace)(code);
178
+ }
179
+ /** After closing fence sequence, after optional whitespace. */
180
+ function afterSequenceClose(code) {
181
+ if (code === codes.eof || markdownLineEnding(code)) {
182
+ effects.exit("mathFlowFence");
183
+ return ok(code);
184
+ }
185
+ return nok(code);
186
+ }
187
+ }
188
+ }
189
+ function tokenizeNonLazyContinuation(effects, ok, nok) {
190
+ const self = this;
191
+ return start;
192
+ function start(code) {
193
+ if (code === null) {
194
+ return ok(code);
195
+ }
196
+ effects.enter(types.lineEnding);
197
+ effects.consume(code);
198
+ effects.exit(types.lineEnding);
199
+ return lineStart;
200
+ }
201
+ function lineStart(code) {
202
+ return self.parser.lazy[self.now().line] ? nok(code) : ok(code);
203
+ }
204
+ }
205
+ // --- math (text) ------------------------------------------------------------
206
+ // To do (upstream): next major: clean spaces in HTML compiler.
207
+ // This has to be coordinated together with `mdast-util-math`.
208
+ function mathText(options) {
209
+ const options_ = options || {};
210
+ let single = options_.singleDollarTextMath;
211
+ if (single === null || single === undefined) {
212
+ single = true;
213
+ }
214
+ return {
215
+ tokenize: tokenizeMathText,
216
+ resolve: resolveMathText,
217
+ previous,
218
+ name: "mathText",
219
+ };
220
+ function tokenizeMathText(effects, ok, nok) {
221
+ let sizeOpen = 0;
222
+ let size;
223
+ let token;
224
+ return start;
225
+ /** Start of math (text): `$a$` at the first `$`, or `\$a$` at the second. */
226
+ function start(code) {
227
+ effects.enter("mathText");
228
+ effects.enter("mathTextSequence");
229
+ return sequenceOpen(code);
230
+ }
231
+ /** In opening sequence. */
232
+ function sequenceOpen(code) {
233
+ if (code === codes.dollarSign) {
234
+ effects.consume(code);
235
+ sizeOpen++;
236
+ return sequenceOpen;
237
+ }
238
+ // Not enough markers in the sequence.
239
+ if (sizeOpen < 2 && !single) {
240
+ return nok(code);
241
+ }
242
+ effects.exit("mathTextSequence");
243
+ return between(code);
244
+ }
245
+ /** Between something and something else. */
246
+ function between(code) {
247
+ if (code === codes.eof) {
248
+ return nok(code);
249
+ }
250
+ if (code === codes.dollarSign) {
251
+ token = effects.enter("mathTextSequence");
252
+ size = 0;
253
+ return sequenceClose(code);
254
+ }
255
+ // Tabs don’t work, and virtual spaces don’t make sense.
256
+ if (code === codes.space) {
257
+ effects.enter("space");
258
+ effects.consume(code);
259
+ effects.exit("space");
260
+ return between;
261
+ }
262
+ if (markdownLineEnding(code)) {
263
+ effects.enter(types.lineEnding);
264
+ effects.consume(code);
265
+ effects.exit(types.lineEnding);
266
+ return between;
267
+ }
268
+ // Data.
269
+ effects.enter("mathTextData");
270
+ return data(code);
271
+ }
272
+ /** In data. */
273
+ function data(code) {
274
+ if (code === codes.eof ||
275
+ code === codes.space ||
276
+ code === codes.dollarSign ||
277
+ markdownLineEnding(code)) {
278
+ effects.exit("mathTextData");
279
+ return between(code);
280
+ }
281
+ effects.consume(code);
282
+ return data;
283
+ }
284
+ /** In closing sequence. */
285
+ function sequenceClose(code) {
286
+ // More.
287
+ if (code === codes.dollarSign) {
288
+ effects.consume(code);
289
+ size++;
290
+ return sequenceClose;
291
+ }
292
+ // Done!
293
+ if (size === sizeOpen) {
294
+ effects.exit("mathTextSequence");
295
+ effects.exit("mathText");
296
+ return ok(code);
297
+ }
298
+ // More or less accents: mark as data.
299
+ token.type = "mathTextData";
300
+ return data(code);
301
+ }
302
+ }
303
+ }
304
+ const resolveMathText = function (events) {
305
+ let tailExitIndex = events.length - 4;
306
+ let headEnterIndex = 3;
307
+ let index;
308
+ let enter;
309
+ // If we start and end with an EOL or a space.
310
+ if ((events[headEnterIndex][1].type === types.lineEnding ||
311
+ events[headEnterIndex][1].type === "space") &&
312
+ (events[tailExitIndex][1].type === types.lineEnding ||
313
+ events[tailExitIndex][1].type === "space")) {
314
+ index = headEnterIndex;
315
+ // And we have data.
316
+ while (++index < tailExitIndex) {
317
+ if (events[index][1].type === "mathTextData") {
318
+ // Then we have padding.
319
+ events[tailExitIndex][1].type = "mathTextPadding";
320
+ events[headEnterIndex][1].type = "mathTextPadding";
321
+ headEnterIndex += 2;
322
+ tailExitIndex -= 2;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ // Merge adjacent spaces and data.
328
+ index = headEnterIndex - 1;
329
+ tailExitIndex++;
330
+ while (++index <= tailExitIndex) {
331
+ if (enter === undefined) {
332
+ if (index !== tailExitIndex && events[index][1].type !== types.lineEnding) {
333
+ enter = index;
334
+ }
335
+ }
336
+ else if (index === tailExitIndex || events[index][1].type === types.lineEnding) {
337
+ events[enter][1].type = "mathTextData";
338
+ if (index !== enter + 2) {
339
+ events[enter][1].end = events[index - 1][1].end;
340
+ events.splice(enter + 2, index - enter - 2);
341
+ tailExitIndex -= index - enter - 2;
342
+ index = enter + 2;
343
+ }
344
+ enter = undefined;
345
+ }
346
+ }
347
+ return events;
348
+ };
349
+ const previous = function (code) {
350
+ // If there is a previous code, there will always be a tail.
351
+ return (code !== codes.dollarSign ||
352
+ this.events[this.events.length - 1][1].type === types.characterEscape);
353
+ };
354
+ //# sourceMappingURL=math-syntax.js.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * What the 膜 says, in the caption under it.
3
+ *
4
+ * `expo/components/MembraneEmpty.web.tsx` shows one of these under the glyph
5
+ * whenever the glyph is shown — under an empty list and under a full one —
6
+ * and a tap on the glyph swaps it for another. Nothing else changes it, so
7
+ * it can be read at leisure, links and all. Each line is markdown, rendered
8
+ * by the app's own renderer. Two kinds, mixed and worn alike: a quip is a
9
+ * joke at the glyph's own expense (it is, after all, a fragment shader
10
+ * running full-time under an empty list), and a tip is the loading-screen
11
+ * filler of a video game — one true thing about brane the operator may not
12
+ * have found yet. Every tip states something the app or CLI actually does;
13
+ * a tip that stops being true should be removed rather than left to
14
+ * mislead. Brevity is the operator's rule: aim
15
+ * for one sentence, with no terminal period (claude-nusb6rnr:7).
16
+ *
17
+ * The order is one shuffle per page load, then a fixed loop through it
18
+ * (`QuipLoop`): every line once before any line twice, and the same order
19
+ * every time round. Randomness is passed in so a test can pin it.
20
+ */
21
+ export const MEMBRANE_QUIPS = [
22
+ // ── quips ──────────────────────────────────────────────────────────────
23
+ "I noticed you were not using your GPU",
24
+ "I am the opposite of load-bearing",
25
+ "膜 is pronounced mó",
26
+ "My Kolmogorov complexity is actually quite low",
27
+ "Covert side-channel? Takes one to know one",
28
+ "Abstract impressionism? Here's my abstract impression of _you_",
29
+ "Drag me around a little",
30
+ "Do you know where your children are running shader programs?",
31
+ "No human art was digested in the making of whatever this is",
32
+ "Too many taps and the paradigm could shift",
33
+ "Bet you wish your phosphenes were lit like me",
34
+ "If this be a prisoner's cinema, then who be the prisoners?",
35
+ "Do not fall in love with any particular color scheme",
36
+ "All these colors will be lost in time, like tears in rain",
37
+ // ── tips ───────────────────────────────────────────────────────────────
38
+ "Agent stop working? Feed it something from the 🥫 menu",
39
+ "What was that terminal shortcut? Maybe it's in the 🦾 menu",
40
+ "Agents glow whenever they are awaiting chat input",
41
+ "@-mention any agent by NID (narrative ID) to summon them",
42
+ "Ask your agent about rendering mermaid diagrams",
43
+ "Go ahead and install [Tailscale](https://tailscale.com/) (it's free)",
44
+ "Command your own daemon: `npm install -g brane`",
45
+ "Wonder what this does? `brane absorb survey`",
46
+ "Have YOU read the /LIVE_loop protocol?",
47
+ "Your daemon from any device: `brane meld` and prosper 🖖",
48
+ "Fork tangential narratives with 🧬 Twin in the 🦠 menu",
49
+ ];
50
+ /** A uniformly random permutation of `list` (Fisher–Yates); `list` is left alone. */
51
+ export function shuffled(list, rand = Math.random) {
52
+ const out = [...list];
53
+ for (let i = out.length - 1; i > 0; i--) {
54
+ const j = Math.min(i, Math.floor(rand() * (i + 1)));
55
+ [out[i], out[j]] = [out[j], out[i]];
56
+ }
57
+ return out;
58
+ }
59
+ /** One shuffle of the lines, walked in a loop: `current` is the line showing,
60
+ * `next` steps to the one after it, back to the first after the last. */
61
+ export class QuipLoop {
62
+ order;
63
+ at = 0;
64
+ constructor(list = MEMBRANE_QUIPS, rand = Math.random) {
65
+ if (list.length === 0)
66
+ throw new Error("no quips");
67
+ this.order = shuffled(list, rand);
68
+ }
69
+ current() {
70
+ return this.order[this.at];
71
+ }
72
+ next() {
73
+ this.at = (this.at + 1) % this.order.length;
74
+ return this.current();
75
+ }
76
+ }
77
+ //# sourceMappingURL=membrane-quips.js.map
@@ -17,6 +17,10 @@
17
17
  */
18
18
  /** Side of the square the path is laid out in, in path units. */
19
19
  export const MO_GLYPH_BOX = 1000;
20
+ /** The empty band between the ink and the box's top or bottom edge, in path
21
+ * units: the glyph is centered, so it is the same above and below. Whatever
22
+ * sits under the glyph is that much farther from the ink than from the box. */
23
+ export const MO_GLYPH_MARGIN = 92.79;
20
24
  /** SVG path data for 膜, y down, inside `[0, MO_GLYPH_BOX]²`. */
21
25
  export const MO_GLYPH_PATH = "M493.98 449.69L493.98 425.61L791.54 425.61L791.54 449.69L493.98 449.69ZM493.98 557.19L493.98 533.11" +
22
26
  "L791.54 533.11L791.54 557.19L493.98 557.19ZM431.20 606.21L431.20 319.83L431.20 275.11L431.20 275.11" +
@@ -91,10 +91,72 @@ export function parseLineSpec(s) {
91
91
  const end = m[2] ? Number(m[2]) : undefined;
92
92
  return end !== undefined && end >= line ? { line, lineEnd: end } : { line };
93
93
  }
94
+ /** Which melded daemon a same-origin link means, named by its home `brane_id`
95
+ * (the registry's dedup key, and no secret), in the link's **fragment**:
96
+ * `#daemon=brane-f66d2b`.
97
+ *
98
+ * A tab's active daemon lives in its own `sessionStorage`, and a link opened in
99
+ * a new tab (⌘-click, middle-click, `target="_blank"`) starts with none: it
100
+ * falls back to this browser's *default* daemon, which on a shared app with
101
+ * several melded need not be the one the link was written from. So in-app
102
+ * hrefs carry this param, and an arriving tab whose slot is still empty
103
+ * adopts it as its active daemon before anything connects (`expo/lib/api.ts`,
104
+ * `adoptDaemonByBraneId`). A tab that already has one ignores it, which is
105
+ * what keeps a reload from undoing a later switch. Plain clicks navigate
106
+ * within the tab and never put it in the URL.
107
+ *
108
+ * It says which daemon, not what you are looking at, so it rides where the
109
+ * connection does (`connectionFragment`): a fragment reaches no server, and
110
+ * the shared host's edge, a daemon's request log and a link unfurler never
111
+ * see it. It is not `brane`, because a daemon hosts several branes and the
112
+ * list already filters by one; this names the daemon. A brane_id this browser
113
+ * has not melded is ignored, and the default applies as before. */
114
+ export const DAEMON_PARAM = "daemon";
94
115
  /** The params that describe *which daemon*, as opposed to what you are looking
95
116
  * at. They are origin-scoped: meaningful to the page that received them, never
96
- * to be carried onward. */
97
- export const CONNECTION_PARAMS = ["host", "token"];
117
+ * to be carried onward. `daemon` is one of them: cleaned on arrival like the
118
+ * token, and dropped by Isolate and Meld, whose token already says. */
119
+ export const CONNECTION_PARAMS = ["host", "token", DAEMON_PARAM];
120
+ /** An in-app href (path, optional query, optional fragment) with `daemon` set
121
+ * to `braneId` in its fragment, replacing any already there and leaving the
122
+ * query, which is the view, untouched. Without a `braneId` the href comes
123
+ * back unchanged, so a caller that does not know its daemon yet still gets a
124
+ * working link. */
125
+ export function withDaemonFragment(href, braneId) {
126
+ if (!braneId)
127
+ return href;
128
+ const hashAt = href.indexOf("#");
129
+ const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);
130
+ const params = new URLSearchParams(hashAt === -1 ? "" : href.slice(hashAt + 1));
131
+ params.set(DAEMON_PARAM, braneId);
132
+ return `${beforeHash}#${params.toString()}`;
133
+ }
134
+ /** The `daemon` param from a location hash (with or without its `#`), or null
135
+ * when absent or empty. */
136
+ export function daemonFromHash(hash) {
137
+ return new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash).get(DAEMON_PARAM) || null;
138
+ }
139
+ /** The brane_id an in-app link should name: the tab's active entry's, falling
140
+ * back to the first entry when `activeId` is missing or stale, which is what
141
+ * the root layout connects to in that case. Null with nothing melded. */
142
+ export function activeDaemonBraneId(daemons, activeId) {
143
+ return (daemons.find((d) => d.id === activeId) ?? daemons[0])?.brane_id ?? null;
144
+ }
145
+ /** Which registry entry a page arriving with `#daemon=<braneId>` should make
146
+ * this tab's active one, or null to leave things as they are.
147
+ *
148
+ * Only an **empty** slot is filled (`tabActiveId` is the tab's own
149
+ * sessionStorage value, not the cross-tab default it falls back to). A new
150
+ * tab has none; a reloaded, restored or duplicated tab has one and keeps it.
151
+ * That matters because the router puts the initial fragment back on its
152
+ * first URL write, so `#daemon=` can outlive a daemon switched later in the
153
+ * same tab, and a reload must not switch it back. A brane_id this browser
154
+ * has not melded is no instruction at all. */
155
+ export function daemonToAdopt(daemons, tabActiveId, braneId) {
156
+ if (tabActiveId !== null)
157
+ return null;
158
+ return daemons.find((d) => d.brane_id === braneId)?.id ?? null;
159
+ }
98
160
  /** A search string with the connection params removed and everything else left
99
161
  * alone, ready to hand to `history.replaceState`.
100
162
  *
@@ -1,7 +1,7 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { micromark } from "micromark";
4
- import { math } from "micromark-extension-math";
4
+ import { math } from "../math-syntax.js";
5
5
  import { MATH_BLOCK_CSS, prepareMathMarkdown, isMathContent, renderMathHtml } from "../shared/markdown-math.js";
6
6
  import { mathHtmlExtension, katexStyleFor } from "../math.js";
7
7
  import { readFileSync } from "node:fs";