@memfork/cli 0.1.33 → 0.1.35
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/dist/commands/ui-server.d.ts +18 -8
- package/dist/commands/ui-server.js +136 -90
- package/package.json +1 -1
- package/plugins/codex/skills/memforks-status/SKILL.md +6 -2
- package/plugins/cursor/rules/memforks.mdc +19 -32
- package/ui/assets/index-DSfS462x.css +1 -0
- package/ui/assets/{index-hWCeym5B.js → index-vHgHmMrf.js} +17 -17
- package/ui/assets/index-vHgHmMrf.js.map +1 -0
- package/ui/index.html +2 -2
- package/ui/assets/index-BPumM0C9.css +0 -1
- package/ui/assets/index-hWCeym5B.js.map +0 -1
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `memfork ui` — local HTTP server.
|
|
3
3
|
*
|
|
4
|
-
* Serves the pre-built React app from
|
|
5
|
-
* exposes
|
|
6
|
-
*
|
|
7
|
-
* browser bundle.
|
|
4
|
+
* Serves the pre-built React app from packages/cli/ui/ as static files and
|
|
5
|
+
* exposes API routes so the React app can query MemWal without exposing
|
|
6
|
+
* credentials in the browser bundle.
|
|
8
7
|
*
|
|
9
|
-
* GET /api/config
|
|
10
|
-
* GET /api/
|
|
11
|
-
* GET
|
|
12
|
-
* GET
|
|
8
|
+
* GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal, rateLimited, retryInSeconds }
|
|
9
|
+
* GET /api/history?branch=<b>[&force=1] → { commits[], branch, rateLimited, retryInSeconds }
|
|
10
|
+
* GET /api/facts?branch=<b>[&force=1] → { facts[], rateLimited, retryInSeconds }
|
|
11
|
+
* GET /* → index.html (SPA fallback)
|
|
12
|
+
* GET /assets/* → static file
|
|
13
|
+
*
|
|
14
|
+
* Rate-limit strategy (relayer caps at 500 weighted-req/hour):
|
|
15
|
+
* • In-memory per-namespace cache (CACHE_TTL_MS). Most poll ticks served free.
|
|
16
|
+
* • On a 429, parse retry_after_seconds, set global backoff in memory AND
|
|
17
|
+
* persist just the timestamp to .memfork/.ui-backoff.json (a plain number —
|
|
18
|
+
* no decrypted content ever written to disk). Loaded on server start so
|
|
19
|
+
* restarts during a ban don't fire immediately.
|
|
20
|
+
* • ?force=1 bypasses the TTL but still respects the backoff window.
|
|
21
|
+
* • All responses include rateLimited + retryInSeconds so the UI can show a
|
|
22
|
+
* banner instead of looking silently empty.
|
|
13
23
|
*/
|
|
14
24
|
import http from "node:http";
|
|
15
25
|
export declare function startUiServer(distDir: string, port?: number): http.Server;
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `memfork ui` — local HTTP server.
|
|
3
3
|
*
|
|
4
|
-
* Serves the pre-built React app from
|
|
5
|
-
* exposes
|
|
6
|
-
*
|
|
7
|
-
* browser bundle.
|
|
4
|
+
* Serves the pre-built React app from packages/cli/ui/ as static files and
|
|
5
|
+
* exposes API routes so the React app can query MemWal without exposing
|
|
6
|
+
* credentials in the browser bundle.
|
|
8
7
|
*
|
|
9
|
-
* GET /api/config
|
|
10
|
-
* GET /api/
|
|
11
|
-
* GET
|
|
12
|
-
* GET
|
|
8
|
+
* GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal, rateLimited, retryInSeconds }
|
|
9
|
+
* GET /api/history?branch=<b>[&force=1] → { commits[], branch, rateLimited, retryInSeconds }
|
|
10
|
+
* GET /api/facts?branch=<b>[&force=1] → { facts[], rateLimited, retryInSeconds }
|
|
11
|
+
* GET /* → index.html (SPA fallback)
|
|
12
|
+
* GET /assets/* → static file
|
|
13
|
+
*
|
|
14
|
+
* Rate-limit strategy (relayer caps at 500 weighted-req/hour):
|
|
15
|
+
* • In-memory per-namespace cache (CACHE_TTL_MS). Most poll ticks served free.
|
|
16
|
+
* • On a 429, parse retry_after_seconds, set global backoff in memory AND
|
|
17
|
+
* persist just the timestamp to .memfork/.ui-backoff.json (a plain number —
|
|
18
|
+
* no decrypted content ever written to disk). Loaded on server start so
|
|
19
|
+
* restarts during a ban don't fire immediately.
|
|
20
|
+
* • ?force=1 bypasses the TTL but still respects the backoff window.
|
|
21
|
+
* • All responses include rateLimited + retryInSeconds so the UI can show a
|
|
22
|
+
* banner instead of looking silently empty.
|
|
13
23
|
*/
|
|
14
24
|
import http from "node:http";
|
|
15
25
|
import fs from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
16
27
|
import path from "node:path";
|
|
17
28
|
import { readProjectConfig, readCredentials, MEMWAL_CONSTANTS } from "../config.js";
|
|
18
29
|
import { MemWal } from "@mysten-incubation/memwal";
|
|
19
30
|
import { branchNamespace } from "@memfork/core";
|
|
31
|
+
// ─── Static file serving ─────────────────────────────────────────────────────
|
|
20
32
|
const MIME = {
|
|
21
33
|
".html": "text/html; charset=utf-8",
|
|
22
34
|
".js": "application/javascript",
|
|
@@ -43,52 +55,82 @@ function json(res, data, status = 200) {
|
|
|
43
55
|
});
|
|
44
56
|
res.end(body);
|
|
45
57
|
}
|
|
46
|
-
|
|
47
|
-
const project = readProjectConfig();
|
|
48
|
-
const creds = readCredentials();
|
|
49
|
-
const treeId = project?.treeId ?? creds.default ?? null;
|
|
50
|
-
const network = (project?.network ?? "mainnet");
|
|
51
|
-
const stored = treeId ? creds.trees[treeId] : undefined;
|
|
52
|
-
json(res, {
|
|
53
|
-
treeId,
|
|
54
|
-
packageId: project?.packageId ?? "0xc13cc014fb8084b3468f6e5ffdc272e64ef35b7a912332eba7a0d44dd66b3121",
|
|
55
|
-
network,
|
|
56
|
-
rpcUrl: project?.rpcUrl ?? null,
|
|
57
|
-
hasMemwal: !!(stored?.memwalKey && stored?.memwalAccountId),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Per-namespace cache + global rate-limit backoff. The relayer caps usage at
|
|
62
|
-
* 500 weighted-requests/hour, so we must avoid redundant calls:
|
|
63
|
-
*
|
|
64
|
-
* • Cache each namespace's recall result for CACHE_TTL_MS — the /api/facts
|
|
65
|
-
* and /api/history endpoints both recall the same namespace, and the UI
|
|
66
|
-
* polls on a timer, so most requests are served from cache for free.
|
|
67
|
-
* • On a 429, parse retry_after_seconds and refuse ALL relayer calls until
|
|
68
|
-
* it elapses, serving stale cache instead. This stops the polling loop
|
|
69
|
-
* from continually re-arming the ban.
|
|
70
|
-
*/
|
|
58
|
+
// ─── Rate-limit state ─────────────────────────────────────────────────────────
|
|
71
59
|
const CACHE_TTL_MS = 60_000;
|
|
60
|
+
const RECALL_LIMIT = 50;
|
|
72
61
|
const recallCache = new Map();
|
|
73
62
|
let rateLimitedUntil = 0;
|
|
63
|
+
/** Path used to persist the ban timestamp across restarts. Plain number only. */
|
|
64
|
+
function backoffFilePath() {
|
|
65
|
+
try {
|
|
66
|
+
// Walk up from cwd looking for .memfork/ — same logic as config resolution.
|
|
67
|
+
let dir = process.cwd();
|
|
68
|
+
while (true) {
|
|
69
|
+
const candidate = path.join(dir, ".memfork");
|
|
70
|
+
if (fs.existsSync(candidate))
|
|
71
|
+
return path.join(candidate, ".ui-backoff.json");
|
|
72
|
+
const parent = path.dirname(dir);
|
|
73
|
+
if (parent === dir)
|
|
74
|
+
break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
// Fallback: home dir .memfork/
|
|
78
|
+
return path.join(os.homedir(), ".memfork", ".ui-backoff.json");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Load a persisted backoff timestamp so restarts during a ban stay quiet. */
|
|
85
|
+
function loadPersistedBackoff() {
|
|
86
|
+
try {
|
|
87
|
+
const p = backoffFilePath();
|
|
88
|
+
if (!p || !fs.existsSync(p))
|
|
89
|
+
return;
|
|
90
|
+
const { rateLimitedUntil: saved } = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
91
|
+
if (typeof saved === "number" && saved > Date.now()) {
|
|
92
|
+
rateLimitedUntil = saved;
|
|
93
|
+
console.warn(`[memforks] Rate-limit backoff active from previous run — pausing relayer calls for ${Math.round((saved - Date.now()) / 1000)}s.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch { /* ignore — bad file, no problem */ }
|
|
97
|
+
}
|
|
98
|
+
function persistBackoff(until) {
|
|
99
|
+
try {
|
|
100
|
+
const p = backoffFilePath();
|
|
101
|
+
if (!p)
|
|
102
|
+
return;
|
|
103
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
104
|
+
fs.writeFileSync(p, JSON.stringify({ rateLimitedUntil: until }));
|
|
105
|
+
}
|
|
106
|
+
catch { /* best-effort */ }
|
|
107
|
+
}
|
|
74
108
|
function parseRetryAfterMs(err) {
|
|
75
109
|
const m = err.match(/retry_after_seconds"?\s*:\s*(\d+)/);
|
|
76
110
|
const secs = m ? Number(m[1]) : 300;
|
|
77
111
|
return secs * 1_000;
|
|
78
112
|
}
|
|
113
|
+
function rateLimitStatus() {
|
|
114
|
+
const remaining = rateLimitedUntil - Date.now();
|
|
115
|
+
return remaining > 0
|
|
116
|
+
? { rateLimited: true, retryInSeconds: Math.ceil(remaining / 1000) }
|
|
117
|
+
: { rateLimited: false, retryInSeconds: 0 };
|
|
118
|
+
}
|
|
119
|
+
// ─── MemWal recall (cached + rate-limit-aware) ────────────────────────────────
|
|
79
120
|
/**
|
|
80
|
-
* Recall entries from a MemWal namespace
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
121
|
+
* Recall entries from a MemWal namespace. Uses the SDK so requests are
|
|
122
|
+
* properly signed (plain Bearer tokens are rejected by the relayer).
|
|
123
|
+
*
|
|
124
|
+
* - Fresh cache hit (< CACHE_TTL_MS): returns immediately, no relayer call.
|
|
125
|
+
* - In backoff window: returns stale cache (or []), no relayer call.
|
|
126
|
+
* - force=true: bypasses TTL but still respects the backoff window.
|
|
127
|
+
* - On 429: sets + persists backoff, returns stale cache.
|
|
84
128
|
*/
|
|
85
|
-
async function memwalRecall(relayer, key, accountId, namespace,
|
|
129
|
+
async function memwalRecall(relayer, key, accountId, namespace, force = false) {
|
|
86
130
|
const now = Date.now();
|
|
87
131
|
const cached = recallCache.get(namespace);
|
|
88
|
-
|
|
89
|
-
if (cached && now - cached.ts < CACHE_TTL_MS)
|
|
132
|
+
if (!force && cached && now - cached.ts < CACHE_TTL_MS)
|
|
90
133
|
return cached.data;
|
|
91
|
-
// In a rate-limit backoff window — serve stale cache (or empty), don't call.
|
|
92
134
|
if (now < rateLimitedUntil)
|
|
93
135
|
return cached?.data ?? [];
|
|
94
136
|
const mw = MemWal.create({ key, accountId, serverUrl: relayer, namespace });
|
|
@@ -97,7 +139,7 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
|
|
|
97
139
|
try {
|
|
98
140
|
const result = await mw.recall({
|
|
99
141
|
query: "facts decisions conventions setup errors architecture memory",
|
|
100
|
-
limit,
|
|
142
|
+
limit: RECALL_LIMIT,
|
|
101
143
|
});
|
|
102
144
|
for (const r of result.results) {
|
|
103
145
|
const blobId = String(r.blob_id ?? "");
|
|
@@ -114,66 +156,74 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
|
|
|
114
156
|
if (msg.includes("429")) {
|
|
115
157
|
const backoff = parseRetryAfterMs(msg);
|
|
116
158
|
rateLimitedUntil = Date.now() + backoff;
|
|
117
|
-
|
|
159
|
+
persistBackoff(rateLimitedUntil);
|
|
160
|
+
console.warn(`[memforks] Rate limit hit — pausing relayer calls for ${Math.round(backoff / 1000)}s.`);
|
|
118
161
|
}
|
|
119
|
-
// Serve stale cache if we have it; otherwise empty.
|
|
120
162
|
return cached?.data ?? [];
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
|
-
|
|
124
|
-
const branch = url.searchParams.get("branch") ?? "main";
|
|
165
|
+
function resolveMemwalCreds(branch) {
|
|
125
166
|
const project = readProjectConfig();
|
|
126
167
|
const creds = readCredentials();
|
|
127
168
|
const treeId = project?.treeId ?? creds.default;
|
|
128
169
|
const network = (project?.network ?? "mainnet");
|
|
129
170
|
const stored = treeId ? creds.trees[treeId] : undefined;
|
|
130
|
-
if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId)
|
|
131
|
-
|
|
171
|
+
if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId)
|
|
172
|
+
return null;
|
|
173
|
+
return {
|
|
174
|
+
relayer: stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer,
|
|
175
|
+
key: stored.memwalKey,
|
|
176
|
+
accountId: stored.memwalAccountId,
|
|
177
|
+
treeId,
|
|
178
|
+
namespace: (b) => branchNamespace(treeId, b),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ─── Route handlers ───────────────────────────────────────────────────────────
|
|
182
|
+
async function handleApiConfig(res) {
|
|
183
|
+
const project = readProjectConfig();
|
|
184
|
+
const creds = readCredentials();
|
|
185
|
+
const treeId = project?.treeId ?? creds.default ?? null;
|
|
186
|
+
const network = (project?.network ?? "mainnet");
|
|
187
|
+
const stored = treeId ? creds.trees[treeId] : undefined;
|
|
188
|
+
json(res, {
|
|
189
|
+
treeId,
|
|
190
|
+
packageId: project?.packageId ?? "0xc13cc014fb8084b3468f6e5ffdc272e64ef35b7a912332eba7a0d44dd66b3121",
|
|
191
|
+
network,
|
|
192
|
+
rpcUrl: project?.rpcUrl ?? null,
|
|
193
|
+
hasMemwal: !!(stored?.memwalKey && stored?.memwalAccountId),
|
|
194
|
+
...rateLimitStatus(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function handleApiFacts(res, url) {
|
|
198
|
+
const branch = url.searchParams.get("branch") ?? "main";
|
|
199
|
+
const force = url.searchParams.get("force") === "1";
|
|
200
|
+
const mc = resolveMemwalCreds(branch);
|
|
201
|
+
if (!mc) {
|
|
202
|
+
json(res, { facts: [], ...rateLimitStatus() });
|
|
132
203
|
return;
|
|
133
204
|
}
|
|
134
|
-
const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
|
|
135
|
-
const namespace = branchNamespace(treeId, branch);
|
|
136
205
|
try {
|
|
137
|
-
const facts = await memwalRecall(relayer,
|
|
138
|
-
json(res, { facts });
|
|
206
|
+
const facts = await memwalRecall(mc.relayer, mc.key, mc.accountId, mc.namespace(branch), force);
|
|
207
|
+
json(res, { facts, ...rateLimitStatus() });
|
|
139
208
|
}
|
|
140
209
|
catch (e) {
|
|
141
|
-
json(res, { facts: [], error: String(e) });
|
|
210
|
+
json(res, { facts: [], error: String(e), ...rateLimitStatus() });
|
|
142
211
|
}
|
|
143
212
|
}
|
|
144
|
-
/**
|
|
145
|
-
* GET /api/history?branch=<name>&limit=<n>
|
|
146
|
-
*
|
|
147
|
-
* Returns all off-chain CommitPayload objects stored in MemWal for this branch,
|
|
148
|
-
* sorted oldest-first. Each entry includes the MemWal blob_id plus the parsed
|
|
149
|
-
* payload fields that the UI needs (branch, author, ts_ms, delta, parent_blob_ids).
|
|
150
|
-
*
|
|
151
|
-
* The browser cannot call MemWal directly (SEAL-encrypted, key lives server-side),
|
|
152
|
-
* so this endpoint acts as the commit-history proxy.
|
|
153
|
-
*/
|
|
154
213
|
async function handleApiHistory(res, url) {
|
|
155
214
|
const branch = url.searchParams.get("branch") ?? "main";
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const network = (project?.network ?? "mainnet");
|
|
161
|
-
const stored = treeId ? creds.trees[treeId] : undefined;
|
|
162
|
-
if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId) {
|
|
163
|
-
json(res, { commits: [] });
|
|
215
|
+
const force = url.searchParams.get("force") === "1";
|
|
216
|
+
const mc = resolveMemwalCreds(branch);
|
|
217
|
+
if (!mc) {
|
|
218
|
+
json(res, { commits: [], ...rateLimitStatus() });
|
|
164
219
|
return;
|
|
165
220
|
}
|
|
166
|
-
const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
|
|
167
|
-
const namespace = branchNamespace(treeId, branch);
|
|
168
221
|
try {
|
|
169
|
-
const results = await memwalRecall(relayer,
|
|
222
|
+
const results = await memwalRecall(mc.relayer, mc.key, mc.accountId, mc.namespace(branch), force);
|
|
170
223
|
const commits = results.flatMap((entry) => {
|
|
171
|
-
const blobId = entry.blob_id;
|
|
172
|
-
const text = entry.text;
|
|
173
|
-
// Try to parse the stored text as a CommitPayload JSON.
|
|
174
224
|
let payload = null;
|
|
175
225
|
try {
|
|
176
|
-
payload = JSON.parse(text);
|
|
226
|
+
payload = JSON.parse(entry.text);
|
|
177
227
|
}
|
|
178
228
|
catch {
|
|
179
229
|
return [];
|
|
@@ -181,32 +231,29 @@ async function handleApiHistory(res, url) {
|
|
|
181
231
|
if (payload["type"] !== "commit")
|
|
182
232
|
return [];
|
|
183
233
|
return [{
|
|
184
|
-
blob_id:
|
|
234
|
+
blob_id: entry.blob_id,
|
|
185
235
|
branch: String(payload["branch"] ?? branch),
|
|
186
236
|
ts_ms: Number(payload["ts_ms"] ?? 0),
|
|
187
237
|
parent_blob_ids: payload["parent_blob_ids"] ?? [],
|
|
188
238
|
parent_blob_hashes: payload["parent_blob_hashes"] ?? [],
|
|
189
|
-
// Extract readable facts from the delta.
|
|
190
239
|
message: (() => {
|
|
191
240
|
const delta = payload["delta"];
|
|
192
241
|
const facts = delta?.["facts"];
|
|
193
|
-
return facts?.length ? facts[0] : `commit ${
|
|
242
|
+
return facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`;
|
|
194
243
|
})(),
|
|
195
244
|
delta: payload["delta"] ?? {},
|
|
196
245
|
}];
|
|
197
246
|
});
|
|
198
|
-
// Sort oldest-first by ts_ms.
|
|
199
247
|
commits.sort((a, b) => a.ts_ms - b.ts_ms);
|
|
200
|
-
json(res, { commits, branch });
|
|
248
|
+
json(res, { commits, branch, ...rateLimitStatus() });
|
|
201
249
|
}
|
|
202
250
|
catch (e) {
|
|
203
|
-
json(res, { commits: [], error: String(e) });
|
|
251
|
+
json(res, { commits: [], error: String(e), ...rateLimitStatus() });
|
|
204
252
|
}
|
|
205
253
|
}
|
|
254
|
+
// ─── Static serving ───────────────────────────────────────────────────────────
|
|
206
255
|
function serveStatic(res, distDir, urlPath) {
|
|
207
|
-
// Resolve the requested file path.
|
|
208
256
|
let filePath = path.join(distDir, urlPath);
|
|
209
|
-
// SPA fallback: no extension or file not found → serve index.html.
|
|
210
257
|
if (!path.extname(filePath) || !fs.existsSync(filePath)) {
|
|
211
258
|
filePath = path.join(distDir, "index.html");
|
|
212
259
|
}
|
|
@@ -219,17 +266,16 @@ function serveStatic(res, distDir, urlPath) {
|
|
|
219
266
|
const isImmutable = urlPath.startsWith("/assets/");
|
|
220
267
|
res.writeHead(200, {
|
|
221
268
|
"Content-Type": mimeType,
|
|
222
|
-
"Cache-Control": isImmutable
|
|
223
|
-
? "public, max-age=31536000, immutable"
|
|
224
|
-
: "no-cache",
|
|
269
|
+
"Cache-Control": isImmutable ? "public, max-age=31536000, immutable" : "no-cache",
|
|
225
270
|
"Access-Control-Allow-Origin": "*",
|
|
226
271
|
});
|
|
227
272
|
fs.createReadStream(filePath).pipe(res);
|
|
228
273
|
}
|
|
274
|
+
// ─── Server factory ───────────────────────────────────────────────────────────
|
|
229
275
|
export function startUiServer(distDir, port = 4242) {
|
|
276
|
+
loadPersistedBackoff();
|
|
230
277
|
const server = http.createServer((req, res) => {
|
|
231
278
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
232
|
-
// CORS pre-flight.
|
|
233
279
|
if (req.method === "OPTIONS") {
|
|
234
280
|
res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
|
|
235
281
|
res.end();
|
package/package.json
CHANGED
|
@@ -22,8 +22,9 @@ memfork ui # open the DAG visualizer
|
|
|
22
22
|
|
|
23
23
|
## Commit a decision on-chain
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
`memfork commit --facts` calls MemWal internally — **one command indexes the facts
|
|
26
|
+
for semantic recall AND anchors them on-chain.** Prefer this over calling
|
|
27
|
+
`memwal_remember` separately.
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
memfork commit \
|
|
@@ -33,6 +34,9 @@ memfork commit \
|
|
|
33
34
|
|
|
34
35
|
The CLI auto-detects the current Git branch; pass `--branch <name>` only to target a different one.
|
|
35
36
|
|
|
37
|
+
Only use `memwal_remember` directly for a lightweight mid-task note that does not
|
|
38
|
+
warrant an on-chain entry.
|
|
39
|
+
|
|
36
40
|
## Merge branches
|
|
37
41
|
|
|
38
42
|
When two branches need to reconcile their memory:
|
|
@@ -29,28 +29,10 @@ memwal_recall(query="auth system design decisions", namespace="branch/<current-b
|
|
|
29
29
|
|
|
30
30
|
Results with high relevance scores are verified context from prior sessions.
|
|
31
31
|
|
|
32
|
-
## Remember —
|
|
32
|
+
## Remember — prefer the CLI commit
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
memwal_remember(text="<the complete fact>", namespace="branch/<current-branch>")
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Only save facts that will be useful in future sessions:
|
|
41
|
-
- Architecture decisions
|
|
42
|
-
- Project conventions and preferences
|
|
43
|
-
- Resolved problems and their solutions
|
|
44
|
-
- Key constraints or non-obvious requirements
|
|
45
|
-
|
|
46
|
-
Do **not** save: current task state, in-progress work, temporary findings.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## On-chain commit — use the CLI
|
|
51
|
-
|
|
52
|
-
After a significant turn where you've committed an architectural decision or resolved
|
|
53
|
-
a non-trivial problem, also anchor it on-chain for immutable versioning:
|
|
34
|
+
`memfork commit --facts` calls MemWal internally, so **one command does both**:
|
|
35
|
+
indexes the facts for semantic recall AND anchors them on-chain.
|
|
54
36
|
|
|
55
37
|
```bash
|
|
56
38
|
memfork commit \
|
|
@@ -58,20 +40,16 @@ memfork commit \
|
|
|
58
40
|
--facts "<fact 1>" "<fact 2>"
|
|
59
41
|
```
|
|
60
42
|
|
|
61
|
-
The CLI auto-detects the current Git branch
|
|
62
|
-
|
|
43
|
+
The CLI auto-detects the current Git branch — omit `--branch` unless targeting
|
|
44
|
+
a different branch.
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
Use this after any significant turn: architectural decisions, resolved problems,
|
|
47
|
+
project conventions, key constraints.
|
|
66
48
|
|
|
67
|
-
|
|
49
|
+
Only use `memwal_remember` directly for a quick lightweight note that does **not**
|
|
50
|
+
warrant an on-chain entry (e.g. a transient preference noted mid-task).
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- The `memfork` CLI auto-detects the current Git branch; pass `--branch` only to override it.
|
|
72
|
-
- The `memwal_*` MCP tools have no Git context, so pass `namespace="branch/<current-branch>"` explicitly.
|
|
73
|
-
- When the user switches branches, recall from the new branch namespace.
|
|
74
|
-
- Use `memfork status` to see the current branch, on-chain tree state, and open merge proposals.
|
|
52
|
+
Do **not** save: current task state, in-progress work, temporary findings.
|
|
75
53
|
|
|
76
54
|
---
|
|
77
55
|
|
|
@@ -111,6 +89,15 @@ human governance act.
|
|
|
111
89
|
|
|
112
90
|
---
|
|
113
91
|
|
|
92
|
+
## Branch awareness
|
|
93
|
+
|
|
94
|
+
- The `memfork` CLI auto-detects the current Git branch; pass `--branch` only to override it.
|
|
95
|
+
- The `memwal_*` MCP tools have no Git context, so pass `namespace="branch/<current-branch>"` explicitly.
|
|
96
|
+
- When the user switches branches, recall from the new branch namespace.
|
|
97
|
+
- Use `memfork status` to see the current branch, on-chain tree state, and open merge proposals.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
114
101
|
## Suggesting a merge — proactive but not autonomous
|
|
115
102
|
|
|
116
103
|
You may **suggest** a merge when you notice the current branch has accumulated
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap";.topbar{display:flex;align-items:center;gap:var(--space-4);height:var(--topbar-height);padding:0 var(--space-5);border-bottom:1px solid var(--border);background:var(--bg-1);flex-shrink:0;position:relative;z-index:10}.topbar-left{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.topbar-logo{display:flex;align-items:center;gap:var(--space-2);font-weight:600;font-size:.9rem;color:var(--fg-0);letter-spacing:-.01em}.topbar-tree-id{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);background:var(--bg-2);border:1px solid var(--border);padding:.15rem .5rem;border-radius:var(--radius-sm)}.topbar-live-badge{display:flex;align-items:center;gap:5px;font-size:.72rem;font-family:var(--font-mono);padding:.15rem .55rem;border-radius:999px;border:1px solid transparent}.topbar-live-badge.live{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-live-badge.offline{background:var(--bg-2);color:var(--fg-3);border-color:var(--border)}.topbar-live-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.topbar-live-badge.live .topbar-live-dot{animation:pulse-dot 1.8s ease-in-out infinite}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.35}}.topbar-views{display:flex;align-items:center;gap:2px;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:3px;flex-shrink:0}.topbar-view-btn{padding:.2rem .75rem;font-size:.78rem;font-weight:500;color:var(--fg-2);border-radius:6px;background:transparent;border:none;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-view-btn:hover{color:var(--fg-0);background:var(--bg-3)}.topbar-view-btn.active{background:var(--bg-0);color:var(--fg-0);box-shadow:var(--shadow-sm)}.topbar-spacer{flex:1}.topbar-branch-select{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:var(--font-mono);font-size:.74rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.25rem 1.6rem .25rem .65rem;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23667066'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .6rem center;transition:border-color var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);max-width:200px}.topbar-branch-select:hover{border-color:var(--border-strong);color:var(--fg-0)}.topbar-branch-select:focus-visible{outline:1px solid var(--accent);outline-offset:1px}.topbar-right{display:flex;align-items:center;gap:var(--space-2);flex-shrink:0}.topbar-replay-btn{padding:.2rem .75rem;font-size:.75rem;font-family:var(--font-mono);font-weight:500;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-replay-btn:hover{background:var(--bg-2);color:var(--fg-0);border-color:var(--border-strong)}.topbar-replay-btn.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-refresh-btn{padding:.2rem .5rem;font-size:.85rem;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);line-height:1}.topbar-refresh-btn:hover:not(:disabled){background:var(--bg-2);color:var(--fg-0)}.topbar-refresh-btn:disabled{cursor:default}.inspector{display:flex;flex-direction:column;gap:var(--space-5);padding:var(--space-5) var(--space-5) var(--space-8);overflow-y:auto;height:100%}.inspector-header{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-4);border-bottom:1px solid var(--border)}.inspector-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-commit-id{font-family:var(--font-mono);font-size:.88rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);padding:.2rem .55rem;border-radius:var(--radius-sm);letter-spacing:.03em}.inspector-message{font-size:.875rem;color:var(--fg-0);line-height:1.55;font-weight:500}.inspector-section{display:flex;flex-direction:column;gap:var(--space-2)}.inspector-section-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;color:var(--fg-3);text-transform:uppercase;margin-bottom:2px}.inspector-kv{display:grid;grid-template-columns:90px 1fr;align-items:baseline;gap:var(--space-3);font-size:.825rem}.inspector-key{color:var(--fg-2);font-size:.78rem}.inspector-val{color:var(--fg-0);font-family:var(--font-mono);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-mono-sm{font-size:.7rem;color:var(--fg-1)}.inspector-parents{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.inspector-parent-row{list-style:none}.inspector-parent-btn{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.8rem;color:var(--fg-1);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-align:left}.inspector-parent-btn:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0)}.inspector-parent-btn code{font-family:var(--font-mono);color:var(--accent);font-size:.78rem}.inspector-parent-branch{flex:1;color:var(--fg-2);font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-parent-arrow{color:var(--fg-3);font-size:.75rem}.inspector-link{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--fg-1);font-size:.8rem;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-decoration:none}.inspector-link:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0);text-decoration:none}.inspector-link code{font-family:var(--font-mono);font-size:.72rem;color:var(--accent)}.inspector-link-icon{font-size:.8rem;color:var(--fg-3)}.inspector-link-ext{margin-left:auto;color:var(--fg-3);font-size:.7rem}.inspector-link-sm{padding:var(--space-1) var(--space-2);font-size:.72rem}.inspector-code-block{display:block;font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3);word-break:break-all;line-height:1.6}.inspector-proposal-summary{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attestations{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attest-row{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);display:flex;flex-direction:column;gap:var(--space-1)}.inspector-attest-top{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-attest-signer{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.inspector-attest-time{margin-left:auto;font-size:.7rem;color:var(--fg-3);font-family:var(--font-mono)}.inspector-snapshot{margin-top:auto}.inspector-snapshot-body{background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);font-size:.825rem;color:var(--fg-0);line-height:1.6}.inspector-snapshot-hint{font-size:.72rem;color:var(--fg-3);font-style:italic}.inspector-empty-hint{font-size:.8rem;color:var(--fg-3);font-style:italic}.inspector-copy-row{display:flex;align-items:center;gap:var(--space-2);width:100%;background:none;border:none;cursor:pointer;padding:var(--space-1) 0;color:var(--fg-1);font-size:.82rem;text-align:left}.inspector-copy-row:hover{color:var(--accent)}.inspector-copy-badge{margin-left:auto;font-size:.7rem;color:var(--fg-3);background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.inspector-copy-row:hover .inspector-copy-badge{color:var(--accent);border-color:var(--accent)}.right-drawer{position:relative;display:flex;flex-direction:column;width:0;overflow:hidden;border-left:1px solid transparent;background:var(--bg-1);transition:width var(--duration-med) var(--ease-out),border-color var(--duration-med) var(--ease-out);flex-shrink:0}.right-drawer.open{width:var(--drawer-width);border-left-color:var(--border)}.drawer-toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 var(--space-4);height:44px;border-bottom:1px solid var(--border);flex-shrink:0}.drawer-title{font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.drawer-body{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.drawer-proposals-footer{border-top:1px solid var(--border);padding:var(--space-3) var(--space-4);flex-shrink:0}.drawer-proposals-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3);margin-bottom:var(--space-2)}.drawer-proposals-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.drawer-proposal-chip{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.76rem;color:var(--fg-1);text-align:left;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.drawer-proposal-chip:hover{background:var(--bg-3);border-color:var(--warning)}.drawer-proposal-chip>span:nth-child(2){flex:1;font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-proposal-chip>span:last-child{color:var(--fg-3);font-size:.7rem;flex-shrink:0}.dag-canvas-wrapper,.dag-canvas-scroll{position:relative;flex:1;overflow:auto;background:var(--bg-0);background-image:linear-gradient(rgba(255,255,255,.012) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.012) 1px,transparent 1px);background-size:32px 32px}.dag-canvas-svg,.dag-canvas{display:block;cursor:grab}.dag-canvas-svg:active,.dag-canvas:active{cursor:grabbing}.dag-node{transition:opacity var(--duration-fast) var(--ease-out)}@keyframes dag-pop-in{0%{transform:scale(.3);opacity:0}65%{transform:scale(1.15);opacity:1}to{transform:scale(1);opacity:1}}.dag-node--new{animation:dag-pop-in .45s cubic-bezier(.34,1.56,.64,1) both;transform-box:fill-box;transform-origin:center}.dag-node:focus-visible circle,.dag-node:focus-visible rect{outline:2px solid var(--accent)}.dag-node-hash{opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.dag-node:hover .dag-node-hash,.dag-node.selected .dag-node-hash{opacity:1}.dag-edge{transition:opacity var(--duration-fast) var(--ease-out),stroke-width var(--duration-fast) var(--ease-out)}.dag-lane-label{transition:opacity var(--duration-fast) var(--ease-out)}.dag-empty{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-2);pointer-events:none;color:var(--fg-3);font-size:.875rem}.dag-empty p{text-align:center}.memory-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.memory-search-row{display:flex;align-items:center;gap:var(--space-4);padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.memory-search-wrap{position:relative;flex:1;max-width:480px}.memory-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--fg-3);pointer-events:none}.memory-search{width:100%;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:.45rem 2rem .45rem 2.1rem;font-family:var(--font-sans);font-size:.85rem;color:var(--fg-0);outline:none;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out)}.memory-search::placeholder{color:var(--fg-3)}.memory-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}.memory-search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:18px;height:18px;font-size:14px;line-height:18px;text-align:center;color:var(--fg-3);border-radius:50%;transition:color var(--duration-fast) var(--ease-out)}.memory-search-clear:hover{color:var(--fg-0)}.memory-count-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);white-space:nowrap}.memory-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--fg-3);font-size:.875rem}.memory-empty p{text-align:center;line-height:1.8}.memory-groups{flex:1;overflow-y:auto;padding:var(--space-4) var(--space-5) var(--space-8);display:flex;flex-direction:column;gap:var(--space-6)}.memory-group{display:flex;flex-direction:column;gap:var(--space-2)}.memory-group-header{display:flex;align-items:center;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.memory-group-name{font-family:var(--font-mono);font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.memory-group-count{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.memory-fact-list{list-style:none;display:flex;flex-direction:column;gap:2px}.memory-fact-row{padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-2);cursor:pointer;transition:border-color var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out);outline:none}.memory-fact-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.memory-fact-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.memory-fact-content{font-size:.85rem;color:var(--fg-0);line-height:1.6}.memory-fact-meta-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.memory-fact-branch{font-family:var(--font-mono);font-size:.68rem;font-weight:500;color:var(--accent);background:var(--accent-dim);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.memory-fact-blob{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);flex-shrink:0}.memory-fact-time{margin-left:auto;font-size:.7rem;font-family:var(--font-mono);color:var(--fg-3);white-space:nowrap;flex-shrink:0;-webkit-user-select:none;user-select:none}.history-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.history-header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.history-header-count{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.history-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;padding:var(--space-8);text-align:center}.history-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:420px;line-height:1.6}.history-empty-sub code{color:var(--fg-2)}.history-list{flex:1;overflow-y:auto;list-style:none;padding:var(--space-2) var(--space-3)}.history-row{display:grid;grid-template-columns:28px 1fr;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid transparent;border-left:3px solid transparent;border-radius:var(--radius-md);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);outline:none}.history-row--commit{cursor:pointer}.history-row--commit:hover{background:var(--bg-2)}.history-row--commit.selected{background:var(--bg-2);border-left-color:var(--border-strong)}.history-row--commit:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.history-row--fork{cursor:default;background:#ffffff04}.history-row--anchor{cursor:pointer;border-left-color:var(--accent-border);background:#1f9d720a}.history-row--anchor:hover{background:#1f9d7214;border-left-color:var(--accent)}.history-row--anchor.selected{background:var(--accent-dim);border-left-color:var(--accent)}.history-row--anchor:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}@keyframes row-pop-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}.is-new{animation:row-pop-in .35s var(--ease-out) both}.history-gutter{position:relative;display:flex;flex-direction:column;align-items:center}.history-line{width:2px;flex:1;background:var(--border-strong);min-height:6px}.history-node{width:10px;height:10px;border-radius:50%;border:2px solid currentColor;background:var(--bg-0);flex-shrink:0;z-index:1}.history-node--fork{width:9px;height:9px;border-radius:2px;border:1.5px dashed currentColor;background:var(--bg-0);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-node--anchor.merge{width:12px;height:12px;border-radius:3px;border:2px solid var(--accent);background:var(--accent-dim);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-row--anchor.selected .history-node--anchor{background:var(--accent)}.history-node.branch-green,.history-node--fork.branch-green{color:var(--lane-0)}.history-node.branch-blue,.history-node--fork.branch-blue{color:var(--lane-1)}.history-node.branch-orange,.history-node--fork.branch-orange{color:var(--lane-3)}.history-node.branch-red,.history-node--fork.branch-red{color:var(--danger)}.history-node.branch-purple,.history-node--fork.branch-purple{color:var(--lane-4)}.history-node.branch-muted,.history-node--fork.branch-muted{color:var(--fg-3)}.history-content{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;padding:3px 0}.history-top-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.history-fork-top{flex-wrap:wrap;row-gap:var(--space-1)}.history-flex-spacer{flex:1}.history-short-id{font-family:var(--font-mono);font-size:.72rem;color:var(--accent);background:var(--bg-1);border:1px solid var(--border);padding:.05rem .4rem;border-radius:var(--radius-sm);flex-shrink:0}.history-row--anchor.selected .history-short-id{background:var(--accent-dim)}.history-message{font-size:.83rem;font-weight:500;color:var(--fg-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.history-row--commit .history-message{color:var(--fg-1);font-weight:400}.history-meta-row{display:flex;align-items:center;gap:var(--space-2);font-size:.72rem;color:var(--fg-2);flex-wrap:wrap}.history-author{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.history-sep{color:var(--fg-3);-webkit-user-select:none;user-select:none}.history-time{color:var(--fg-2)}.history-parents-hint,.history-resolver-label{font-family:var(--font-mono);color:var(--fg-3);font-size:.68rem}.history-resolver-label{color:var(--fg-2)}.history-tx{font-family:var(--font-mono);font-size:.66rem;color:var(--fg-3)}.history-fork-glyph{font-size:.85rem;color:var(--fg-3);flex-shrink:0;line-height:1;margin-right:1px}.history-fork-label{font-size:.78rem;color:var(--fg-3);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-fork-label strong{color:var(--fg-1);font-weight:500}.history-onchain-glyph{color:var(--fg-3);font-size:.75rem;flex-shrink:0;line-height:1}.history-onchain-glyph--anchor{color:var(--accent);font-size:.85rem}.history-message strong{font-weight:600}.merges-view{flex:1;display:flex;flex-direction:column;overflow-y:auto;gap:var(--space-6);padding:var(--space-5);background:var(--bg-0)}.merges-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;text-align:center;padding:var(--space-8)}.merges-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:400px;line-height:1.6}.merges-empty-sub code{color:var(--fg-2)}.merges-section{display:flex;flex-direction:column;gap:var(--space-3)}.merges-section-header{display:flex;align-items:center;gap:var(--space-3);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.merges-section-title{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3)}.merges-ceremonies{display:flex;flex-direction:column;gap:var(--space-4)}.ceremony-card{display:flex;flex-direction:column;gap:var(--space-4);border:1px solid var(--border);border-left:3px solid var(--warning);border-radius:var(--radius-lg);background:var(--bg-1);padding:var(--space-4) var(--space-5)}.ceremony-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.ceremony-header-left{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.ceremony-icon{font-size:1rem;color:var(--warning)}.ceremony-resolver-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.ceremony-route-text{display:flex;align-items:center;gap:var(--space-2);font-size:.85rem;color:var(--fg-1)}.ceremony-route-text strong{color:var(--fg-0);font-weight:600}.ceremony-arrow{color:var(--fg-3);font-size:.8rem}.ceremony-expiry{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-2)}.ceremony-expiry--warn{color:var(--warning)}.ceremony-attests{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-attests-empty{font-size:.8rem;color:var(--fg-3);font-style:italic;padding:var(--space-2) 0}.ceremony-attest-row{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:.8rem;flex-wrap:wrap}.ceremony-attest-row--cast{background:var(--bg-2);border:1px solid var(--border)}.ceremony-attest-row--pending{background:transparent;border:1px dashed var(--border);opacity:.65}@keyframes pulse-dot{0%,to{opacity:.3;transform:scale(.85)}50%{opacity:1;transform:scale(1.1)}}.ceremony-attest-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--warning);animation:pulse-dot 1.4s ease-in-out infinite;flex-shrink:0}.ceremony-attest-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.ceremony-attest-label{font-family:var(--font-mono);font-size:.76rem;color:var(--fg-1);min-width:60px}.ceremony-attest-model{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);background:var(--bg-3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 .3rem}.ceremony-attest-vote-text{font-size:.74rem;color:var(--fg-2);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ceremony-attest-voting{font-size:.75rem;color:var(--fg-3);font-style:italic}.ceremony-attest-time-spacer{flex:1}.ceremony-attest-reltime{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);white-space:nowrap}.ceremony-threshold{display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-threshold-bar{height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden}.ceremony-threshold-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .5s var(--ease-out)}.ceremony-threshold-label{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2)}.ceremony-threshold-note{color:var(--fg-3)}.merges-settled-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.settled-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);flex-wrap:wrap}.settled-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.settled-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.settled-row-left{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;min-width:0}.settled-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.settled-route{display:flex;align-items:center;gap:var(--space-2);font-size:.82rem;color:var(--fg-1);min-width:0}.settled-route strong{color:var(--fg-0);font-weight:500}.settled-arrow{color:var(--fg-3);font-size:.78rem}.settled-resolver{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.settled-row-right{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.settled-time{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.settled-view-hint{font-size:.72rem;color:var(--fg-3);opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.settled-row:hover .settled-view-hint{opacity:1}.merges-section--graveyard .merges-section-title{color:var(--danger);opacity:.75}.merges-graveyard-note{font-size:.78rem;color:var(--fg-3);line-height:1.6;font-style:italic;max-width:560px}.merges-graveyard-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-3)}.graveyard-row{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-left:3px solid var(--danger);border-radius:var(--radius-md);opacity:.8;transition:opacity var(--duration-fast) var(--ease-out)}.graveyard-row:hover{opacity:1}.graveyard-row-top{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.graveyard-cross{color:var(--danger);font-size:.82rem;flex-shrink:0}.graveyard-time{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);margin-left:auto}.graveyard-ask-btn{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.15rem .55rem;cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.graveyard-ask-btn:hover{background:var(--bg-3);border-color:var(--accent-border);color:var(--accent)}.graveyard-rationale{font-size:.78rem;color:var(--fg-2);line-height:1.55;padding-left:var(--space-2);border-left:2px solid var(--border-strong)}:root{--bg-0: #0c0e13;--bg-1: #12151c;--bg-2: #181c25;--bg-3: #20252f;--fg-0: #e4e7ed;--fg-1: #9aa1ae;--fg-2: #69707d;--fg-3: #474d58;--accent: #1f9d72;--accent-hover: #27b886;--accent-dim: rgba(31, 157, 114, .13);--accent-glow: rgba(31, 157, 114, .22);--accent-border: rgba(31, 157, 114, .3);--border: rgba(255, 255, 255, .06);--border-strong: rgba(255, 255, 255, .11);--danger: #d65b5b;--danger-dim: rgba(214, 91, 91, .13);--danger-border: rgba(214, 91, 91, .3);--warning: #d2972f;--warning-dim: rgba(210, 151, 47, .13);--warning-border: rgba(210, 151, 47, .3);--purple: #8a82c9;--purple-dim: rgba(138, 130, 201, .13);--purple-border: rgba(138, 130, 201, .3);--lane-0: #1f9d72;--lane-1: #5683c4;--lane-2: #c06b95;--lane-3: #c1813f;--lane-4: #8a82c9;--lane-5: #3f9d8a;--lane-6: #b39a3c;--lane-7: #a76eb0;--radius-sm: 4px;--radius-md: 7px;--radius-lg: 11px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);--shadow-md: 0 4px 12px rgba(0, 0, 0, .45);--shadow-lg: 0 14px 40px rgba(0, 0, 0, .55);--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--ease-out: cubic-bezier(.2, .8, .2, 1);--duration-fast: .11s;--duration-med: .22s;--duration-slow: .4s;--topbar-height: 52px;--drawer-width: 420px;--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;--font-sans: "Inter", "Segoe UI", system-ui, sans-serif}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-sans);font-size:14px;line-height:1.5;background:var(--bg-0);color:var(--fg-0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,kbd,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--fg-3)}.chip{display:inline-flex;align-items:center;gap:3px;padding:.1rem .45rem;font-family:var(--font-mono);font-size:.68rem;font-weight:500;border-radius:999px;border:1px solid transparent;white-space:nowrap;line-height:1.6}.chip.green{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.chip.blue{background:#5683c421;color:var(--lane-1);border-color:#5683c44d}.chip.orange{background:var(--warning-dim);color:var(--warning);border-color:var(--warning-border)}.chip.red{background:var(--danger-dim);color:var(--danger);border-color:var(--danger-border)}.chip.purple{background:var(--purple-dim);color:var(--purple);border-color:var(--purple-border)}.chip.muted{background:var(--bg-2);color:var(--fg-2);border-color:var(--border)}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--radius-sm);color:var(--fg-2);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out)}.icon-btn:hover{background:var(--bg-3);color:var(--fg-0)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.app-root{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.app-body{display:flex;flex:1;overflow:hidden;min-height:0}
|