isorite 0.12.0 → 0.13.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.
- package/README.md +6 -5
- package/dist/main.js +5688 -8903
- package/docs/setup-for-agents.md +44 -102
- package/docs/setup.md +23 -87
- package/docs/terminal.md +54 -55
- package/guide/AGENTS.md +4 -0
- package/guide/atlas/find-what-it-rests-on.md +9 -0
- package/guide/atlas/the-atlas.md +21 -0
- package/guide/atlas/the-switches/satellites.svg +17 -0
- package/guide/atlas/the-switches.md +27 -0
- package/guide/iso/agents-md.md +13 -0
- package/guide/iso/artifacts/the-guide.csv +8 -0
- package/guide/iso/artifacts/the-guide.html +35 -0
- package/guide/iso/artifacts.md +15 -0
- package/guide/iso/ask-iso.md +7 -0
- package/guide/iso/give-it-notes.md +9 -0
- package/guide/iso/in-hand.md +18 -0
- package/guide/iso/iso-writes-marked.md +15 -0
- package/guide/iso/iso.md +17 -0
- package/guide/iso/let-it-write.md +11 -0
- package/guide/iso/write-agents-md.md +11 -0
- package/guide/notes/a-note-is-markdown.md +17 -0
- package/guide/notes/a-notes-details.md +23 -0
- package/guide/notes/diagrams-and-code.md +43 -0
- package/guide/notes/every-change-is-kept.md +14 -0
- package/guide/notes/folders.md +11 -0
- package/guide/notes/go-back-a-version.md +9 -0
- package/guide/notes/link-your-notes.md +9 -0
- package/guide/notes/links-and-their-kinds.md +23 -0
- package/guide/notes/make-move-delete.md +9 -0
- package/guide/notes/write-in-your-notes.md +9 -0
- package/guide/out-of-the-base/web-and-code-links.md +13 -0
- package/guide/questions-and-problems/ask-a-question.md +9 -0
- package/guide/questions-and-problems/how-iso-reads-you.md +9 -0
- package/guide/questions-and-problems/problems.md +15 -0
- package/guide/questions-and-problems/questions.md +27 -0
- package/guide/questions-and-problems/resolve-a-question.md +9 -0
- package/guide/questions-and-problems/why-markdown-links.md +7 -0
- package/guide/solve-this-one.md +11 -0
- package/guide/start-here.md +39 -0
- package/guide/terminal/let-the-agent-remember.md +9 -0
- package/guide/terminal/memory.md +13 -0
- package/guide/terminal/your-own-agent.md +24 -0
- package/guide/your-notes.md +6 -0
- package/guide/your-own-base/keeping-a-base-well.md +16 -0
- package/guide/your-own-base/starting-a-base.md +21 -0
- package/package.json +4 -7
- package/web/_app/immutable/assets/0.BEjn1Bfl.css +1 -0
- package/web/_app/immutable/assets/4.fOHsAKBF.css +1 -0
- package/web/_app/immutable/assets/drawn.D6lJzDcc.css +1 -0
- package/web/_app/immutable/chunks/{BK6pLgdi.js → 3fCuxe18.js} +1 -1
- package/web/_app/immutable/chunks/{D5XlJBy8.js → B-DzK7Gb.js} +1 -1
- package/web/_app/immutable/chunks/{BuUzVkG2.js → B35sQTcK.js} +1 -1
- package/web/_app/immutable/chunks/{D4YEq54N.js → B6uHgLdT.js} +1 -1
- package/web/_app/immutable/chunks/{Dh1lBxfx.js → BA78_vo5.js} +1 -1
- package/web/_app/immutable/chunks/{tUSDgbpy.js → BCGaqnao.js} +1 -1
- package/web/_app/immutable/chunks/BCyLp7pS.js +1 -0
- package/web/_app/immutable/chunks/{EMo1IeTW.js → BK31dpt6.js} +1 -1
- package/web/_app/immutable/chunks/BOxykeIc.js +1 -0
- package/web/_app/immutable/chunks/{DlXrOzlh.js → BT_btfh0.js} +1 -1
- package/web/_app/immutable/chunks/{Xm0YTnxV.js → BWE_HS0c.js} +1 -1
- package/web/_app/immutable/chunks/{BLmF1B2i.js → BXBhaEho.js} +1 -1
- package/web/_app/immutable/chunks/{BAclEN0O.js → Bb_N98K9.js} +1 -1
- package/web/_app/immutable/chunks/{gPtCGHiy.js → Bd-WxALw.js} +1 -1
- package/web/_app/immutable/chunks/{BA8Hpxhg.js → BhEqJU15.js} +1 -1
- package/web/_app/immutable/chunks/{DkxkvOjx.js → BiATTH-R.js} +1 -1
- package/web/_app/immutable/chunks/{D5vocsTl.js → Bj-5n5Pb.js} +1 -1
- package/web/_app/immutable/chunks/{DRX-TQzj.js → BqipEJXw.js} +1 -1
- package/web/_app/immutable/chunks/{CiMp12cC.js → Bt-fHApV.js} +1 -1
- package/web/_app/immutable/chunks/{DXTK-eyo.js → BvPxdlBc.js} +1 -1
- package/web/_app/immutable/chunks/{DfCZPwp1.js → C2npZzkn.js} +1 -1
- package/web/_app/immutable/chunks/{DF89zyH-.js → C5Z-Y7Lu.js} +1 -1
- package/web/_app/immutable/chunks/{CD6xPodA.js → C8h1oeAZ.js} +1 -1
- package/web/_app/immutable/chunks/{DDCg82xL.js → C8nK3Jvr.js} +1 -1
- package/web/_app/immutable/chunks/{CFEI5lV9.js → CAlIuau0.js} +1 -1
- package/web/_app/immutable/chunks/{9z49435t.js → CFf_dAzX.js} +1 -1
- package/web/_app/immutable/chunks/{D3-rqfsM.js → CMIt1Ma8.js} +1 -1
- package/web/_app/immutable/chunks/{-2Meesge.js → CMikI1NP.js} +3 -3
- package/web/_app/immutable/chunks/{Czz9qOh8.js → CT4_O9X_.js} +1 -1
- package/web/_app/immutable/chunks/{DX4bJJO0.js → CTDYUZPz.js} +1 -1
- package/web/_app/immutable/chunks/{By4S_gy5.js → CdY1G_wk.js} +1 -1
- package/web/_app/immutable/chunks/Cd_SqIWq.js +523 -0
- package/web/_app/immutable/chunks/{DBC2xjOo.js → CdmC_l9F.js} +1 -1
- package/web/_app/immutable/chunks/{DbNETcPs.js → Cf-5I5_6.js} +1 -1
- package/web/_app/immutable/chunks/Cl5gQm2e.js +1 -0
- package/web/_app/immutable/chunks/{BWVMfAMh.js → CryuQgNh.js} +1 -1
- package/web/_app/immutable/chunks/{CT_Ry4q8.js → Ct_EK1xu.js} +1 -1
- package/web/_app/immutable/chunks/{BymwHy7o.js → CtkUy_15.js} +1 -1
- package/web/_app/immutable/chunks/{Dpa4Syyy.js → CzYPMYd4.js} +1 -1
- package/web/_app/immutable/chunks/{RlApGeew.js → D1LK3xRu.js} +1 -1
- package/web/_app/immutable/chunks/{DXK7PB4g.js → D6sVhEz3.js} +1 -1
- package/web/_app/immutable/chunks/{jTXVas70.js → D9JwUycj.js} +1 -1
- package/web/_app/immutable/chunks/{rghE0CVz.js → DCZYeMlH.js} +1 -1
- package/web/_app/immutable/chunks/{RFxekmX-.js → DDAG55ce.js} +1 -1
- package/web/_app/immutable/chunks/{CyFLqlvP.js → DFuPQaNa.js} +1 -1
- package/web/_app/immutable/chunks/{BXAOBcPb.js → DHJ8FcFD.js} +1 -1
- package/web/_app/immutable/chunks/{Bt7VOT5H.js → DKqMzyyE.js} +1 -1
- package/web/_app/immutable/chunks/{Bq2GBAJ0.js → DRcTlHZP.js} +1 -1
- package/web/_app/immutable/chunks/{C_amgxV1.js → DjUnonHY.js} +1 -1
- package/web/_app/immutable/chunks/{whhiiiga.js → Djr0h_v1.js} +1 -1
- package/web/_app/immutable/chunks/{vfDvz3qP.js → DrVtdHUW.js} +1 -1
- package/web/_app/immutable/chunks/{Uo6irDQW.js → O98JkczU.js} +74 -74
- package/web/_app/immutable/chunks/{oWM_kgYq.js → PsfIQCJ1.js} +1 -1
- package/web/_app/immutable/chunks/{Cl5zRP_n.js → TanDZqdf.js} +1 -1
- package/web/_app/immutable/chunks/{D95RKsvN.js → UDPiRmKc.js} +1 -1
- package/web/_app/immutable/chunks/WLe0MT1c.js +1 -0
- package/web/_app/immutable/chunks/{BTclWEfx.js → ZwM5k8r0.js} +1 -1
- package/web/_app/immutable/chunks/e2LGvGEb.js +3 -0
- package/web/_app/immutable/chunks/ej_0XxR7.js +1 -0
- package/web/_app/immutable/chunks/{CjtKPNlv.js → kG4HctVa.js} +1 -1
- package/web/_app/immutable/chunks/{CQd9D8Nc.js → l1LsK1sB.js} +1 -1
- package/web/_app/immutable/chunks/{D_jbSWJl.js → s_hW4oA1.js} +1 -1
- package/web/_app/immutable/entry/app.DM_AWBgH.js +2 -0
- package/web/_app/immutable/entry/start.BLhIKDcR.js +1 -0
- package/web/_app/immutable/nodes/0.xSbkmYPn.js +2 -0
- package/web/_app/immutable/nodes/1.BD2wJdoH.js +1 -0
- package/web/_app/immutable/nodes/2.shPqwtS_.js +1 -0
- package/web/_app/immutable/nodes/3.e_5kWaf_.js +1 -0
- package/web/_app/immutable/nodes/4.BEMEU6Jz.js +15 -0
- package/web/_app/immutable/nodes/5.DxaO-e4O.js +1 -0
- package/web/_app/immutable/nodes/6.C9ia8cDm.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +16 -19
- package/dist/core.js +0 -2464
- package/plugin/assets/mark-lapis.png +0 -0
- package/plugin/assets/mark-quartz.png +0 -0
- package/plugin/assets/wordmark-lapis.png +0 -0
- package/plugin/assets/wordmark-quartz.png +0 -0
- package/plugin/isorite.ts +0 -65
- package/plugin/logo.ts +0 -99
- package/plugin/notepage.tsx +0 -165
- package/plugin/notes.ts +0 -10
- package/plugin/noteview.ts +0 -60
- package/plugin/oneatlas.ts +0 -83
- package/plugin/palette.ts +0 -21
- package/plugin/play.ts +0 -104
- package/plugin/server.ts +0 -76
- package/plugin/sources.ts +0 -18
- package/plugin/tui.tsx +0 -556
- package/plugin/wordmark.ts +0 -180
- package/web/_app/immutable/assets/0.BXPxx6Q0.css +0 -1
- package/web/_app/immutable/assets/4.CUdclsPi.css +0 -1
- package/web/_app/immutable/assets/5.rBvyRuST.css +0 -1
- package/web/_app/immutable/assets/Drawing.Q7J9MyIo.css +0 -1
- package/web/_app/immutable/assets/NoteEditor.S3q5lmKq.css +0 -1
- package/web/_app/immutable/assets/Timeline.LsRXYIBv.css +0 -1
- package/web/_app/immutable/chunks/4kgFUGOH.js +0 -1
- package/web/_app/immutable/chunks/8SS1q0nk.js +0 -7
- package/web/_app/immutable/chunks/Bk_JinGf.js +0 -1
- package/web/_app/immutable/chunks/BqrMZ9MX.js +0 -1
- package/web/_app/immutable/chunks/DThNJvmp.js +0 -1
- package/web/_app/immutable/chunks/DbeMVOH4.js +0 -1
- package/web/_app/immutable/chunks/DsQj0LyB.js +0 -523
- package/web/_app/immutable/chunks/DshHzxgH.js +0 -1
- package/web/_app/immutable/chunks/Iqr2CudW.js +0 -3
- package/web/_app/immutable/entry/app.CvK8zypr.js +0 -2
- package/web/_app/immutable/entry/start.Dcgm6sQd.js +0 -1
- package/web/_app/immutable/nodes/0.0TdRBEoR.js +0 -26
- package/web/_app/immutable/nodes/1.Dchqgt4j.js +0 -1
- package/web/_app/immutable/nodes/2.C0CMUdSu.js +0 -1
- package/web/_app/immutable/nodes/3.MjtWDMWd.js +0 -1
- package/web/_app/immutable/nodes/4.hhS0SQta.js +0 -2
- package/web/_app/immutable/nodes/5.C33_cw2J.js +0 -9
- package/web/_app/immutable/nodes/6.owNfwwRl.js +0 -1
- package/web/_app/immutable/nodes/7.BpxHVHoY.js +0 -1
- /package/web/_app/immutable/assets/{7.C00vyVPo.css → 6.C00vyVPo.css} +0 -0
package/plugin/tui.tsx
DELETED
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
// isorite's TUI half: the views OpenCode does not have. The splash (`Terminal-Splash`), and in the sidebar the
|
|
3
|
-
// notes in play: what the agent found in the knowledge base and how far each got (`Terminal`, `Terminal-Chat`).
|
|
4
|
-
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { BoxRenderable, fg, ImageRenderable, RGBA, StyledText, TextAttributes, TextRenderable, type Renderable, type TextChunk } from "@opentui/core";
|
|
7
|
-
import { Plugin } from "@opencode/plugin/tui";
|
|
8
|
-
import { createEffect, createResource, createSignal, For, onCleanup, Show } from "solid-js";
|
|
9
|
-
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
10
|
-
import { ago, anchorOf, blocks, describe, momentKey, moments, reach, type Entry, type MapModel } from "../dist/core.js";
|
|
11
|
-
import { drawAtlas, runsOf, type Role } from "./oneatlas.ts";
|
|
12
|
-
import { noteView, type ViewChange } from "./noteview.ts";
|
|
13
|
-
import { NotePage, fromWhom, type NoteData } from "./notepage.tsx";
|
|
14
|
-
import * as Isorite from "./isorite.ts";
|
|
15
|
-
import * as Logo from "./logo.ts";
|
|
16
|
-
import { lapis, quartz, routeColours } from "./palette.ts";
|
|
17
|
-
import { Play, type Step } from "./play.ts";
|
|
18
|
-
import { parseSources } from "./sources.ts";
|
|
19
|
-
import * as Wordmark from "./wordmark.ts";
|
|
20
|
-
|
|
21
|
-
const SPLASH = "isorite-splash";
|
|
22
|
-
const NOTE = "isorite-note";
|
|
23
|
-
const ATLAS = "isorite-atlas";
|
|
24
|
-
|
|
25
|
-
// Below the keys on the board: rule 90 from a single cell, in the line colour.
|
|
26
|
-
const FIELD = [
|
|
27
|
-
" : : ",
|
|
28
|
-
" : : : : ",
|
|
29
|
-
" : : ",
|
|
30
|
-
" : : ",
|
|
31
|
-
" : ",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
/** Opens a URL in the browser, where the platform has a way to. */
|
|
35
|
-
function openInBrowser(url: string): void {
|
|
36
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
37
|
-
spawn(command, [url], { stdio: "ignore", detached: true }).on("error", () => {}).unref();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export default Plugin.define({
|
|
41
|
-
id: "isorite",
|
|
42
|
-
setup(ctx) {
|
|
43
|
-
const ground = () => (ctx.themeMode === "light" ? quartz : lapis);
|
|
44
|
-
// The mark and the wordmark in quarter blocks, which every terminal draws itself, the wordmark with its shadow.
|
|
45
|
-
// ISORITE_LOGO chooses another style of characters, or "image": images, in a terminal that shows them (kitty's
|
|
46
|
-
// graphics protocol, or sixel), their edges at 45° as the board and the browser draw them.
|
|
47
|
-
const logoChoice = process.env.ISORITE_LOGO;
|
|
48
|
-
const style = (logoChoice && logoChoice !== "image" ? logoChoice : "quadrant") as Logo.Style;
|
|
49
|
-
const [capabilities, setCapabilities] = createSignal(ctx.renderer.capabilities);
|
|
50
|
-
ctx.renderer.on("capabilities", () => setCapabilities(ctx.renderer.capabilities));
|
|
51
|
-
const protocol = (): "kitty" | "sixel" | null => {
|
|
52
|
-
if (logoChoice !== "image") return null;
|
|
53
|
-
const caps = capabilities();
|
|
54
|
-
return caps?.kitty_graphics ? "kitty" : caps?.sixel && ctx.renderer.resolution ? "sixel" : null;
|
|
55
|
-
};
|
|
56
|
-
const ASSETS = new URL("./assets/", import.meta.url);
|
|
57
|
-
/** The mark or the wordmark as an image, `width` by `height` cells, for the ground in use. */
|
|
58
|
-
const picture = (name: "mark" | "wordmark", width: number, height: number) =>
|
|
59
|
-
new ImageRenderable(ctx.renderer, { source: new URL(`${name}-${ctx.themeMode === "light" ? "quartz" : "lapis"}.png`, ASSETS), width, height, fit: "fit", protocol: protocol()! });
|
|
60
|
-
|
|
61
|
-
// The notes in play, by session, from the server's events; titles from its listing, asked again when notes change.
|
|
62
|
-
const plays = new Map<string, Play>();
|
|
63
|
-
const [heard, hear] = createSignal(0);
|
|
64
|
-
const [titles, setTitles] = createSignal(new Map<string, string>());
|
|
65
|
-
const listTitles = () => Isorite.get<Isorite.Listing>("/api/notes").then((l) => setTitles(new Map(l.notes.map((n) => [n.id, n.title]))), () => undefined);
|
|
66
|
-
void listTitles();
|
|
67
|
-
// What the person sends from the browser goes into the session on screen, to be read with the next message there
|
|
68
|
-
// (resume: false), and is kept under "from you". With no session on screen it waits for one.
|
|
69
|
-
interface Sent { id: string; note: string; title: string; said: string; text: string }
|
|
70
|
-
const fromYou = new Map<string, Sent[]>();
|
|
71
|
-
const waiting: Sent[] = [];
|
|
72
|
-
const deliver = async (sessionID: string) => {
|
|
73
|
-
while (waiting.length) {
|
|
74
|
-
const sent = waiting[0]!;
|
|
75
|
-
try {
|
|
76
|
-
await ctx.client.session.synthetic({ sessionID, text: sent.text, description: `from you: ${sent.title}`, resume: false });
|
|
77
|
-
} catch { return; }
|
|
78
|
-
waiting.shift();
|
|
79
|
-
fromYou.set(sessionID, [...(fromYou.get(sessionID) ?? []), sent]);
|
|
80
|
-
hear((n) => n + 1);
|
|
81
|
-
await Isorite.post("/api/agent/received", { id: sent.id, agent: Isorite.AGENT }).catch(() => undefined);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
const onScreen = () => { const route = ctx.ui.router.current(); return route.type === "session" ? route.sessionID : null; };
|
|
85
|
-
|
|
86
|
-
// ------------------------------------------------------------ the note view (Terminal-Note)
|
|
87
|
-
const [shownNote, setShownNote] = createSignal("");
|
|
88
|
-
const [noteVersion, bumpNote] = createSignal(0);
|
|
89
|
-
const [current, setCurrent] = createSignal("");
|
|
90
|
-
// The block the cursor is on, where [/] asks; the moment of the timeline shown, if any; Iso writing here.
|
|
91
|
-
const [cursor, setCursor] = createSignal(0);
|
|
92
|
-
const [moment, setMoment] = createSignal<{ key: string; said: string; ago: string } | null>(null);
|
|
93
|
-
const [asking, setAsking] = createSignal(false);
|
|
94
|
-
let back: Parameters<typeof ctx.ui.router.navigate>[0] = { type: "home" };
|
|
95
|
-
let scroll: ScrollBoxRenderable | undefined;
|
|
96
|
-
const openNote = (id: string) => {
|
|
97
|
-
const route = ctx.ui.router.current();
|
|
98
|
-
// A copy: the router changes its route in place as it goes.
|
|
99
|
-
if (!(route.type === "plugin" && route.name === NOTE))
|
|
100
|
-
back = route.type === "plugin" ? { type: "plugin", name: route.name, data: { ...route.data } } : route.type === "session" ? { type: "session", sessionID: route.sessionID } : { type: "home" };
|
|
101
|
-
setCurrent("");
|
|
102
|
-
setCursor(0);
|
|
103
|
-
setMoment(null);
|
|
104
|
-
setShownNote(id);
|
|
105
|
-
ctx.ui.router.navigate({ type: "plugin", name: NOTE, data: { id } });
|
|
106
|
-
};
|
|
107
|
-
const loadNote = async (id: string, at: { key: string; said: string; ago: string } | null): Promise<NoteData> => {
|
|
108
|
-
const q = `?id=${encodeURIComponent(id)}`;
|
|
109
|
-
const [note, entries] = await Promise.all([Isorite.get<{ text: string }>(`/api/note${q}`), Isorite.get<Entry[]>(`/api/journal${q}`)]);
|
|
110
|
-
const title = titles().get(id) ?? id.split("/").at(-1)!;
|
|
111
|
-
const common = { id, title, entries };
|
|
112
|
-
if (!at) return { ...common, text: note.text, items: noteView(note.text, entries) };
|
|
113
|
-
// At a moment of the timeline: the note as it was then, read only, its changes then waiting left out.
|
|
114
|
-
const then = await Isorite.get<{ text: string }>(`/api/version${q}&entry=${encodeURIComponent(at.key)}`);
|
|
115
|
-
const upTo = entries.filter((e) => e.id <= at.key);
|
|
116
|
-
return { ...common, text: then.text, at: { said: at.said, ago: at.ago }, items: noteView(then.text, upTo).filter((i) => i.type === "block") };
|
|
117
|
-
};
|
|
118
|
-
const [note] = createResource(() => (shownNote() ? { id: shownNote(), v: noteVersion(), at: moment() } : null), ({ id, at }) => loadNote(id, at));
|
|
119
|
-
const blockCount = () => note()?.items.filter((i) => i.type === "block").length ?? 0;
|
|
120
|
-
const moveCursor = (by: number) => {
|
|
121
|
-
const next = Math.max(0, Math.min(blockCount() - 1, cursor() + by));
|
|
122
|
-
setCursor(next);
|
|
123
|
-
scroll?.scrollChildIntoView(`block-${next}`);
|
|
124
|
-
};
|
|
125
|
-
const askHere = async () => {
|
|
126
|
-
const shown = note(), id = shownNote();
|
|
127
|
-
if (!shown || shown.at) return;
|
|
128
|
-
const all = blocks(shown.text), k = Math.min(cursor(), all.length - 1);
|
|
129
|
-
const asked = await ctx.ui.dialog.prompt({ title: "Ask Iso here", description: k >= 0 ? `after “${all[k]!.text.split("\n")[0]!.slice(0, 60)}”` : "at the top", placeholder: "what you want written here" });
|
|
130
|
-
if (!asked?.trim()) return;
|
|
131
|
-
await Isorite.post("/api/iso/ask", { note: id, form: "insert", after: k >= 0 ? anchorOf(all, k) : null, before: k + 1 < all.length ? anchorOf(all, k + 1) : null, asked: asked.trim(), tier: "mid" })
|
|
132
|
-
.catch((e: Error) => ctx.ui.toast?.show?.({ message: e.message, variant: "error" }));
|
|
133
|
-
};
|
|
134
|
-
const showTimeline = async () => {
|
|
135
|
-
const shown = note();
|
|
136
|
-
if (!shown) return;
|
|
137
|
-
const all = moments(shown.entries).reverse();
|
|
138
|
-
const key = await ctx.ui.dialog.select({ title: "The note's timeline", options: all.map((m) => ({ title: describe(m), value: momentKey(m), description: ago(m.at) })) });
|
|
139
|
-
const chosen = all.find((m) => momentKey(m) === key);
|
|
140
|
-
if (chosen) setMoment({ key: momentKey(chosen), said: describe(chosen), ago: ago(chosen.at) });
|
|
141
|
-
};
|
|
142
|
-
const changes = () => (note()?.items.filter((i): i is ViewChange => i.type === "change") ?? []);
|
|
143
|
-
// The current change: the one chosen, while it waits; else the first.
|
|
144
|
-
const chosen = () => changes().find((c) => c.key === current()) ?? changes()[0];
|
|
145
|
-
const settle = async (how: "keep" | "discard") => {
|
|
146
|
-
const c = chosen(), id = shownNote();
|
|
147
|
-
if (!c) return;
|
|
148
|
-
await Isorite.post(`/api/proposal/${how}?id=${encodeURIComponent(id)}`, { proposal: c.proposal, ...(c.region === undefined ? {} : { region: c.region }) })
|
|
149
|
-
.catch((e: Error) => ctx.ui.toast?.show?.({ message: e.message, variant: "error" }));
|
|
150
|
-
};
|
|
151
|
-
const edit = async () => {
|
|
152
|
-
const c = chosen(), id = shownNote();
|
|
153
|
-
if (!c) return;
|
|
154
|
-
const text = await ctx.ui.dialog.prompt({ title: `Edit ${fromWhom(c.who)}'s change, then keep it`, value: c.text });
|
|
155
|
-
if (text === undefined) return;
|
|
156
|
-
// A rewrite is revised whole: this region's new text, the rest as they are.
|
|
157
|
-
const entry = note()?.entries.find((e) => e.id === c.proposal) as { regions?: { new: string }[] } | undefined;
|
|
158
|
-
const revised = c.region === undefined ? text : (entry?.regions ?? []).map((r, k) => (k === c.region ? text : r.new));
|
|
159
|
-
try {
|
|
160
|
-
await Isorite.post(`/api/proposal/revise?id=${encodeURIComponent(id)}`, { proposal: c.proposal, text: revised });
|
|
161
|
-
await settle("keep");
|
|
162
|
-
} catch (e) { ctx.ui.toast?.show?.({ message: (e as Error).message, variant: "error" }); }
|
|
163
|
-
};
|
|
164
|
-
ctx.ui.router.register({
|
|
165
|
-
name: NOTE,
|
|
166
|
-
render: () => <NotePage note={note} current={() => (note()?.at ? "" : (chosen()?.key ?? ""))} cursor={cursor} asking={asking} ground={ground} base={baseName()} scroll={(s) => { scroll = s; }} />,
|
|
167
|
-
});
|
|
168
|
-
const [baseName, setBaseName] = createSignal("");
|
|
169
|
-
void Isorite.get<Isorite.Session>("/api/session").then((s) => setBaseName(s.name), () => undefined);
|
|
170
|
-
|
|
171
|
-
// ------------------------------------------------------------ one note's Atlas (Terminal-Atlas), in the session's panel
|
|
172
|
-
/** The note the agent is reading now, by session, while its turn goes on. */
|
|
173
|
-
const reading = new Map<string, string>();
|
|
174
|
-
const [atlasNote, setAtlasNote] = createSignal("");
|
|
175
|
-
const [mapVersion, bumpMap] = createSignal(0);
|
|
176
|
-
const [map] = createResource(mapVersion, () => Isorite.get<{ model: MapModel }>("/api/map").then((m) => m.model, () => null));
|
|
177
|
-
const openAtlas = (id: string) => {
|
|
178
|
-
setAtlasNote(id);
|
|
179
|
-
ctx.ui.panel.open(ATLAS, { presentation: "panel" });
|
|
180
|
-
// The chat keeps the keys; the panel takes them when it is focused (click it, or leader and →).
|
|
181
|
-
setTimeout(() => ctx.keymap.dispatch("pane.focus.left"), 0);
|
|
182
|
-
};
|
|
183
|
-
ctx.ui.slot({
|
|
184
|
-
append: "session.panel",
|
|
185
|
-
render: (input) => {
|
|
186
|
-
const routes = () => (ctx.themeMode === "light" ? routeColours.quartz : routeColours.lapis);
|
|
187
|
-
const colour = (role: Role) => (role === "req" ? routes().req : role === "bld" ? routes().bld : role === "gold" ? ground().growth : role === "ls" ? ground().ls : role === "faint" ? ground().faint : ground().text);
|
|
188
|
-
const title = (id: string) => (titles().get(id) ?? id.split("/").at(-1)!).toLowerCase();
|
|
189
|
-
const play = () => (heard(), plays.get(input.sessionID));
|
|
190
|
-
const now = () => (heard(), reading.get(input.sessionID));
|
|
191
|
-
// The note shown: the one chosen, else the one the agent read last, else the first in play.
|
|
192
|
-
const focal = () => atlasNote() || now() || play()?.rows(title)[0]?.id || "";
|
|
193
|
-
const near = () => {
|
|
194
|
-
const model = map(), id = focal();
|
|
195
|
-
if (!model || !id) return null;
|
|
196
|
-
const r = reach(model, id), direct = r.requires.filter((n) => r.depth[n] === 1);
|
|
197
|
-
return { direct, further: r.requires.length - direct.length, builds: r.builds };
|
|
198
|
-
};
|
|
199
|
-
const grid = () => {
|
|
200
|
-
const n = near(), id = focal();
|
|
201
|
-
if (!n) return [];
|
|
202
|
-
const box = (b: string) => ({ id: b, title: title(b), ...(b === now() ? { gold: true } : {}) });
|
|
203
|
-
return drawAtlas(box(id), n.direct.map(box), n.builds.map(box), Math.max(46, input.width - 6));
|
|
204
|
-
};
|
|
205
|
-
ctx.keymap.layer(() => ({
|
|
206
|
-
enabled: () => input.focused && input.name === ATLAS,
|
|
207
|
-
commands: [
|
|
208
|
-
{ id: "isorite.atlas.next", title: "Next note in play", bind: "a", run: () => { const ids = play()?.rows(title).map((r) => r.id) ?? []; if (ids.length) setAtlasNote(ids[(ids.indexOf(focal()) + 1) % ids.length]!); } },
|
|
209
|
-
{ id: "isorite.atlas.open", title: "Open the note", bind: "n", run: () => { if (focal()) openNote(focal()); } },
|
|
210
|
-
{ id: "isorite.atlas.send", title: "Send the note to the agent", bind: "s", run: () => void sendToAgent(focal()) },
|
|
211
|
-
{ id: "isorite.atlas.browser", title: "The whole map in the browser", bind: "b", run: () => openInBrowser(`${Isorite.url}/#/atlas`) },
|
|
212
|
-
{ id: "isorite.atlas.close", title: "Close the atlas", bind: "escape", run: () => input.close() },
|
|
213
|
-
],
|
|
214
|
-
}));
|
|
215
|
-
return (
|
|
216
|
-
<Show when={input.name === ATLAS}>
|
|
217
|
-
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={1} paddingTop={1} border={["left"]} borderColor={ground().line} backgroundColor={ground().g0}>
|
|
218
|
-
<Show when={focal()} fallback={<text fg={ground().faint} wrapMode="word">No note yet: ask the agent about your notes, or name one with /atlas <words>.</text>}>
|
|
219
|
-
<box flexDirection="row">
|
|
220
|
-
<text fg={ground().faint} wrapMode="none">{"atlas · "}</text>
|
|
221
|
-
<text fg={ground().text} wrapMode="none" flexShrink={1} minWidth={0}>{title(focal())}</text>
|
|
222
|
-
<box flexGrow={1} />
|
|
223
|
-
<text fg={ground().faint} wrapMode="none">{"[a] next note in play"}</text>
|
|
224
|
-
</box>
|
|
225
|
-
<text> </text>
|
|
226
|
-
<Show when={near()}>
|
|
227
|
-
{(n) => (
|
|
228
|
-
<>
|
|
229
|
-
<text wrapMode="none">
|
|
230
|
-
<span style={{ fg: routes().req }}>{n().direct.length ? "it requires" : "it requires nothing"}</span>
|
|
231
|
-
<span>{" ".repeat(Math.max(1, (grid()[0]?.length ?? 60) - 12 - (n().direct.length ? 11 : 19)))}</span>
|
|
232
|
-
<span style={{ fg: routes().bld }}>{n().builds.length ? "builds on it" : ""}</span>
|
|
233
|
-
</text>
|
|
234
|
-
<For each={grid()}>
|
|
235
|
-
{(row) => <text wrapMode="none"><For each={runsOf(row)}>{(r) => <span style={{ fg: colour(r.role), ...(r.bold ? { attributes: 1 } : {}) }}>{r.text}</span>}</For></text>}
|
|
236
|
-
</For>
|
|
237
|
-
<text> </text>
|
|
238
|
-
<box flexDirection="row">
|
|
239
|
-
<text fg={ground().faint} wrapMode="none">{n().further ? `${n().further} more ${n().further === 1 ? "is" : "are"} required further back` : ""}</text>
|
|
240
|
-
<box flexGrow={1} />
|
|
241
|
-
<text fg={ground().faint} wrapMode="none">{"[b] the whole map in the browser"}</text>
|
|
242
|
-
</box>
|
|
243
|
-
</>
|
|
244
|
-
)}
|
|
245
|
-
</Show>
|
|
246
|
-
<Show when={now()}>{(id) => <text wrapMode="word"><span style={{ fg: ground().growth }}>{"◆ "}</span><span style={{ fg: ground().soft }}>{"OpenCode is reading "}</span><span style={{ fg: ground().growth }}>{title(id())}</span></text>}</Show>
|
|
247
|
-
<Show when={input.focused}><text fg={ground().faint}>{"[n] open the note [s] send to agent esc closes"}</text></Show>
|
|
248
|
-
</Show>
|
|
249
|
-
</box>
|
|
250
|
-
</Show>
|
|
251
|
-
);
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// ------------------------------------------------------------ the status line, and sending from the terminal
|
|
256
|
-
/** How many browser pages have isorite open: the Atlas follows the agent only where one does. */
|
|
257
|
-
const [browsers, setBrowsers] = createSignal(0);
|
|
258
|
-
ctx.ui.slot({
|
|
259
|
-
append: "prompt.footer.status",
|
|
260
|
-
render: () => (
|
|
261
|
-
<text wrapMode="none">
|
|
262
|
-
<span style={{ fg: ground().faint }}>{baseName()}</span>
|
|
263
|
-
<Show when={browsers() > 0}><span style={{ fg: ground().growth }}>{" ◆ "}</span><span style={{ fg: ground().faint }}>{"atlas following"}</span></Show>
|
|
264
|
-
</text>
|
|
265
|
-
),
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
/** [s]: a note, or a passage of it, sent to the agent from the terminal, as from the browser: under "from you". */
|
|
269
|
-
const sendToAgent = async (note: string, passage?: string) => {
|
|
270
|
-
if (!note) return;
|
|
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)" });
|
|
272
|
-
if (said === undefined) return;
|
|
273
|
-
await Isorite.post("/api/agent/send", { note, ...(passage ? { passage } : {}), said: said.trim() })
|
|
274
|
-
.catch((e: Error) => ctx.ui.toast?.show?.({ message: e.message, variant: "error" }));
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const stop = new AbortController();
|
|
278
|
-
Isorite.listen((type, data) => {
|
|
279
|
-
if (type === "note-changed") void listTitles();
|
|
280
|
-
if (type === "watchers") setBrowsers(Number(data.browsers) || 0);
|
|
281
|
-
if ((type === "journal" || type === "note-changed") && data.id === shownNote()) bumpNote((n) => n + 1);
|
|
282
|
-
if (type.startsWith("iso-") && data.note === shownNote()) {
|
|
283
|
-
setAsking(type === "iso-start" || type === "iso-step" || type === "iso-text");
|
|
284
|
-
if (type === "iso-failed") ctx.ui.toast?.show?.({ message: `Iso could not answer: ${String(data.message)}`, variant: "error" });
|
|
285
|
-
}
|
|
286
|
-
if (type === "agent-sent") {
|
|
287
|
-
waiting.push({ id: String(data.id), note: String(data.note), title: String(data.title), said: String(data.said ?? ""), text: String(data.text) });
|
|
288
|
-
const sessionID = onScreen();
|
|
289
|
-
if (sessionID) void deliver(sessionID);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (type === "agent-done" && typeof data.session === "string") { reading.delete(data.session); hear((n) => n + 1); }
|
|
293
|
-
if (type === "note-changed") bumpMap((n) => n + 1);
|
|
294
|
-
if (type !== "agent-step" || data.agent !== Isorite.AGENT || typeof data.session !== "string") return;
|
|
295
|
-
const play = plays.get(data.session) ?? new Play();
|
|
296
|
-
plays.set(data.session, play);
|
|
297
|
-
play.add(data.step as Step);
|
|
298
|
-
const step = data.step as Step;
|
|
299
|
-
if (step.opened && !step.failed) reading.set(data.session, step.opened);
|
|
300
|
-
hear((n) => n + 1);
|
|
301
|
-
}, stop.signal);
|
|
302
|
-
|
|
303
|
-
// The sources an answer ends with, drawn as the board draws a citation; the notes they name are used.
|
|
304
|
-
ctx.markdown.registerCodeBlockRenderer("isorite", (token) => {
|
|
305
|
-
const sources = parseSources(token.text);
|
|
306
|
-
if (!sources.length) return null;
|
|
307
|
-
const chunks: TextChunk[] = [];
|
|
308
|
-
sources.forEach((s, k) => {
|
|
309
|
-
const title = (titles().get(s.note) ?? s.note.split("/").at(-1)!).toLowerCase();
|
|
310
|
-
chunks.push(fg(ground().growth)(`${k ? "\n" : ""}[${s.n}]`), fg(ground().text)(` ${title}`));
|
|
311
|
-
if (s.section) chunks.push(fg(ground().faint)(` § ${s.section.toLowerCase()}`));
|
|
312
|
-
});
|
|
313
|
-
// Drawing happens in the session on screen: its notes in play are the ones to mark.
|
|
314
|
-
queueMicrotask(() => {
|
|
315
|
-
const route = ctx.ui.router.current();
|
|
316
|
-
const play = route.type === "session" ? plays.get(route.sessionID) : undefined;
|
|
317
|
-
if (play?.cite(sources.map((s) => s.note))) hear((n) => n + 1);
|
|
318
|
-
});
|
|
319
|
-
return new TextRenderable(ctx.renderer, { content: new StyledText(chunks) });
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
ctx.ui.slot({
|
|
323
|
-
append: "sidebar.content",
|
|
324
|
-
render: (input) => {
|
|
325
|
-
const play = () => (heard(), plays.get(input.sessionID));
|
|
326
|
-
const rows = () => play()?.rows((id) => titles().get(id) ?? id.split("/").at(-1)!) ?? [];
|
|
327
|
-
const summary = () => {
|
|
328
|
-
const s = play()?.summary();
|
|
329
|
-
return s ? [`${s.found} found`, s.followed ? `${s.followed} by links` : "", s.used ? `${s.used} used` : ""].filter(Boolean).join(" · ") : "";
|
|
330
|
-
};
|
|
331
|
-
const state = (row: { state: string }) => (row.state === "used" ? ground().growth : row.state === "read" ? ground().soft : ground().faint);
|
|
332
|
-
const sent = () => (heard(), fromYou.get(input.sessionID) ?? []);
|
|
333
|
-
return (
|
|
334
|
-
<box flexDirection="column">
|
|
335
|
-
<Show when={rows().length > 0}>
|
|
336
|
-
<box border borderStyle="rounded" borderColor={ground().ls} title=" notes in play " paddingLeft={1} paddingRight={1} flexDirection="column">
|
|
337
|
-
<text fg={ground().faint} wrapMode="none">{summary()}</text>
|
|
338
|
-
<For each={rows()}>
|
|
339
|
-
{(row) => (
|
|
340
|
-
<box flexDirection="row" minWidth={0} onMouseUp={() => openNote(row.id)}>
|
|
341
|
-
<text fg={ground().line} wrapMode="none" flexShrink={0}>{row.prefix}</text>
|
|
342
|
-
<text fg={ground().text} wrapMode="word" flexShrink={1} minWidth={0}>{row.label}</text>
|
|
343
|
-
<box flexGrow={1} minWidth={1} />
|
|
344
|
-
<text fg={state(row)} wrapMode="none" flexShrink={0}>{row.state === "used" ? "◆ used" : row.state}</text>
|
|
345
|
-
</box>
|
|
346
|
-
)}
|
|
347
|
-
</For>
|
|
348
|
-
</box>
|
|
349
|
-
</Show>
|
|
350
|
-
<Show when={sent().length > 0}>
|
|
351
|
-
<box border borderStyle="rounded" borderColor={ground().ls} title=" from you " paddingLeft={1} paddingRight={1} flexDirection="column">
|
|
352
|
-
<For each={sent()}>
|
|
353
|
-
{(s) => (
|
|
354
|
-
<box flexDirection="column">
|
|
355
|
-
<text fg={ground().text} wrapMode="word">{s.note.replace(/-/g, " ").replace(/\//g, " / ")}</text>
|
|
356
|
-
<Show when={s.said}><text fg={ground().faint} wrapMode="word">{`“${s.said}”`}</text></Show>
|
|
357
|
-
</box>
|
|
358
|
-
)}
|
|
359
|
-
</For>
|
|
360
|
-
</box>
|
|
361
|
-
</Show>
|
|
362
|
-
</box>
|
|
363
|
-
);
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
ctx.ui.router.register({
|
|
368
|
-
name: SPLASH,
|
|
369
|
-
render: () => {
|
|
370
|
-
const [base] = createResource(() => Isorite.base().catch((e: Error) => ({ error: e.message })));
|
|
371
|
-
const line = () => {
|
|
372
|
-
const b = base();
|
|
373
|
-
if (!b) return " ";
|
|
374
|
-
if ("error" in b) return `isorite is not answering at ${Isorite.url}`;
|
|
375
|
-
return `${b.name} · ${b.notes} ${b.notes === 1 ? "note" : "notes"}`;
|
|
376
|
-
};
|
|
377
|
-
return (
|
|
378
|
-
<box flexDirection="column" alignItems="center" justifyContent="center" width="100%" height="100%">
|
|
379
|
-
<Show when={protocol()} keyed fallback={
|
|
380
|
-
<For each={Logo.rows(style)}>
|
|
381
|
-
{(runs) => (
|
|
382
|
-
<text>
|
|
383
|
-
<For each={runs}>{(run) => <span style={{ fg: run.growth ? ground().growth : ground().text }}>{run.text}</span>}</For>
|
|
384
|
-
</text>
|
|
385
|
-
)}
|
|
386
|
-
</For>
|
|
387
|
-
}>
|
|
388
|
-
{(_: string) => <box width={Logo.width * 2} height={Logo.height} ref={(b) => b.add(picture("mark", Logo.width * 2, Logo.height))} />}
|
|
389
|
-
</Show>
|
|
390
|
-
<text> </text>
|
|
391
|
-
<text fg={ground().text}><b>{" i s o r i t e"}</b></text>
|
|
392
|
-
<text> </text>
|
|
393
|
-
<text fg={ground().soft}>{line()}</text>
|
|
394
|
-
<text> </text>
|
|
395
|
-
<text>
|
|
396
|
-
<span style={{ fg: ground().g0, bg: ground().text }}>{" [enter] open "}</span>
|
|
397
|
-
<span style={{ fg: ground().soft }}>{" [a] atlas in the browser [?] keys [q] quit"}</span>
|
|
398
|
-
</text>
|
|
399
|
-
<text> </text>
|
|
400
|
-
<text> </text>
|
|
401
|
-
<For each={FIELD}>{(row) => <text fg={ground().line}>{row}</text>}</For>
|
|
402
|
-
</box>
|
|
403
|
-
);
|
|
404
|
-
},
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Commands live in a component, as OpenCode's own do; made in setup they do not appear.
|
|
408
|
-
ctx.ui.slot({
|
|
409
|
-
append: "app",
|
|
410
|
-
render() {
|
|
411
|
-
const onSplash = () => {
|
|
412
|
-
const route = ctx.ui.router.current();
|
|
413
|
-
return route.type === "plugin" && route.name === SPLASH;
|
|
414
|
-
};
|
|
415
|
-
const onNote = () => { const route = ctx.ui.router.current(); return route.type === "plugin" && route.name === NOTE; };
|
|
416
|
-
// The base mode, not global: a dialog opened from the note (asking Iso, the timeline) takes the keys typed in it.
|
|
417
|
-
ctx.keymap.layer(() => ({
|
|
418
|
-
enabled: onNote,
|
|
419
|
-
commands: [
|
|
420
|
-
{ id: "isorite.note.next", title: "Next change", bind: "n", run: () => { const all = changes(), k = all.findIndex((c) => c.key === chosen()?.key); if (all.length) setCurrent(all[(k + 1) % all.length]!.key); } },
|
|
421
|
-
{ id: "isorite.note.keep", title: "Keep the change", bind: "k", run: () => { if (!moment()) void settle("keep"); } },
|
|
422
|
-
{ id: "isorite.note.edit", title: "Edit the change, then keep it", bind: "e", run: () => { if (!moment()) void edit(); } },
|
|
423
|
-
{ id: "isorite.note.remove", title: "Remove the change", bind: "r", run: () => { if (!moment()) void settle("discard"); } },
|
|
424
|
-
{ id: "isorite.note.send", title: "Send the paragraph to the agent", bind: "s", run: () => { const shown = note(); if (shown && !shown.at) { const b = blocks(shown.text)[cursor()]; void sendToAgent(shown.id, b?.text); } } },
|
|
425
|
-
{ id: "isorite.note.atlas", title: "Its atlas", bind: "a", run: () => { const id = shownNote(); ctx.ui.router.navigate(back); if (back.type === "session") setTimeout(() => openAtlas(id), 0); else setAtlasNote(id); } },
|
|
426
|
-
{ id: "isorite.note.browser", title: "The note in the browser", bind: "b", run: () => openInBrowser(`${Isorite.url}/#/n/${shownNote()}`) },
|
|
427
|
-
{ id: "isorite.note.back", title: "Back", bind: "q", run: () => (moment() ? setMoment(null) : ctx.ui.router.navigate(back)) },
|
|
428
|
-
{ id: "isorite.note.escape", title: "Back", bind: "escape", run: () => (moment() ? setMoment(null) : ctx.ui.router.navigate(back)) },
|
|
429
|
-
{ id: "isorite.note.down", title: "Next block", bind: "down", run: () => moveCursor(1) },
|
|
430
|
-
{ id: "isorite.note.up", title: "Block before", bind: "up", run: () => moveCursor(-1) },
|
|
431
|
-
{ id: "isorite.note.ask", title: "Ask Iso here", bind: "/", run: () => void askHere() },
|
|
432
|
-
{ id: "isorite.note.timeline", title: "The note's timeline", bind: "t", run: () => void showTimeline() },
|
|
433
|
-
{ id: "isorite.note.pagedown", title: "Page down", bind: "pagedown", run: () => scroll?.scrollBy(0.8 * (scroll?.height ?? 20)) },
|
|
434
|
-
{ id: "isorite.note.pageup", title: "Page up", bind: "pageup", run: () => scroll?.scrollBy(-0.8 * (scroll?.height ?? 20)) },
|
|
435
|
-
],
|
|
436
|
-
}));
|
|
437
|
-
ctx.keymap.layer(() => ({
|
|
438
|
-
mode: "global",
|
|
439
|
-
commands: [{
|
|
440
|
-
id: "isorite.atlas.show", title: "One note's atlas, beside the chat", palette: true, slash: { name: "atlas", arguments: true },
|
|
441
|
-
run: async (input) => {
|
|
442
|
-
const words = input?.trim();
|
|
443
|
-
// A note titled with exactly these words comes before the search's first hit, which may only mention them.
|
|
444
|
-
const hits = words ? await Isorite.get<{ id: string; title: string }[]>(`/api/search?q=${encodeURIComponent(words)}`).catch(() => []) : [];
|
|
445
|
-
const id = words ? (hits.find((h) => h.title.toLowerCase() === words.toLowerCase()) ?? hits[0])?.id : "";
|
|
446
|
-
if (words && !id) { ctx.ui.toast?.show?.({ message: `no note matches “${words}”`, variant: "warning" }); return; }
|
|
447
|
-
openAtlas(id ?? "");
|
|
448
|
-
},
|
|
449
|
-
}, {
|
|
450
|
-
// OpenCode's own /web pairs a browser with its page, by a link that works once; opened again, that page asks
|
|
451
|
-
// for a password nobody was given (M21). isorite's /web, listed before it, opens isorite's page; OpenCode's
|
|
452
|
-
// pairing is still /pair.
|
|
453
|
-
id: "isorite.web", title: "isorite in the browser (OpenCode's pairing: /pair)", palette: true, slash: { name: "web" },
|
|
454
|
-
run: () => openInBrowser(Isorite.url + "/"),
|
|
455
|
-
}, {
|
|
456
|
-
id: "isorite.note.open", title: "Open a note of the knowledge base", palette: true, slash: { name: "note", arguments: true },
|
|
457
|
-
run: async (input) => {
|
|
458
|
-
const words = input?.trim();
|
|
459
|
-
if (words) {
|
|
460
|
-
const hits = await Isorite.get<{ id: string }[]>(`/api/search?q=${encodeURIComponent(words)}`).catch(() => []);
|
|
461
|
-
const exact = titles().has(words.replace(/^\/+|\.md$/g, "")) ? words.replace(/^\/+|\.md$/g, "") : undefined;
|
|
462
|
-
const id = exact ?? hits[0]?.id;
|
|
463
|
-
if (id) openNote(id); else ctx.ui.toast?.show?.({ message: `no note matches “${words}”`, variant: "warning" });
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const id = await ctx.ui.dialog.select({
|
|
467
|
-
title: "Open a note", placeholder: "its title",
|
|
468
|
-
options: [...titles()].map(([value, title]) => ({ title, value, description: value.split("/").slice(0, -1).join(" / ") })),
|
|
469
|
-
});
|
|
470
|
-
if (id) openNote(id);
|
|
471
|
-
},
|
|
472
|
-
}],
|
|
473
|
-
}));
|
|
474
|
-
// On OpenCode's home, isorite's name where its own is.
|
|
475
|
-
createEffect(() => {
|
|
476
|
-
if (ctx.ui.router.current().type !== "home") return;
|
|
477
|
-
const timer = setInterval(swapLogo, 200);
|
|
478
|
-
swapLogo();
|
|
479
|
-
onCleanup(() => clearInterval(timer));
|
|
480
|
-
});
|
|
481
|
-
// Anything sent while no session was on screen goes to the first one opened.
|
|
482
|
-
createEffect(() => { const sessionID = onScreen(); if (sessionID && waiting.length) void deliver(sessionID); });
|
|
483
|
-
ctx.keymap.layer(() => ({
|
|
484
|
-
mode: "global",
|
|
485
|
-
enabled: onSplash,
|
|
486
|
-
commands: [
|
|
487
|
-
{ id: "isorite.splash.open", title: "Open", bind: "return", run: () => ctx.ui.router.navigate({ type: "home" }) },
|
|
488
|
-
{ id: "isorite.splash.atlas", title: "The Atlas in the browser", bind: "a", run: () => openInBrowser(Isorite.url + "/") },
|
|
489
|
-
{ id: "isorite.splash.keys", title: "Keys", bind: "?", run: () => ctx.keymap.dispatch("help.show") },
|
|
490
|
-
{ id: "isorite.splash.quit", title: "Quit", bind: "q", run: () => ctx.keymap.dispatch("app.exit") },
|
|
491
|
-
],
|
|
492
|
-
}));
|
|
493
|
-
return null;
|
|
494
|
-
},
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// OpenCode's home draws its name in its own letters, and no slot reaches it: its box is found among the renderables
|
|
498
|
-
// by the rows it draws, hidden, and isorite's wordmark drawn beside it. Should OpenCode draw it otherwise, nothing
|
|
499
|
-
// is found, and its own stays.
|
|
500
|
-
const OPEN = ["█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀"];
|
|
501
|
-
const textOf = (r: Renderable): string => (r instanceof TextRenderable ? r.plainText : r.getChildren().map((c) => textOf(c as Renderable)).join(""));
|
|
502
|
-
const findLogo = (r: Renderable): Renderable | undefined => {
|
|
503
|
-
for (const child of r.getChildren() as Renderable[]) {
|
|
504
|
-
const found = findLogo(child);
|
|
505
|
-
if (found) return found;
|
|
506
|
-
}
|
|
507
|
-
const text = textOf(r);
|
|
508
|
-
return !(r instanceof TextRenderable) && OPEN.every((row) => text.includes(row)) ? r : undefined;
|
|
509
|
-
};
|
|
510
|
-
const mix = (a: string, b: string, t: number) => {
|
|
511
|
-
const [x, y] = [RGBA.fromHex(a), RGBA.fromHex(b)];
|
|
512
|
-
return RGBA.fromValues(x.r + (y.r - x.r) * t, x.g + (y.g - x.g) * t, x.b + (y.b - x.b) * t, 1);
|
|
513
|
-
};
|
|
514
|
-
// The wordmark as the browser draws it, where the home has room for it; else the name in OpenCode's letters.
|
|
515
|
-
const wide = [...Wordmark.rows(style)[0]!.map((run) => run.text).join("")].length;
|
|
516
|
-
const roomy = () => ctx.renderer.width >= (protocol() ? 76 : wide + 6) && ctx.renderer.height >= 28;
|
|
517
|
-
const wordmark = () => {
|
|
518
|
-
const g = ground();
|
|
519
|
-
const mark = new BoxRenderable(ctx.renderer, { id: "isorite-wordmark", flexDirection: "column", alignItems: "center" });
|
|
520
|
-
const chunk = (text: string, ink: string, more: Partial<TextChunk> = {}): TextChunk => ({ __isChunk: true, text, fg: RGBA.fromHex(ink), ...more });
|
|
521
|
-
if (roomy() && protocol()) {
|
|
522
|
-
mark.add(picture("wordmark", 70, 10));
|
|
523
|
-
return mark;
|
|
524
|
-
}
|
|
525
|
-
if (roomy()) {
|
|
526
|
-
const colour = { ink: g.text, iso: g.growth, right: g.mal, shadow: g.g3 };
|
|
527
|
-
for (const row of Wordmark.rows(style)) mark.add(new TextRenderable(ctx.renderer, { content: new StyledText(row.map((run) => chunk(run.text, colour[run.role], run.bg ? { bg: RGBA.fromHex(colour[run.bg]) } : {}))), selectable: false }));
|
|
528
|
-
return mark;
|
|
529
|
-
}
|
|
530
|
-
// As OpenCode's: the first half muted, the second bold, each letter on a shade a quarter of its colour.
|
|
531
|
-
const half = (row: string, ink: string, bold: boolean): TextChunk[] => Wordmark.runs(row).map((run) =>
|
|
532
|
-
chunk(run.text, ink, { ...(run.shaded ? { bg: mix(g.g0, ink, 0.25) } : {}), ...(bold ? { attributes: TextAttributes.BOLD } : {}) }));
|
|
533
|
-
Wordmark.left.forEach((row, k) => mark.add(new TextRenderable(ctx.renderer, {
|
|
534
|
-
content: new StyledText([...half(row, g.faint, false), { __isChunk: true, text: " " }, ...half(Wordmark.right[k]!, g.text, true)]), selectable: false,
|
|
535
|
-
})));
|
|
536
|
-
return mark;
|
|
537
|
-
};
|
|
538
|
-
let drawnFor = "";
|
|
539
|
-
const swapLogo = () => {
|
|
540
|
-
const logo = findLogo(ctx.renderer.root);
|
|
541
|
-
const parent = logo?.parent;
|
|
542
|
-
if (!logo || !parent) return;
|
|
543
|
-
logo.visible = false;
|
|
544
|
-
const mine = parent.getChildren().find((c) => c.id === "isorite-wordmark");
|
|
545
|
-
const drawing = `${ctx.themeMode} ${roomy()} ${protocol()}`;
|
|
546
|
-
if (mine && drawnFor === drawing) return;
|
|
547
|
-
if (mine) { parent.remove(mine); mine.destroy(); }
|
|
548
|
-
parent.add(wordmark());
|
|
549
|
-
drawnFor = drawing;
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// The splash is shown once, when the TUI starts.
|
|
553
|
-
setTimeout(() => ctx.ui.router.navigate({ type: "plugin", name: SPLASH, data: {} }), 0);
|
|
554
|
-
return () => stop.abort();
|
|
555
|
-
},
|
|
556
|
-
});
|