brane 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +7 -19
- package/dist/export.js +1 -1
- package/dist/markdown-diagnostics.js +1 -1
- package/dist/math-syntax.js +354 -0
- package/dist/shared/membrane-quips.js +77 -0
- package/dist/shared/mo-glyph.js +4 -0
- package/dist/shared/view-url.js +64 -2
- package/dist/test/markdown-math.test.js +1 -1
- package/dist/test/membrane-quips.test.js +62 -0
- package/dist/test/mo-glyph.test.js +3 -1
- package/dist/test/view-url.test.js +106 -1
- package/docs/math.md +3 -0
- package/docs/view-urls.md +32 -2
- package/expo/dist/_expo/static/js/web/{entry-807575a48b8a2f18f8bffe53c8b35fbd.js → entry-523ef6a002ad040cec40c370616a0e15.js} +28 -27
- package/expo/dist/index.html +1 -1
- package/expo/dist/sw.js +2 -2
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ 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.4 - 2026-10-14
|
|
10
|
+
|
|
11
|
+
- "All narratives" in the narrative switcher pushes the list in this tab
|
|
12
|
+
again, so Back returns to the narrative; 0.3.3 opened it in a new tab
|
|
13
|
+
on every click. It is still a link, so a ⌘-click opens a new tab, and
|
|
14
|
+
that tab lands on the same daemon
|
|
15
|
+
([#611](https://github.com/benjamn/brane/pull/611)).
|
|
16
|
+
|
|
17
|
+
Published while the repository is private, so like 0.3.0–0.3.3 it
|
|
18
|
+
carries no npm provenance attestation.
|
|
19
|
+
|
|
20
|
+
## 0.3.3 - 2026-10-14
|
|
21
|
+
|
|
22
|
+
- A fresh `npm install brane` audits clean. The math syntax extension
|
|
23
|
+
is now vendored (`src/math-syntax.ts`) instead of depending on
|
|
24
|
+
`micromark-extension-math`, which pinned a second, older katex into
|
|
25
|
+
every install (GHSA-238p-pmpm-9mq7) and led `npm audit fix --force` to
|
|
26
|
+
propose brane@0.1.1 ([#608](https://github.com/benjamn/brane/pull/608)).
|
|
27
|
+
- In the web app, "All narratives" opens the list in a new tab, and a
|
|
28
|
+
link opened in a new tab lands on the daemon it came from rather than
|
|
29
|
+
the browser's default one ([#607](https://github.com/benjamn/brane/pull/607)).
|
|
30
|
+
- The 膜 glyph carries a caption — a quip or a tip about brane — that
|
|
31
|
+
changes on each tap and nothing else; the empty state's words move
|
|
32
|
+
above it, and it no longer says "No narratives yet" when they are all
|
|
33
|
+
archived ([#606](https://github.com/benjamn/brane/pull/606)).
|
|
34
|
+
- The README, which is also the npm page, opens with a new
|
|
35
|
+
introduction: what brane is for, the LIVE loop, and terminal
|
|
36
|
+
mirroring ([#610](https://github.com/benjamn/brane/pull/610)).
|
|
37
|
+
|
|
38
|
+
Published while the repository is private, so like 0.3.0–0.3.2 it
|
|
39
|
+
carries no npm provenance attestation.
|
|
40
|
+
|
|
9
41
|
## 0.3.2 - 2026-10-13
|
|
10
42
|
|
|
11
43
|
- 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
|
-
|
|
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
|
[](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 "
|
|
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 "
|
|
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
|
package/dist/shared/mo-glyph.js
CHANGED
|
@@ -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" +
|
package/dist/shared/view-url.js
CHANGED
|
@@ -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
|
-
|
|
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 "
|
|
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";
|