isorite 0.3.0 → 0.5.0

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.
Files changed (62) hide show
  1. package/README.md +3 -3
  2. package/dist/core.js +4 -2
  3. package/dist/embed-worker.js +9 -4
  4. package/dist/main.js +7707 -7203
  5. package/docs/setup-for-agents.md +27 -2
  6. package/docs/setup.md +28 -2
  7. package/docs/terminal.md +16 -4
  8. package/package.json +1 -1
  9. package/plugin/tui.tsx +3 -77
  10. package/web/_app/immutable/assets/{0.IOi79DXH.css → 0.DNvf5eFW.css} +1 -1
  11. package/web/_app/immutable/chunks/-7PsQOY4.js +3 -0
  12. package/web/_app/immutable/chunks/B9vLu37F.js +1 -0
  13. package/web/_app/immutable/chunks/BAVeNK5c.js +20 -0
  14. package/web/_app/immutable/chunks/C1xA2mdF.js +1 -0
  15. package/web/_app/immutable/chunks/CQqNkDRK.js +1 -0
  16. package/web/_app/immutable/chunks/Cs9sQ8Oa.js +1 -0
  17. package/web/_app/immutable/chunks/{BNCmWi9e.js → D4Jg9Smi.js} +1 -1
  18. package/web/_app/immutable/chunks/{BN1DRYj2.js → DG2llOXe.js} +21 -21
  19. package/web/_app/immutable/chunks/DqVFJWdL.js +1 -0
  20. package/web/_app/immutable/chunks/{BZeALCyJ.js → HCrXwzvD.js} +1 -1
  21. package/web/_app/immutable/chunks/MjV3DsQW.js +1 -0
  22. package/web/_app/immutable/chunks/uID4v_1K.js +1 -0
  23. package/web/_app/immutable/entry/app.CR9ubCCo.js +2 -0
  24. package/web/_app/immutable/entry/start.D4DOZCP6.js +1 -0
  25. package/web/_app/immutable/nodes/0.BYXv43VP.js +26 -0
  26. package/web/_app/immutable/nodes/1.DGP71l35.js +1 -0
  27. package/web/_app/immutable/nodes/2.CxspTYRG.js +1 -0
  28. package/web/_app/immutable/nodes/3.ZTzs6oEI.js +1 -0
  29. package/web/_app/immutable/nodes/4.CeIGu2c2.js +2 -0
  30. package/web/_app/immutable/nodes/5.k1DmYkGm.js +5 -0
  31. package/web/_app/immutable/nodes/6.Q0yGno8p.js +1 -0
  32. package/web/_app/immutable/nodes/7.BJRu312o.js +1 -0
  33. package/web/_app/version.json +1 -1
  34. package/web/index.html +17 -20
  35. package/dist/tutorial/barnes_hut.py +0 -46
  36. package/dist/tutorial/sim.py +0 -29
  37. package/plugin/tour.ts +0 -74
  38. package/web/_app/immutable/assets/8.CFA9_nYZ.css +0 -1
  39. package/web/_app/immutable/assets/Tutorial.DOfsM49T.css +0 -1
  40. package/web/_app/immutable/chunks/-XH9wLvF.js +0 -1
  41. package/web/_app/immutable/chunks/B_AYGp9o.js +0 -1
  42. package/web/_app/immutable/chunks/BavLcJN1.js +0 -1
  43. package/web/_app/immutable/chunks/BcrJkegU.js +0 -1
  44. package/web/_app/immutable/chunks/BqrPWk6N.js +0 -20
  45. package/web/_app/immutable/chunks/Bze-M_kr.js +0 -3
  46. package/web/_app/immutable/chunks/CRzLMJPS.js +0 -1
  47. package/web/_app/immutable/chunks/Cr4-_2Vw.js +0 -1
  48. package/web/_app/immutable/chunks/D25FgEb3.js +0 -43
  49. package/web/_app/immutable/chunks/D8jeeZa0.js +0 -1
  50. package/web/_app/immutable/chunks/DHMLhlug.js +0 -1
  51. package/web/_app/immutable/chunks/DsnHcTIO.js +0 -1
  52. package/web/_app/immutable/entry/app.B9MdwZzs.js +0 -2
  53. package/web/_app/immutable/entry/start.gtQcHKDR.js +0 -1
  54. package/web/_app/immutable/nodes/0.DSajjyn5.js +0 -15
  55. package/web/_app/immutable/nodes/1.hmQ_l8t-.js +0 -1
  56. package/web/_app/immutable/nodes/2.Bu6Zvx8F.js +0 -1
  57. package/web/_app/immutable/nodes/3.DcSG1nNk.js +0 -1
  58. package/web/_app/immutable/nodes/4.QUhLgH4i.js +0 -2
  59. package/web/_app/immutable/nodes/5.D3JSWVyR.js +0 -5
  60. package/web/_app/immutable/nodes/6.CWfUNDww.js +0 -1
  61. package/web/_app/immutable/nodes/7.DmPPLDvb.js +0 -1
  62. package/web/_app/immutable/nodes/8.CwMTBog4.js +0 -486
@@ -14,12 +14,13 @@ turn off certificate checking (`NODE_TLS_REJECT_UNAUTHORIZED=0`).
14
14
 
15
15
  | Command | What it does |
16
16
  |---|---|
17
+ | `isorite new <folder>` | Makes the folder (and its parents), empty but for `.isorite/`. Refuses one with files in it already: that is opened as it is. |
17
18
  | `isorite doctor [<notes>] [--report] [--offline]` | Checks the setup a step at a time (below). Exit 1 if anything failed. `--offline`: asks the provider nothing. |
18
19
  | `isorite serve <notes> [--port <n>]` | The browser app at `http://localhost:4767/`, on 127.0.0.1 only. |
19
20
  | `isorite term [<notes>] [--port <n>]` | OpenCode 2 in this terminal, in the current folder (the code), with the notes beside it. Starts the isorite server if none answers on the port, and stops it on exit. |
20
21
  | `isorite check <notes>` | Lints the notes: frontmatter, links, cycles. |
21
- | `isorite --tutorial` | A tour on an example base with a scripted model: no key or network needed. Good to check the install alone. |
22
- | `isorite model [--from <folder>]` | Optional: fetches the model for search by meaning (53 MB). |
22
+ | `isorite guide [<folder>]` | The guide, a knowledge base about isorite, written into `~/isorite-guide` (or the folder named) and served. With no provider set up, Iso is the guide's finder: no key or network needed. Good to check the install alone. `isorite --tutorial` is another name for it. |
23
+ | `isorite model [--from <folder>]` | Optional: the model for search by meaning. Says which is installed and where, else fetches it (53 MB) from npm, a GitHub release, then Hugging Face, saying why each failed. `--from`: granite's files, or any compatible model (below). |
23
24
  | `isorite help setup` | Prints this. |
24
25
 
25
26
  ## Files
@@ -29,7 +30,10 @@ turn off certificate checking (`NODE_TLS_REJECT_UNAUTHORIZED=0`).
29
30
  | `~/.config/isorite/config.toml` (or `$XDG_CONFIG_HOME/isorite/`) | isorite's settings for this machine: `device`, `[provider]`, `[tiers]`, `[prices]`. Mode 600. Holds the key. |
30
31
  | `~/.config/isorite/opencode/` | OpenCode's settings as isorite writes them at each `isorite term` start (`opencode.json`, theme, plugin link). Rewritten; don't edit. |
31
32
  | `~/.config/isorite/opencode/local.json` | The person's own OpenCode settings, laid over isorite's. Never rewritten. |
33
+ | `<notes>/AGENTS.md` | The base's own instructions (notation, level, how notes are named), sent to Iso with every turn and named in OpenCode's `instructions`. Not a note: not on the Atlas, not linted. |
32
34
  | `<notes>/.isorite/` | isorite's own folder in the notes: journals of every change, `settings.toml` (`repository = "<path>"`, where code links point). Made on first open; commit it with the notes. |
35
+ | `~/.local/share/isorite/models/` (or `$XDG_DATA_HOME/isorite/models/`) | The models for search by meaning, a folder each: `granite-embedding-small-english-r2/`, and any other installed with `--from`. `use` names the one used, if not granite. |
36
+ | `~/.cache/isorite/vectors-<hash>.json` | Each base's vectors, made by the model; made again when the model changes. |
33
37
  | `$TMPDIR/isorite-term-<port>.log` | The isorite server's output, when `isorite term` started it. Look here when "the isorite server did not start". |
34
38
  | `$TMPDIR/isorite-opencode-<port>.log` | OpenCode's server's output, under `isorite term`. |
35
39
 
@@ -44,6 +48,7 @@ url = "https://…/v1" # OpenAI-compatible; /chat/completions a
44
48
  key = "…" # the key; also settable in the browser (behind the logo)
45
49
  key_header = "api-key" # send the key in this header, as it is, instead of Authorization: Bearer
46
50
  stream_usage = false # don't send stream_options (some gateways answer 400 to it)
51
+ max_tokens = 16000 # the least output budget any call gets, for models that think at length
47
52
  cache_marks = true # send Anthropic cache marks (OpenRouter-style gateways only)
48
53
  pfx = "~/certs/me.pfx" # client certificate (mTLS), PKCS#12; or cert + cert_key (PEM)
49
54
  pfx_passphrase = "$PFX_PASS" # "$NAME": read from that environment variable
@@ -82,6 +87,8 @@ Rules:
82
87
  | `ISORITE_NODE_DEFAULTS=0` | Leave Node as it is: no system authorities, no proxy unless Node is told. |
83
88
  | `NODE_EXTRA_CA_CERTS` | A .pem of extra authorities, read by Node at start. |
84
89
  | `ISORITE_GATEWAY_URL`, `ISORITE_GATEWAY_MODELS`, `ISORITE_GATEWAY_KEY` | The terminal only: OpenCode asks this OpenAI-compatible address directly, with these models (comma-separated) and key, instead of `config.toml`'s provider. No certificate or headers apply. |
90
+ | `ISORITE_EMBED_DIR` | The model for search by meaning is in this folder (and nowhere else is looked). |
91
+ | `ISORITE_EMBED=off` | No search by meaning, whatever is installed. |
85
92
  | `ISORITE_OPENCODE` | The OpenCode 2 binary to run. Otherwise `opencode2`, then `opencode`, must be version 2. |
86
93
  | `XDG_CONFIG_HOME` | Where `isorite/config.toml` is looked for. |
87
94
  | `SSH_CONNECTION` | Set over SSH: `term`, `serve` and `doctor` print the `ssh -L` command for the browser. |
@@ -96,6 +103,12 @@ Rules:
96
103
  server's relay, and with the server's token as its key. The relay adds the real key, certificate, headers and
97
104
  query, and passes `/chat/completions` and `/models` on. So whatever works for Iso works for the terminal. The
98
105
  relay answers only on 127.0.0.1, and only that token.
106
+ - **Retries.** A 502, 503 or 504, a dropped connection or a timeout is asked again up to 3 times (after about 1, 2
107
+ and 4 s), and a 429 once (after its `Retry-After`, if 10 s or less), but only before any of the reply has come.
108
+ The relay does the same before it passes anything to OpenCode (which retries on its own as well). 4xx answers
109
+ and names not found are the settings', and are not asked again.
110
+ - **An empty reply.** A reply cut off at its length limit with nothing written (a reasoning model that spent the
111
+ budget thinking) is asked once more with three times the room.
99
112
  - With no key and no certificate in `config.toml`, and no `ISORITE_GATEWAY_*`, OpenCode uses its own models.
100
113
  `isorite term` prints which models it uses, and from where.
101
114
 
@@ -131,6 +144,10 @@ Rules:
131
144
  | `ETIMEDOUT`, `UND_ERR_CONNECT_TIMEOUT` | No answer: often a proxy is needed. | Set `HTTPS_PROXY` (and `NO_PROXY` for internal hosts reached directly). |
132
145
  | `said 401` / `said 403` | The key, or where it's sent, isn't accepted. | Check the key, `key_header`, and any header the gateway wants. The gateway's own message usually names it. |
133
146
  | `said 404 … no model called X, which is OpenRouter's name for it` | No `[tiers]`. | Name the gateway's models under `[tiers]`. `doctor` lists the models `/models` offers. |
147
+ | `… is unavailable (503 …), tried 4 times over N s: it is likely …, not your settings` | The gateway, or the model behind it, is down. | Wait, and send it again (↻ under the turn). If it lasts, the gateway's status page or owner. |
148
+ | `… used its whole output budget (N tokens) thinking, and wrote nothing` | A reasoning model ran out of room before it wrote. | A lower tier, or `max_tokens` under `[provider]`. |
149
+ | `! search by meaning: no model …` | Not installed (optional). | `isorite model`. Its failures name each source; offline, `isorite model --from <folder>` with a copy. |
150
+ | `The model could not be fetched:` and a line for npm, GitHub, Hugging Face | None reachable from here. | Copy the model's folder from a machine that has it (`isorite model` there says where), then `isorite model --from <folder>`; or any compatible model. |
134
151
  | `said 400: … stream_options` | The gateway refuses `stream_options`. | `stream_usage = false`. |
135
152
  | `✗ tools: … answered without calling the tool` | The model or gateway drops tools. | Choose a model that supports tool calls. Iso and the terminal need them. |
136
153
  | `isorite term needs OpenCode 2` | OpenCode 1, or none. | `npm i -g @opencode/cli`, or `ISORITE_OPENCODE=…`. |
@@ -138,5 +155,13 @@ Rules:
138
155
  | The link doesn't open, over SSH | `localhost` is the remote machine. | `ssh -L 4767:localhost:4767 <host>` from the person's own machine. |
139
156
  | The terminal works but uses an unexpected model | Check the line `isorite term` prints: `config.toml`, `ISORITE_GATEWAY_URL`, or OpenCode's own (no key or certificate). | Fix what it names. |
140
157
 
158
+ ## Another model for search by meaning
159
+
160
+ Any ONNX model that gives 384 numbers for a text, with its `tokenizer.json`, can stand in for granite:
161
+ `isorite model --from <folder>`. It's checked by use (loaded, and asked for one vector), copied into the models
162
+ folder, and used from then on; each base's vectors are made again with it. An optional `isorite-model.json` beside
163
+ its files gives `{"name": "…", "pooling": "cls" | "mean"}`: `cls` (the default) takes the first token's state, as
164
+ granite is used; `mean` averages them all, as sentence-transformers models such as all-MiniLM-L6-v2 are used.
165
+
141
166
  If the gateway isn't OpenAI-compatible (Bedrock's or Vertex's own APIs, for example), isorite can't use it directly.
142
167
  An OpenAI-compatible proxy in front of it, such as LiteLLM, can.
package/docs/setup.md CHANGED
@@ -9,7 +9,7 @@ run `isorite doctor`: it checks each step and says what to change.
9
9
  - **isorite**: `npm i -g isorite`.
10
10
  - For the terminal, **OpenCode 2**: `npm i -g @opencode/cli`.
11
11
 
12
- `isorite --tutorial` now shows you around, on an example base, with a scripted model: no key, nothing sent anywhere.
12
+ `isorite guide` now opens the guide, a knowledge base about isorite with things to do in it. Until a provider is set up, Iso there is the guide's finder: no key, nothing sent anywhere.
13
13
 
14
14
  ## 2. A model provider
15
15
 
@@ -45,6 +45,7 @@ url = "https://llm-gateway.corp.example/v1"
45
45
  key = "…"
46
46
  key_header = "api-key" # the header the key goes in, as it is, instead of Authorization: Bearer
47
47
  stream_usage = false # for a gateway that refuses stream_options; the gauge then counts nothing
48
+ max_tokens = 16000 # the least room a reply is given, for models that think at length
48
49
 
49
50
  # A client certificate (mTLS), for a gateway that asks for one: a .pfx (or .p12) ...
50
51
  pfx = "~/certs/me.pfx"
@@ -77,12 +78,20 @@ If the company's authority isn't installed on the machine, name its certificate
77
78
 
78
79
  ## 3. Your notes
79
80
 
80
- A folder of Markdown files; empty is fine.
81
+ A folder of Markdown files. To start from nothing:
81
82
 
82
83
  ```sh
84
+ isorite new ~/notes/my-project
83
85
  isorite serve ~/notes/my-project
84
86
  ```
85
87
 
88
+ `isorite new` makes the folder, empty but for isorite's own `.isorite/`. A folder of notes you have already is
89
+ opened as it is, with `isorite serve`.
90
+
91
+ How you want the base kept (your notation, the level, the language, how notes are named) goes in `AGENTS.md` at
92
+ the top of the notes, which Iso and the terminal's agent are both given. Edit it in Iso's settings, the icon at the
93
+ right of its pane's header, or in any editor. Iso may offer a line for it when you say how you want things done.
94
+
86
95
  It prints a link to click: `http://localhost:4767/`. isorite keeps what it needs in the folder's `.isorite/`.
87
96
 
88
97
  ## 4. The terminal
@@ -95,8 +104,25 @@ isorite term ~/notes/my-project
95
104
  It prints the same link, and which models the terminal uses. OpenCode uses the provider above, through isorite. More
96
105
  is in [terminal.md](terminal.md).
97
106
 
107
+ ## Search by meaning (optional)
108
+
109
+ ```sh
110
+ isorite model
111
+ ```
112
+
113
+ It says which model is installed and where, or fetches it (53 MB, once): from npm, else from a release on GitHub,
114
+ else from Hugging Face. Without it, Iso searches by words and follows links. Where none of those can be reached,
115
+ copy the model's folder from a machine that has it (`isorite model` there says where it is) and run
116
+ `isorite model --from <folder>`. Any compatible model works the same way: an ONNX model that gives 384 numbers,
117
+ with its `tokenizer.json` (for example all-MiniLM-L6-v2, with `{"pooling": "mean"}` in an `isorite-model.json`
118
+ beside it).
119
+
98
120
  ## When something fails
99
121
 
122
+ If a gateway is down for a moment (it answers 502, 503 or 504, or drops the connection), Iso and the terminal ask
123
+ it again, up to three times. When it stays down, Iso says it's likely the gateway, not your settings, and the ↻
124
+ under the turn sends it again.
125
+
100
126
  ```sh
101
127
  isorite doctor ~/notes/my-project
102
128
  ```
package/docs/terminal.md CHANGED
@@ -17,7 +17,8 @@ Setting isorite up the first time, or at work behind a company's gateway, is in
17
17
 
18
18
  ## Your notes for the project
19
19
 
20
- Keep the project's notes in a folder of their own, for example `~/notes/<project>`. It can start empty. isorite
20
+ Keep the project's notes in a folder of their own, for example `~/notes/<project>`. It can start empty:
21
+ `isorite new ~/notes/<project>` makes it. isorite
21
22
  keeps what it needs in the folder's `.isorite/` (the journals of every change, and its settings), made the first
22
23
  time the folder is opened. Commit `.isorite/` with the notes, if they're in git.
23
24
 
@@ -71,7 +72,12 @@ terminal's session:
71
72
  - **Notes the agent changes** are written at once, under its name. Turn on provenance in the Note pane's header to
72
73
  see the agent's words marked, and the timeline to put a note back.
73
74
 
74
- With both off, the pane is Iso's own again.
75
+ With both off, the pane is Iso's own again. The ≡ in its header, or a click on its gauge, opens what Iso is sent:
76
+ a row for each part, with its size. These are the notes you gave and the notes Iso read, the conversation's turns,
77
+ and what is always sent. Iso keeps a note it read in hand, and sets it aside after three turns unused, as a line it
78
+ can read again. A click switches a row on or off. At the head of the conversation, one icon sums the earlier turns
79
+ up into a briefing sent instead of them, and another starts a new conversation from that briefing. Iso sums up on
80
+ its own only when a turn would be more than the model takes.
75
81
 
76
82
  ## The models
77
83
 
@@ -88,6 +94,9 @@ OpenCode doesn't know a gateway's prices, so Iso's pane shows the session's toke
88
94
 
89
95
  ## OpenCode's settings
90
96
 
97
+ OpenCode is given the base's `AGENTS.md` (in its `instructions`), when `isorite term` is named the notes, and the
98
+ knowledge base's ways through `brief_base`: how a note is written and linked, and how the base is kept.
99
+
91
100
  isorite keeps OpenCode's settings in `~/.config/isorite/opencode/`, rewritten at each start, so your own OpenCode
92
101
  settings are never read or changed. Anything of your own goes in `~/.config/isorite/opencode/local.json`, which is
93
102
  never rewritten, and is laid over what isorite writes. For example, to have OpenCode ask before every command (by
@@ -104,8 +113,11 @@ gateway, or a model served on your own machine). What it fetches, it fetches onc
104
113
 
105
114
  - isorite and OpenCode, from npm: through your company's npm mirror, if it has one.
106
115
  - The model for search by meaning (optional): `isorite model` gets it from npm too (`isorite-model-granite`), so the
107
- same mirror serves it; else from Hugging Face. On a machine with neither, copy its files from one that has them:
108
- `isorite model --from <folder>`. Every file is checked against its SHA-256.
116
+ same mirror serves it; else from its release on GitHub, else from Hugging Face. On a machine with none of them,
117
+ copy its folder from one that has it (`isorite model` there says where), or download the package from
118
+ `https://github.com/lachlanjs/isorite/releases/tag/model-granite-1.0.0` and unpack it, then
119
+ `isorite model --from <folder>`. Granite's files are checked against their SHA-256; any other compatible model is
120
+ checked by use ([setup-for-agents.md](setup-for-agents.md) says which).
109
121
 
110
122
  ## Who can reach it
111
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isorite",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "An agentic harness for understanding: your notes as a knowledge base, an Atlas of them, an agent that reads them, and study.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin/tui.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { BoxRenderable, fg, ImageRenderable, RGBA, StyledText, TextAttributes, TextRenderable, type Renderable, type TextChunk } from "@opentui/core";
7
7
  import { Plugin } from "@opencode/plugin/tui";
8
- import { createEffect, createResource, createSignal, For, onCleanup, Show, untrack } from "solid-js";
8
+ import { createEffect, createResource, createSignal, For, onCleanup, Show } from "solid-js";
9
9
  import type { ScrollBoxRenderable } from "@opentui/core";
10
10
  import { ago, anchorOf, blocks, describe, momentKey, moments, reach, type Entry, type MapModel } from "../dist/core.js";
11
11
  import { drawAtlas, runsOf, type Role } from "./oneatlas.ts";
@@ -15,7 +15,6 @@ import * as Isorite from "./isorite.ts";
15
15
  import * as Logo from "./logo.ts";
16
16
  import { lapis, quartz, routeColours } from "./palette.ts";
17
17
  import { Play, type Step } from "./play.ts";
18
- import { TOUR, type TourState } from "./tour.ts";
19
18
  import { parseSources } from "./sources.ts";
20
19
  import * as Wordmark from "./wordmark.ts";
21
20
 
@@ -265,9 +264,6 @@ export default Plugin.define({
265
264
  </text>
266
265
  ),
267
266
  });
268
- // What the tour (isorite --tutorial) waits on that nothing else counts: the agent's turns ended, Iso's answers,
269
- // and notes sent from here.
270
- const [turns, setTurns] = createSignal(0), [isoAnswers, setIsoAnswers] = createSignal(0), [sent, setSent] = createSignal(0);
271
267
 
272
268
  /** [s]: a note, or a passage of it, sent to the agent from the terminal, as from the browser: under "from you". */
273
269
  const sendToAgent = async (note: string, passage?: string) => {
@@ -275,7 +271,7 @@ export default Plugin.define({
275
271
  const said = await ctx.ui.dialog.prompt({ title: "Send to the agent", description: passage ? "the passage the cursor is on" : `the note “${(titles().get(note) ?? note).toLowerCase()}”`, placeholder: "what you want of it (optional)" });
276
272
  if (said === undefined) return;
277
273
  await Isorite.post("/api/agent/send", { note, ...(passage ? { passage } : {}), said: said.trim() })
278
- .then(() => setSent((n) => n + 1), (e: Error) => ctx.ui.toast?.show?.({ message: e.message, variant: "error" }));
274
+ .catch((e: Error) => ctx.ui.toast?.show?.({ message: e.message, variant: "error" }));
279
275
  };
280
276
 
281
277
  const stop = new AbortController();
@@ -293,8 +289,7 @@ export default Plugin.define({
293
289
  if (sessionID) void deliver(sessionID);
294
290
  return;
295
291
  }
296
- if (type === "agent-done" && typeof data.session === "string") { reading.delete(data.session); hear((n) => n + 1); setTurns((n) => n + 1); }
297
- if (type === "iso-done") setIsoAnswers((n) => n + 1);
292
+ if (type === "agent-done" && typeof data.session === "string") { reading.delete(data.session); hear((n) => n + 1); }
298
293
  if (type === "note-changed") bumpMap((n) => n + 1);
299
294
  if (type !== "agent-step" || data.agent !== Isorite.AGENT || typeof data.session !== "string") return;
300
295
  const play = plays.get(data.session) ?? new Play();
@@ -324,75 +319,6 @@ export default Plugin.define({
324
319
  return new TextRenderable(ctx.renderer, { content: new StyledText(chunks) });
325
320
  });
326
321
 
327
- // ------------------------------------------------------------ the tour (isorite --tutorial)
328
- const TUTORIAL = process.env.ISORITE_TUTORIAL === "1";
329
- const [stepAt, setStepAt] = createSignal(TUTORIAL ? 0 : TOUR.length);
330
- const [cardShown, setCardShown] = createSignal(true);
331
- const tourState = (): TourState => {
332
- heard();
333
- const route = ctx.ui.router.current(), shown = note();
334
- return {
335
- page: route.type === "plugin" ? (route.name === SPLASH ? "splash" : route.name === NOTE ? "note" : route.name) : route.type,
336
- turns: turns(),
337
- inPlay: route.type === "session" ? plays.get(route.sessionID)?.size ?? 0 : 0,
338
- atlasOpen: ctx.ui.panel.current()?.name === ATLAS,
339
- note: shownNote(),
340
- // Not known until the note is read: no step moves on for a note still opening.
341
- waiting: shown && shown.id === shownNote() ? shown.items.filter((i) => i.type === "change").length : -1,
342
- past: moment() !== null,
343
- isoAnswers: isoAnswers(),
344
- sent: sent(),
345
- };
346
- };
347
- /** Puts the step's prompt in the prompt being typed in, once it has focus and while it is empty. */
348
- const fillPrompt = (text: string) => {
349
- let tries = 0;
350
- const timer = setInterval(() => {
351
- const editor = ctx.renderer.currentFocusedEditor;
352
- if (editor && !editor.plainText.trim()) editor.insertText(text);
353
- if (editor || ++tries > 50) clearInterval(timer);
354
- }, 100);
355
- };
356
- ctx.ui.slot({
357
- append: "app",
358
- render() {
359
- if (!TUTORIAL) return null;
360
- // A step moves on when what it waits for holds, judged against what held when it began.
361
- let at = -1, began = untrack(tourState);
362
- createEffect(() => {
363
- const i = stepAt(), now = tourState(), step = TOUR[i];
364
- if (i !== at) { at = i; began = now; if (step?.fill) fillPrompt(step.fill); return; }
365
- if (step?.until?.(now, began)) setStepAt(i + 1);
366
- });
367
- ctx.keymap.layer(() => ({
368
- mode: "global",
369
- commands: [
370
- { id: "isorite.tour.next", title: "Tour: the next step", palette: true, slash: { name: "next" }, run: () => { setStepAt((i) => Math.min(i + 1, TOUR.length)); } },
371
- { id: "isorite.tour.card", title: "Tour: hide or show the card", palette: true, slash: { name: "tour" }, run: () => { setCardShown((v) => !v); } },
372
- ],
373
- }));
374
- return (
375
- <Show when={cardShown() && TOUR[stepAt()]}>
376
- {(step) => (
377
- <box position="absolute" right={2} bottom={4} width={48} zIndex={3000} flexDirection="column" paddingLeft={1} paddingRight={1}
378
- border borderStyle="rounded" borderColor={ground().growth} backgroundColor={ground().g0} title={` tour · ${stepAt() + 1} of ${TOUR.length} `}>
379
- <text fg={ground().text} wrapMode="word"><b>{step().title}</b></text>
380
- <text fg={ground().soft} wrapMode="word">{step().text}</text>
381
- <text> </text>
382
- <box flexDirection="row">
383
- <text wrapMode="none">
384
- <For each={TOUR}>{(_, k) => <span style={{ fg: k() <= stepAt() ? ground().growth : ground().line }}>{k() <= stepAt() ? "■" : "□"}</span>}</For>
385
- </text>
386
- <box flexGrow={1} />
387
- <text fg={ground().faint} wrapMode="none">{step().until ? "/tour hides" : "/next /tour hides"}</text>
388
- </box>
389
- </box>
390
- )}
391
- </Show>
392
- );
393
- },
394
- });
395
-
396
322
  ctx.ui.slot({
397
323
  append: "sidebar.content",
398
324
  render: (input) => {