clankerbend 0.1.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/DEVELOPERS.md +63 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/apps/README.md +11 -0
- package/apps/sticky-notes/README.md +11 -0
- package/apps/sticky-notes/onewhack.manifest.json +74 -0
- package/apps/sticky-notes/package.json +14 -0
- package/apps/sticky-notes/public/app.js +117 -0
- package/apps/sticky-notes/public/index.html +23 -0
- package/apps/sticky-notes/public/styles.css +84 -0
- package/apps/sticky-notes/src/sticky-notes-app.js +413 -0
- package/apps/vim-nav/README.md +126 -0
- package/apps/vim-nav/onewhack.manifest.json +69 -0
- package/apps/vim-nav/package.json +16 -0
- package/apps/vim-nav/public/app.js +276 -0
- package/apps/vim-nav/public/index.html +53 -0
- package/apps/vim-nav/public/styles.css +211 -0
- package/apps/vim-nav/server.mjs +10 -0
- package/apps/vim-nav/src/vim-nav-app.js +221 -0
- package/assets/onewhack.jpg +0 -0
- package/cli.mjs +91 -0
- package/docs/app-lifecycle.md +63 -0
- package/docs/app-manifest.md +130 -0
- package/docs/author-guide.md +126 -0
- package/docs/launcher-profiles.md +50 -0
- package/docs/protocol.md +1935 -0
- package/host/README.md +18 -0
- package/host/src/app-registry.js +315 -0
- package/host/src/codex-desktop-cdp-adapter.js +1826 -0
- package/host/src/codex-desktop-renderer-bridge.js +3536 -0
- package/host/src/index.js +1177 -0
- package/launch/profiles.mjs +93 -0
- package/launch/runtime-paths.mjs +21 -0
- package/package.json +66 -0
- package/scripts/release-npm.mjs +202 -0
- package/server.mjs +58 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
const APP_ID = "onewill.vim-nav";
|
|
2
|
+
|
|
3
|
+
const els = {
|
|
4
|
+
subtitle: document.getElementById("subtitle"),
|
|
5
|
+
openPanel: document.getElementById("open-panel"),
|
|
6
|
+
commandInput: document.getElementById("command-input"),
|
|
7
|
+
runCommand: document.getElementById("run-command"),
|
|
8
|
+
commandStatus: document.getElementById("command-status"),
|
|
9
|
+
currentIndex: document.getElementById("current-index"),
|
|
10
|
+
currentTitle: document.getElementById("current-title"),
|
|
11
|
+
currentPreview: document.getElementById("current-preview"),
|
|
12
|
+
anchorCount: document.getElementById("anchor-count"),
|
|
13
|
+
anchorList: document.getElementById("anchor-list")
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let currentState = null;
|
|
17
|
+
let search = { query: "", index: -1 };
|
|
18
|
+
let lastRenderSignature = "";
|
|
19
|
+
const token = readToken();
|
|
20
|
+
|
|
21
|
+
els.openPanel.addEventListener("click", async () => {
|
|
22
|
+
try {
|
|
23
|
+
await postJson("/onewhack/panel/open", {});
|
|
24
|
+
} catch (err) {
|
|
25
|
+
els.commandStatus.textContent = err.message;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
els.runCommand.addEventListener("click", () => runCommand(els.commandInput.value));
|
|
29
|
+
els.commandInput.addEventListener("keydown", (event) => {
|
|
30
|
+
if (event.key === "Enter") {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
runCommand(els.commandInput.value);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
bootstrap();
|
|
36
|
+
|
|
37
|
+
async function bootstrap() {
|
|
38
|
+
try {
|
|
39
|
+
const state = await getJson("/onewhack/state");
|
|
40
|
+
render(state, true);
|
|
41
|
+
connectEvents();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
els.subtitle.textContent = "disconnected";
|
|
44
|
+
els.commandStatus.textContent = err.message;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readToken() {
|
|
49
|
+
const params = new URLSearchParams(location.hash.startsWith("#") ? location.hash.slice(1) : location.hash);
|
|
50
|
+
const value = params.get("onewhack_token") || "";
|
|
51
|
+
if (value) history.replaceState(null, "", location.pathname + location.search);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function headers(extra = {}) {
|
|
56
|
+
return {
|
|
57
|
+
...extra,
|
|
58
|
+
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function connectEvents() {
|
|
63
|
+
if (!token) {
|
|
64
|
+
const events = new EventSource("/onewhack/events");
|
|
65
|
+
events.addEventListener("state", (event) => render(JSON.parse(event.data)));
|
|
66
|
+
events.addEventListener("app-state", () => getJson("/onewhack/state").then((state) => render(state)));
|
|
67
|
+
events.addEventListener("action", () => getJson("/onewhack/state").then((state) => render(state)));
|
|
68
|
+
events.addEventListener("error", () => {
|
|
69
|
+
els.subtitle.textContent = "event stream interrupted";
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const res = await fetch("/onewhack/events", { headers: headers() });
|
|
75
|
+
if (!res.ok || !res.body) throw new Error(`events failed: ${res.status}`);
|
|
76
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
77
|
+
let buffer = "";
|
|
78
|
+
while (true) {
|
|
79
|
+
const { value, done } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
buffer += value;
|
|
82
|
+
let boundary;
|
|
83
|
+
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
|
|
84
|
+
const chunk = buffer.slice(0, boundary);
|
|
85
|
+
buffer = buffer.slice(boundary + 2);
|
|
86
|
+
handleSseChunk(chunk);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleSseChunk(chunk) {
|
|
92
|
+
const lines = chunk.split(/\n/);
|
|
93
|
+
const event = lines.find((line) => line.startsWith("event: "))?.slice(7) || "message";
|
|
94
|
+
const data = lines.filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
|
|
95
|
+
if (!data || event === "heartbeat") return;
|
|
96
|
+
if (event === "state") render(JSON.parse(data));
|
|
97
|
+
if (event === "app-state" || event === "action") getJson("/onewhack/state").then((state) => render(state));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runCommand(raw) {
|
|
101
|
+
const command = String(raw || "").trim();
|
|
102
|
+
if (!command) return;
|
|
103
|
+
const anchors = currentAnchors();
|
|
104
|
+
const current = currentAnchor();
|
|
105
|
+
let result = null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (/^:\d+$/.test(command)) {
|
|
109
|
+
result = await jumpToIndex(Number(command.slice(1)) - 1);
|
|
110
|
+
} else if (/^\d+$/.test(command)) {
|
|
111
|
+
result = await jumpToIndex(Number(command) - 1);
|
|
112
|
+
} else if (command === "j") {
|
|
113
|
+
result = await runAction("vim.jumpRelative", { delta: 1 });
|
|
114
|
+
} else if (command === "k") {
|
|
115
|
+
result = await runAction("vim.jumpRelative", { delta: -1 });
|
|
116
|
+
} else if (command === "gg") {
|
|
117
|
+
result = await runAction("vim.jumpIndex", { index: 0 });
|
|
118
|
+
} else if (command === "G") {
|
|
119
|
+
result = await runAction("vim.jumpIndex", { index: -1 });
|
|
120
|
+
} else if (command === "{") {
|
|
121
|
+
result = await runAction("vim.jumpRole", { role: "user", direction: -1 });
|
|
122
|
+
} else if (command === "}") {
|
|
123
|
+
result = await runAction("vim.jumpRole", { role: "user", direction: 1 });
|
|
124
|
+
} else if (command.startsWith("/")) {
|
|
125
|
+
search.query = command.slice(1).trim();
|
|
126
|
+
search.index = -1;
|
|
127
|
+
result = await runAction("vim.search", { query: search.query, direction: 1 });
|
|
128
|
+
} else if (command === "n") {
|
|
129
|
+
result = await searchAgain(1);
|
|
130
|
+
} else if (command === "N") {
|
|
131
|
+
result = await searchAgain(-1);
|
|
132
|
+
} else if (command === ":latest assistant") {
|
|
133
|
+
result = await runAction("vim.latestRole", { role: "assistant" });
|
|
134
|
+
} else if (command === ":latest user") {
|
|
135
|
+
result = await runAction("vim.latestRole", { role: "user" });
|
|
136
|
+
} else {
|
|
137
|
+
result = { ok: false, error: `unknown command: ${command}` };
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
result = { ok: false, error: err.message };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
els.commandStatus.textContent = result?.ok ? result.message || "ok" : result?.error || "failed";
|
|
144
|
+
if (result?.ok) els.commandInput.value = "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function currentAnchors() {
|
|
148
|
+
return currentState?.transcript?.anchors || [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function currentApp() {
|
|
152
|
+
return (currentState?.apps || []).find((app) => app.appId === APP_ID) || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function currentAnchor() {
|
|
156
|
+
const anchors = currentAnchors();
|
|
157
|
+
const selected = currentState?.selection?.anchorId;
|
|
158
|
+
return anchors.find((anchor) => anchor.anchorId === selected) ||
|
|
159
|
+
anchors.find((anchor) => anchor.visible) ||
|
|
160
|
+
anchors[0] ||
|
|
161
|
+
null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function currentIndex() {
|
|
165
|
+
const anchors = currentAnchors();
|
|
166
|
+
const current = currentAnchor();
|
|
167
|
+
return Math.max(0, anchors.findIndex((anchor) => anchor.anchorId === current?.anchorId));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function jumpToIndex(index) {
|
|
171
|
+
const anchors = currentAnchors();
|
|
172
|
+
if (!anchors.length) return { ok: false, error: "no anchors" };
|
|
173
|
+
const clamped = Math.max(0, Math.min(anchors.length - 1, index));
|
|
174
|
+
return jumpToAnchor(anchors[clamped].anchorId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function jumpToAnchor(anchorId) {
|
|
178
|
+
return runAction("vim.jump", { anchorId, behavior: "smooth", block: "center" }, anchorId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function searchAgain(direction) {
|
|
182
|
+
if (!search.query) return { ok: false, error: "empty search" };
|
|
183
|
+
return runAction("vim.search", { query: search.query, direction });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runAction(type, payload = {}, anchorId) {
|
|
187
|
+
const action = {
|
|
188
|
+
actionId: `${type}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
189
|
+
appId: APP_ID,
|
|
190
|
+
type,
|
|
191
|
+
anchorId: anchorId || payload.anchorId,
|
|
192
|
+
entryId: (anchorId || payload.anchorId) ? `nav:${anchorId || payload.anchorId}` : undefined,
|
|
193
|
+
payload,
|
|
194
|
+
requestedAt: new Date().toISOString()
|
|
195
|
+
};
|
|
196
|
+
const result = await postJson(`/onewhack/apps/${encodeURIComponent(APP_ID)}/actions`, { action });
|
|
197
|
+
if (result.ok) getJson("/onewhack/state").then((state) => render(state));
|
|
198
|
+
return result.ok ? { ok: true, message: result.status || "ok" } : { ok: false, error: result.error || "action failed" };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function render(state, force = false) {
|
|
202
|
+
if (!state) return;
|
|
203
|
+
currentState = state;
|
|
204
|
+
const app = currentApp();
|
|
205
|
+
const signature = JSON.stringify({
|
|
206
|
+
sequence: state.sequence,
|
|
207
|
+
anchors: (state.transcript?.anchors || []).map((anchor) => `${anchor.anchorId}:${anchor.visible ? 1 : 0}`),
|
|
208
|
+
selection: state.selection?.anchorId,
|
|
209
|
+
appServer: state.appServer?.status,
|
|
210
|
+
appUpdatedAt: app?.updatedAt
|
|
211
|
+
});
|
|
212
|
+
if (!force && signature === lastRenderSignature) return;
|
|
213
|
+
lastRenderSignature = signature;
|
|
214
|
+
|
|
215
|
+
const anchors = currentAnchors();
|
|
216
|
+
const current = currentAnchor();
|
|
217
|
+
const index = anchors.findIndex((anchor) => anchor.anchorId === current?.anchorId);
|
|
218
|
+
els.subtitle.textContent = `${state.desktop?.cdpStatus || "unknown"} · ${app?.status || "unknown"} · app-server ${state.appServer?.status || "unknown"}`;
|
|
219
|
+
els.anchorCount.textContent = String(anchors.length);
|
|
220
|
+
els.currentIndex.textContent = index >= 0 ? String(index + 1).padStart(2, "0") : "--";
|
|
221
|
+
els.currentTitle.textContent = current ? `${current.inferredRole || current.kind} · ${current.visible ? "visible" : "offscreen"}` : "No transcript anchor";
|
|
222
|
+
els.currentPreview.textContent = current?.textPreview || "Waiting for transcript anchors.";
|
|
223
|
+
renderAnchors(anchors, current);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderAnchors(anchors, current) {
|
|
227
|
+
els.anchorList.replaceChildren(...anchors.slice(0, 300).map((anchor) => {
|
|
228
|
+
const item = document.createElement("li");
|
|
229
|
+
item.className = anchor.anchorId === current?.anchorId ? "is-selected" : "";
|
|
230
|
+
const button = document.createElement("button");
|
|
231
|
+
button.type = "button";
|
|
232
|
+
button.addEventListener("click", () => jumpToAnchor(anchor.anchorId));
|
|
233
|
+
const idx = document.createElement("span");
|
|
234
|
+
idx.className = "idx";
|
|
235
|
+
idx.textContent = anchor.indexed === false ? "?" : String(anchor.order).padStart(2, "0");
|
|
236
|
+
const body = document.createElement("span");
|
|
237
|
+
body.className = "anchor-body";
|
|
238
|
+
const title = document.createElement("strong");
|
|
239
|
+
title.textContent = anchor.inferredRole || anchor.kind;
|
|
240
|
+
const preview = document.createElement("span");
|
|
241
|
+
preview.textContent = anchor.textPreview || "No preview";
|
|
242
|
+
body.append(title, preview);
|
|
243
|
+
button.append(idx, body);
|
|
244
|
+
item.append(button);
|
|
245
|
+
return item;
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function empty(text) {
|
|
250
|
+
const span = document.createElement("span");
|
|
251
|
+
span.className = "empty";
|
|
252
|
+
span.textContent = text;
|
|
253
|
+
return span;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function getJson(url) {
|
|
257
|
+
const res = await fetch(url, { headers: headers() });
|
|
258
|
+
return unwrap(res);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function postJson(url, body) {
|
|
262
|
+
const res = await fetch(url, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: headers({ "content-type": "application/json" }),
|
|
265
|
+
body: JSON.stringify(body)
|
|
266
|
+
});
|
|
267
|
+
return unwrap(res);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function unwrap(res) {
|
|
271
|
+
const envelope = await res.json();
|
|
272
|
+
if (!envelope.ok) {
|
|
273
|
+
throw new Error(envelope.error?.message || `request failed: ${res.status}`);
|
|
274
|
+
}
|
|
275
|
+
return envelope.data;
|
|
276
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Vim Nav</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="shell">
|
|
11
|
+
<header class="topbar">
|
|
12
|
+
<div>
|
|
13
|
+
<strong>Vim Nav</strong>
|
|
14
|
+
<p id="subtitle">Transcript keyboard navigation</p>
|
|
15
|
+
</div>
|
|
16
|
+
<button id="open-panel" type="button">Panel</button>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<section class="command">
|
|
20
|
+
<label for="command-input">Command</label>
|
|
21
|
+
<div class="command-row">
|
|
22
|
+
<input id="command-input" autocomplete="off" spellcheck="false" placeholder=":42, /search, gg, G, j, k">
|
|
23
|
+
<button id="run-command" type="button">Run</button>
|
|
24
|
+
</div>
|
|
25
|
+
<p id="command-status"></p>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="cheatsheet" aria-label="Vim commands">
|
|
29
|
+
<span><kbd>j</kbd>/<kbd>k</kbd> next/prev</span>
|
|
30
|
+
<span><kbd>gg</kbd>/<kbd>G</kbd> first/last</span>
|
|
31
|
+
<span><kbd>{</kbd>/<kbd>}</kbd> turns</span>
|
|
32
|
+
<span><kbd>/</kbd> search</span>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<section class="current">
|
|
36
|
+
<span id="current-index">--</span>
|
|
37
|
+
<div>
|
|
38
|
+
<strong id="current-title">No transcript anchor</strong>
|
|
39
|
+
<p id="current-preview">Waiting for Codex transcript anchors.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<section class="anchors">
|
|
44
|
+
<div class="section-head">
|
|
45
|
+
<h1>Anchors</h1>
|
|
46
|
+
<span id="anchor-count">0</span>
|
|
47
|
+
</div>
|
|
48
|
+
<ol id="anchor-list"></ol>
|
|
49
|
+
</section>
|
|
50
|
+
</main>
|
|
51
|
+
<script src="./app.js"></script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--bg: #080a0c;
|
|
4
|
+
--panel: #0d1318;
|
|
5
|
+
--panel-2: #111a21;
|
|
6
|
+
--text: #eef3f6;
|
|
7
|
+
--muted: #94a4ae;
|
|
8
|
+
--border: #2a3742;
|
|
9
|
+
--accent: #8fc7d4;
|
|
10
|
+
--ok: #8ccf9d;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button,
|
|
26
|
+
input {
|
|
27
|
+
font: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button {
|
|
31
|
+
border: 1px solid var(--border);
|
|
32
|
+
border-radius: 6px;
|
|
33
|
+
background: #131d25;
|
|
34
|
+
color: var(--text);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
button:hover {
|
|
39
|
+
background: #1a2933;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.shell {
|
|
43
|
+
display: grid;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
padding: 12px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.topbar,
|
|
49
|
+
.section-head {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
gap: 8px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.topbar {
|
|
57
|
+
border-bottom: 1px solid #202b33;
|
|
58
|
+
padding-bottom: 10px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.topbar strong {
|
|
62
|
+
display: block;
|
|
63
|
+
font-size: 15px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
p,
|
|
67
|
+
h1 {
|
|
68
|
+
margin: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
h1 {
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.topbar p,
|
|
76
|
+
.command p,
|
|
77
|
+
.current p,
|
|
78
|
+
.anchor-body span,
|
|
79
|
+
.empty {
|
|
80
|
+
color: var(--muted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#open-panel,
|
|
84
|
+
#run-command {
|
|
85
|
+
padding: 5px 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.command,
|
|
89
|
+
.current,
|
|
90
|
+
.anchors {
|
|
91
|
+
border: 1px solid var(--border);
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
background: var(--panel);
|
|
94
|
+
padding: 10px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.command label {
|
|
98
|
+
display: block;
|
|
99
|
+
margin-bottom: 6px;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
font-size: 11px;
|
|
102
|
+
text-transform: uppercase;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.command-row {
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
108
|
+
gap: 6px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#command-input {
|
|
112
|
+
min-width: 0;
|
|
113
|
+
border: 1px solid var(--border);
|
|
114
|
+
border-radius: 6px;
|
|
115
|
+
background: #070b0e;
|
|
116
|
+
color: var(--text);
|
|
117
|
+
padding: 7px 8px;
|
|
118
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#command-status {
|
|
122
|
+
min-height: 18px;
|
|
123
|
+
margin-top: 6px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.cheatsheet {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
gap: 5px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cheatsheet span {
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 3px;
|
|
136
|
+
border: 1px solid var(--border);
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
background: transparent;
|
|
139
|
+
color: var(--muted);
|
|
140
|
+
padding: 3px 7px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
kbd {
|
|
144
|
+
border: 1px solid #3a4a55;
|
|
145
|
+
border-radius: 4px;
|
|
146
|
+
background: #0a1014;
|
|
147
|
+
color: #d7e5eb;
|
|
148
|
+
padding: 1px 4px;
|
|
149
|
+
font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.current {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
155
|
+
gap: 10px;
|
|
156
|
+
align-items: center;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#current-index,
|
|
160
|
+
.idx {
|
|
161
|
+
display: inline-grid;
|
|
162
|
+
place-items: center;
|
|
163
|
+
width: 30px;
|
|
164
|
+
height: 24px;
|
|
165
|
+
border: 1px solid rgba(143, 199, 212, .52);
|
|
166
|
+
border-radius: 999px;
|
|
167
|
+
color: #cfe6ec;
|
|
168
|
+
font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#anchor-list {
|
|
172
|
+
display: grid;
|
|
173
|
+
gap: 4px;
|
|
174
|
+
margin: 8px 0 0;
|
|
175
|
+
padding: 0;
|
|
176
|
+
list-style: none;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#anchor-list li button {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
182
|
+
gap: 8px;
|
|
183
|
+
align-items: start;
|
|
184
|
+
width: 100%;
|
|
185
|
+
min-width: 0;
|
|
186
|
+
border: 1px solid transparent;
|
|
187
|
+
background: transparent;
|
|
188
|
+
padding: 6px;
|
|
189
|
+
text-align: left;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#anchor-list li.is-selected button {
|
|
193
|
+
border-color: var(--accent);
|
|
194
|
+
background: #102129;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.anchor-body {
|
|
198
|
+
display: grid;
|
|
199
|
+
min-width: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.anchor-body strong,
|
|
203
|
+
.anchor-body span {
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
text-overflow: ellipsis;
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.anchor-body strong {
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { launchOneWhackCodex } from "../../server.mjs";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
await launchOneWhackCodex({
|
|
8
|
+
mock: process.argv.includes("--mock"),
|
|
9
|
+
runDir: join(__dirname, "run")
|
|
10
|
+
});
|