dev-memory-cli 0.18.2

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.
@@ -0,0 +1,1052 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>dev-memory UI</title>
6
+ <style>
7
+ :root {
8
+ --fg: #1f2328;
9
+ --fg-muted: #59636e;
10
+ --fg-subtle: #8b949e;
11
+ --bg: #f6f8fa;
12
+ --bg-card: #ffffff;
13
+ --bg-hover: #f0f3f6;
14
+ --bg-active: #ddf4ff;
15
+ --border: #d1d9e0;
16
+ --border-subtle: rgba(209, 217, 224, 0.6);
17
+ --accent: #0969da;
18
+ --accent-fg: #ffffff;
19
+ --tag-bg: #eff2f5;
20
+ --tag-fg: #59636e;
21
+ --archived-bg: #fff8c5;
22
+ --archived-fg: #9a6700;
23
+ --success-bg: #dafbe1;
24
+ --success-fg: #1a7f37;
25
+ --shadow-sm: 0 1px 2px rgba(31,35,40,0.04);
26
+ --shadow-md: 0 4px 12px rgba(31,35,40,0.1);
27
+ --radius: 8px;
28
+ --radius-sm: 6px;
29
+ }
30
+ @media (prefers-color-scheme: dark) {
31
+ :root {
32
+ --fg: #e6edf3;
33
+ --fg-muted: #8b949e;
34
+ --fg-subtle: #6e7681;
35
+ --bg: #0d1117;
36
+ --bg-card: #161b22;
37
+ --bg-hover: #1c2128;
38
+ --bg-active: #1f2d44;
39
+ --border: #30363d;
40
+ --border-subtle: rgba(48, 54, 61, 0.5);
41
+ --accent: #58a6ff;
42
+ --accent-fg: #0d1117;
43
+ --tag-bg: #262c36;
44
+ --tag-fg: #8b949e;
45
+ --archived-bg: #4d3800;
46
+ --archived-fg: #eac54f;
47
+ --success-bg: #033d1a;
48
+ --success-fg: #3fb950;
49
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
50
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.5);
51
+ }
52
+ }
53
+
54
+ * { box-sizing: border-box; }
55
+ html, body { margin: 0; height: 100%; }
56
+ body {
57
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Helvetica Neue", system-ui, sans-serif;
58
+ color: var(--fg);
59
+ background: var(--bg);
60
+ font-size: 14px;
61
+ line-height: 1.5;
62
+ -webkit-font-smoothing: antialiased;
63
+ }
64
+ .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
65
+ button { font-family: inherit; }
66
+
67
+ .app { display: grid; grid-template-rows: auto 1fr; height: 100vh; }
68
+ .topbar {
69
+ display: flex; align-items: center; justify-content: space-between;
70
+ padding: 12px 20px;
71
+ background: var(--bg-card);
72
+ border-bottom: 1px solid var(--border);
73
+ gap: 16px;
74
+ }
75
+ .topbar .brand { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
76
+ .topbar .title { font-weight: 600; font-size: 15px; }
77
+ .topbar .meta { color: var(--fg-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
+
79
+ .main { display: grid; grid-template-columns: 320px 1fr; overflow: hidden; min-height: 0; }
80
+
81
+ .sidebar {
82
+ overflow-y: auto;
83
+ border-right: 1px solid var(--border);
84
+ padding: 14px 12px;
85
+ background: var(--bg);
86
+ }
87
+ .search {
88
+ width: 100%;
89
+ padding: 8px 12px;
90
+ border: 1px solid var(--border);
91
+ background: var(--bg-card);
92
+ border-radius: var(--radius-sm);
93
+ color: var(--fg);
94
+ margin-bottom: 12px;
95
+ font-size: 13px;
96
+ outline: none;
97
+ transition: border-color 0.1s;
98
+ }
99
+ .search:focus { border-color: var(--accent); }
100
+
101
+ .repo-card {
102
+ padding: 12px 14px;
103
+ background: var(--bg-card);
104
+ border: 1px solid var(--border-subtle);
105
+ border-radius: var(--radius);
106
+ cursor: pointer;
107
+ margin-bottom: 8px;
108
+ transition: background 0.1s, border-color 0.1s, box-shadow 0.1s;
109
+ }
110
+ .repo-card:hover { background: var(--bg-hover); box-shadow: var(--shadow-sm); }
111
+ .repo-card.active {
112
+ border-color: var(--accent);
113
+ background: var(--bg-active);
114
+ }
115
+ .repo-card .name {
116
+ font-weight: 600; font-size: 14px;
117
+ word-break: break-word;
118
+ display: flex; align-items: center; gap: 6px;
119
+ }
120
+ .repo-card .sub {
121
+ color: var(--fg-subtle);
122
+ font-size: 11px;
123
+ margin-top: 3px;
124
+ word-break: break-all;
125
+ line-height: 1.4;
126
+ }
127
+ .repo-card .badges { margin-top: 8px; display: flex; gap: 4px; flex-wrap: wrap; }
128
+
129
+ .content {
130
+ overflow-y: auto;
131
+ padding: 24px 28px;
132
+ min-width: 0;
133
+ }
134
+ .content > .placeholder {
135
+ display: flex; align-items: center; justify-content: center;
136
+ height: 80%; color: var(--fg-muted); font-size: 13px;
137
+ }
138
+
139
+ .repo-header h2 {
140
+ margin: 0; font-size: 22px; font-weight: 600;
141
+ word-break: break-word;
142
+ }
143
+ .repo-header .repo-key {
144
+ color: var(--fg-subtle); font-size: 12px;
145
+ margin-top: 4px; word-break: break-all;
146
+ }
147
+ .repo-header .path {
148
+ color: var(--fg-muted); font-size: 13px;
149
+ margin-top: 6px; word-break: break-all;
150
+ }
151
+
152
+ .stat-row {
153
+ display: grid;
154
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
155
+ gap: 10px; margin: 18px 0;
156
+ }
157
+ .stat {
158
+ background: var(--bg-card);
159
+ border: 1px solid var(--border-subtle);
160
+ border-radius: var(--radius);
161
+ padding: 10px 14px;
162
+ }
163
+ .stat .label {
164
+ color: var(--fg-muted); font-size: 10px;
165
+ text-transform: uppercase; letter-spacing: 0.6px;
166
+ font-weight: 500;
167
+ }
168
+ .stat .value {
169
+ font-size: 18px; font-weight: 600; margin-top: 4px;
170
+ word-break: break-word; line-height: 1.3;
171
+ }
172
+ .stat .value.sm { font-size: 13px; font-weight: 500; color: var(--fg); }
173
+
174
+ .section-title {
175
+ font-size: 11px; font-weight: 600;
176
+ color: var(--fg-muted);
177
+ text-transform: uppercase; letter-spacing: 0.6px;
178
+ margin: 28px 0 12px;
179
+ }
180
+
181
+ .branch-bar {
182
+ display: flex; gap: 6px; flex-wrap: wrap;
183
+ padding-bottom: 14px;
184
+ border-bottom: 1px solid var(--border);
185
+ margin-bottom: 18px;
186
+ }
187
+ .branch-tab {
188
+ padding: 6px 12px;
189
+ border: 1px solid var(--border);
190
+ background: var(--bg-card);
191
+ border-radius: var(--radius-sm);
192
+ cursor: pointer;
193
+ font-size: 13px;
194
+ color: var(--fg);
195
+ display: inline-flex; align-items: center; gap: 6px;
196
+ transition: all 0.1s;
197
+ }
198
+ .branch-tab:hover { background: var(--bg-hover); }
199
+ .branch-tab.active {
200
+ background: var(--accent); color: var(--accent-fg);
201
+ border-color: var(--accent);
202
+ }
203
+ .branch-tab.active .tag { background: rgba(255,255,255,0.25); color: var(--accent-fg); }
204
+
205
+ .file-grid {
206
+ display: grid;
207
+ gap: 12px;
208
+ grid-template-columns: 1fr;
209
+ align-items: stretch;
210
+ }
211
+ @media (min-width: 1100px) {
212
+ .file-grid { grid-template-columns: 1fr 1fr; }
213
+ }
214
+ @media (min-width: 1600px) {
215
+ .file-grid { grid-template-columns: 1fr 1fr 1fr; }
216
+ }
217
+ .file-card {
218
+ background: var(--bg-card);
219
+ border: 1px solid var(--border-subtle);
220
+ border-radius: var(--radius);
221
+ overflow: hidden;
222
+ box-shadow: var(--shadow-sm);
223
+ transition: box-shadow 0.1s, transform 0.1s;
224
+ display: flex;
225
+ flex-direction: column;
226
+ }
227
+ .file-card.clickable { cursor: pointer; }
228
+ .file-card.clickable:hover { box-shadow: var(--shadow-md); }
229
+ .file-card > header {
230
+ display: flex; align-items: center; justify-content: space-between;
231
+ padding: 10px 14px;
232
+ border-bottom: 1px solid var(--border-subtle);
233
+ background: var(--bg);
234
+ gap: 12px;
235
+ }
236
+ .file-card .fname { font-weight: 600; font-size: 13px; }
237
+ .file-card .fmeta { color: var(--fg-subtle); font-size: 11px; flex-shrink: 0; }
238
+ .file-card .preview {
239
+ padding: 12px 14px;
240
+ flex: 1;
241
+ min-height: 120px;
242
+ max-height: 280px;
243
+ overflow: hidden;
244
+ position: relative;
245
+ font-size: 12px;
246
+ line-height: 1.6;
247
+ word-break: break-word;
248
+ }
249
+ .file-card .preview.plain {
250
+ white-space: pre-wrap;
251
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
252
+ }
253
+
254
+ .md {
255
+ color: var(--fg);
256
+ }
257
+ .md > :first-child { margin-top: 0; }
258
+ .md > :last-child { margin-bottom: 0; }
259
+ .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
260
+ margin: 14px 0 6px;
261
+ font-weight: 600;
262
+ line-height: 1.3;
263
+ color: var(--fg);
264
+ }
265
+ .md h1 { font-size: 1.25em; border-bottom: 1px solid var(--border-subtle); padding-bottom: 4px; }
266
+ .md h2 { font-size: 1.15em; border-bottom: 1px solid var(--border-subtle); padding-bottom: 3px; }
267
+ .md h3 { font-size: 1.05em; }
268
+ .md h4 { font-size: 1em; color: var(--fg-muted); }
269
+ .md h5, .md h6 { font-size: 0.95em; color: var(--fg-muted); }
270
+ .md p { margin: 6px 0; }
271
+ .md ul, .md ol { margin: 6px 0; padding-left: 22px; }
272
+ .md li { margin: 2px 0; }
273
+ .md li > ul, .md li > ol { margin: 2px 0; }
274
+ .md code {
275
+ background: var(--tag-bg);
276
+ color: var(--fg);
277
+ padding: 1px 6px;
278
+ border-radius: 4px;
279
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
280
+ font-size: 0.88em;
281
+ }
282
+ .md pre.code {
283
+ background: var(--bg);
284
+ border: 1px solid var(--border-subtle);
285
+ border-radius: var(--radius-sm);
286
+ padding: 10px 12px;
287
+ overflow-x: auto;
288
+ margin: 10px 0;
289
+ font-size: 0.9em;
290
+ line-height: 1.55;
291
+ }
292
+ .md pre.code code {
293
+ background: none;
294
+ padding: 0;
295
+ font-size: inherit;
296
+ color: inherit;
297
+ white-space: pre;
298
+ }
299
+ .md blockquote {
300
+ border-left: 3px solid var(--border);
301
+ padding: 2px 12px;
302
+ color: var(--fg-muted);
303
+ margin: 8px 0;
304
+ }
305
+ .md blockquote p { margin: 4px 0; }
306
+ .md a { color: var(--accent); text-decoration: none; }
307
+ .md a:hover { text-decoration: underline; }
308
+ .md hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
309
+ .md strong { font-weight: 600; }
310
+ .md em { font-style: italic; }
311
+ .md table {
312
+ border-collapse: collapse;
313
+ margin: 10px 0;
314
+ font-size: 0.95em;
315
+ }
316
+ .md th, .md td {
317
+ border: 1px solid var(--border);
318
+ padding: 4px 10px;
319
+ text-align: left;
320
+ }
321
+ .md th { background: var(--bg); font-weight: 600; }
322
+ .file-card .preview.truncated::after {
323
+ content: "";
324
+ position: absolute; bottom: 0; left: 0; right: 0; height: 48px;
325
+ background: linear-gradient(transparent, var(--bg-card));
326
+ pointer-events: none;
327
+ }
328
+ .file-card .preview.empty { color: var(--fg-subtle); font-style: italic; }
329
+ .file-card .expand-btn {
330
+ padding: 8px 14px;
331
+ text-align: center;
332
+ color: var(--accent);
333
+ cursor: pointer;
334
+ font-size: 12px;
335
+ font-weight: 500;
336
+ border-top: 1px solid var(--border-subtle);
337
+ background: var(--bg);
338
+ transition: background 0.1s;
339
+ }
340
+ .file-card .expand-btn:hover { background: var(--bg-hover); }
341
+
342
+ .tag {
343
+ display: inline-block;
344
+ font-size: 10px;
345
+ padding: 2px 7px;
346
+ border-radius: 10px;
347
+ background: var(--tag-bg);
348
+ color: var(--tag-fg);
349
+ font-weight: 500;
350
+ letter-spacing: 0.3px;
351
+ white-space: nowrap;
352
+ }
353
+ .tag.archived { background: var(--archived-bg); color: var(--archived-fg); }
354
+ .tag.success { background: var(--success-bg); color: var(--success-fg); }
355
+
356
+ .modal-overlay {
357
+ position: fixed; inset: 0;
358
+ background: rgba(10, 15, 25, 0.55);
359
+ display: none; align-items: center; justify-content: center;
360
+ z-index: 100; padding: 40px 20px;
361
+ backdrop-filter: blur(3px);
362
+ }
363
+ .modal-overlay.open { display: flex; animation: fadeIn 0.12s ease; }
364
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
365
+ .modal {
366
+ background: var(--bg-card);
367
+ border-radius: var(--radius);
368
+ width: 100%; max-width: 960px; max-height: 100%;
369
+ display: flex; flex-direction: column;
370
+ box-shadow: var(--shadow-md);
371
+ overflow: hidden;
372
+ }
373
+ .modal > header {
374
+ padding: 14px 20px;
375
+ border-bottom: 1px solid var(--border);
376
+ display: flex; align-items: center; justify-content: space-between;
377
+ gap: 16px;
378
+ }
379
+ .modal .mtitle { font-size: 13px; word-break: break-all; min-width: 0; }
380
+ .modal .close {
381
+ cursor: pointer; color: var(--fg-muted);
382
+ font-size: 22px; line-height: 1; padding: 0 8px;
383
+ background: none; border: none; border-radius: var(--radius-sm);
384
+ }
385
+ .modal .close:hover { color: var(--fg); background: var(--bg-hover); }
386
+ .modal .body { overflow-y: auto; padding: 18px 24px; font-size: 14px; line-height: 1.7; }
387
+ .modal .body pre.raw {
388
+ margin: 0; font-size: 13px; line-height: 1.7;
389
+ white-space: pre-wrap; word-break: break-word;
390
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
391
+ }
392
+ .modal .mactions { display: flex; align-items: center; gap: 6px; }
393
+ .modal .editor {
394
+ width: 100%;
395
+ min-height: 60vh;
396
+ border: 1px solid var(--border);
397
+ border-radius: var(--radius-sm);
398
+ padding: 12px;
399
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
400
+ font-size: 13px;
401
+ line-height: 1.6;
402
+ color: var(--fg);
403
+ background: var(--bg);
404
+ resize: vertical;
405
+ outline: none;
406
+ box-sizing: border-box;
407
+ }
408
+ .modal .editor:focus { border-color: var(--accent); }
409
+ .modal .status {
410
+ font-size: 12px;
411
+ margin-top: 8px;
412
+ color: var(--fg-muted);
413
+ min-height: 18px;
414
+ word-break: break-word;
415
+ }
416
+ .modal .status.error { color: #cf222e; }
417
+ .modal .status.success { color: var(--success-fg); }
418
+ @media (prefers-color-scheme: dark) {
419
+ .modal .status.error { color: #ff7b72; }
420
+ }
421
+
422
+ .empty-state {
423
+ text-align: center;
424
+ color: var(--fg-muted); font-size: 13px;
425
+ padding: 32px 16px;
426
+ background: var(--bg-card);
427
+ border: 1px dashed var(--border);
428
+ border-radius: var(--radius);
429
+ }
430
+ .loading { color: var(--fg-subtle); font-size: 12px; font-style: italic; }
431
+
432
+ button.icon-btn {
433
+ background: var(--bg-card);
434
+ border: 1px solid var(--border);
435
+ padding: 6px 12px;
436
+ border-radius: var(--radius-sm);
437
+ color: var(--fg-muted);
438
+ cursor: pointer;
439
+ font-size: 12px;
440
+ transition: all 0.1s;
441
+ }
442
+ button.icon-btn:hover {
443
+ background: var(--bg-hover); color: var(--fg);
444
+ border-color: var(--fg-muted);
445
+ }
446
+ </style>
447
+ </head>
448
+ <body>
449
+ <div class="app">
450
+ <div class="topbar">
451
+ <div class="brand">
452
+ <span class="title">dev-memory UI</span>
453
+ <span class="meta" id="meta">加载中…</span>
454
+ </div>
455
+ <div class="actions">
456
+ <button class="icon-btn" id="reload" title="重新加载">↻ 刷新</button>
457
+ </div>
458
+ </div>
459
+ <div class="main">
460
+ <aside class="sidebar">
461
+ <input class="search" id="search" placeholder="搜索仓库…" autocomplete="off"/>
462
+ <div id="repoList"></div>
463
+ </aside>
464
+ <section class="content" id="content">
465
+ <div class="placeholder">← 从左侧选择一个仓库</div>
466
+ </section>
467
+ </div>
468
+ </div>
469
+
470
+ <div class="modal-overlay" id="modal" role="dialog" aria-hidden="true">
471
+ <div class="modal">
472
+ <header>
473
+ <span id="modalTitle" class="mtitle mono"></span>
474
+ <div class="mactions">
475
+ <button class="icon-btn" id="modalEdit" hidden>✎ 编辑</button>
476
+ <button class="icon-btn" id="modalSave" hidden>保存</button>
477
+ <button class="icon-btn" id="modalCancel" hidden>取消</button>
478
+ <button class="close" id="modalClose" aria-label="关闭">×</button>
479
+ </div>
480
+ </header>
481
+ <div class="body">
482
+ <div id="modalBody"></div>
483
+ <div class="status" id="modalStatus"></div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+
488
+ <script>
489
+ "use strict";
490
+
491
+ var state = {
492
+ tree: null,
493
+ selectedRepoKey: null,
494
+ selectedBranch: null,
495
+ fileCache: new Map(),
496
+ readOnly: false,
497
+ };
498
+
499
+ var modalState = {
500
+ relPath: null,
501
+ text: "",
502
+ editing: false,
503
+ };
504
+
505
+ function q(sel) { return document.querySelector(sel); }
506
+
507
+ function el(tag, attrs) {
508
+ var e = document.createElement(tag);
509
+ if (attrs) {
510
+ for (var k in attrs) {
511
+ var v = attrs[k];
512
+ if (v == null || v === false) continue;
513
+ if (k === "class") e.className = v;
514
+ else if (k === "onclick") e.onclick = v;
515
+ else if (k === "html") e.innerHTML = v;
516
+ else e.setAttribute(k, v);
517
+ }
518
+ }
519
+ for (var i = 2; i < arguments.length; i++) {
520
+ var c = arguments[i];
521
+ if (c == null || c === false) continue;
522
+ if (Array.isArray(c)) {
523
+ for (var j = 0; j < c.length; j++) {
524
+ if (c[j] == null || c[j] === false) continue;
525
+ e.appendChild(typeof c[j] === "string" ? document.createTextNode(c[j]) : c[j]);
526
+ }
527
+ } else {
528
+ e.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
529
+ }
530
+ }
531
+ return e;
532
+ }
533
+
534
+ function displayName(repo) {
535
+ if (repo.identity) {
536
+ var parts = repo.identity.split("/");
537
+ var last = parts[parts.length - 1];
538
+ if (last) return last;
539
+ }
540
+ return repo.key.replace(/-[a-f0-9]{12}$/, "");
541
+ }
542
+
543
+ function formatDateShort(iso) {
544
+ if (!iso) return "";
545
+ try { return new Date(iso).toLocaleDateString(); } catch (e) { return iso; }
546
+ }
547
+
548
+ function formatDateTime(iso) {
549
+ if (!iso) return "";
550
+ try { return new Date(iso).toLocaleString(); } catch (e) { return iso; }
551
+ }
552
+
553
+ function escapeHtml(s) {
554
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
555
+ }
556
+
557
+ function renderInline(s) {
558
+ var out = escapeHtml(s);
559
+ // inline code first so its content isn't further transformed
560
+ var codeSpans = [];
561
+ out = out.replace(/`([^`]+)`/g, function (_m, g) {
562
+ codeSpans.push(g);
563
+ return "" + (codeSpans.length - 1) + "";
564
+ });
565
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
566
+ out = out.replace(/__([^_]+)__/g, "<strong>$1</strong>");
567
+ out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
568
+ out = out.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
569
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
570
+ out = out.replace(/(\d+)/g, function (_m, i) {
571
+ return "<code>" + codeSpans[Number(i)] + "</code>";
572
+ });
573
+ return out;
574
+ }
575
+
576
+ function renderMd(text) {
577
+ if (!text) return "";
578
+ var lines = text.replace(/\r\n?/g, "\n").split("\n");
579
+ var out = [];
580
+ var i = 0;
581
+ while (i < lines.length) {
582
+ var line = lines[i];
583
+ var fence = /^```+\s*(\w*)\s*$/.exec(line);
584
+ if (fence) {
585
+ var buf = [];
586
+ i++;
587
+ while (i < lines.length && !/^```+\s*$/.test(lines[i])) {
588
+ buf.push(lines[i]);
589
+ i++;
590
+ }
591
+ i++;
592
+ out.push('<pre class="code"><code>' + escapeHtml(buf.join("\n")) + "</code></pre>");
593
+ continue;
594
+ }
595
+ var h = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
596
+ if (h) {
597
+ var lvl = h[1].length;
598
+ out.push("<h" + lvl + ">" + renderInline(h[2]) + "</h" + lvl + ">");
599
+ i++;
600
+ continue;
601
+ }
602
+ if (/^\s*(---|\*\*\*|___)\s*$/.test(line)) {
603
+ out.push("<hr/>");
604
+ i++;
605
+ continue;
606
+ }
607
+ if (/^>\s?/.test(line)) {
608
+ var bq = [];
609
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
610
+ bq.push(lines[i].replace(/^>\s?/, ""));
611
+ i++;
612
+ }
613
+ out.push("<blockquote><p>" + renderInline(bq.join(" ")) + "</p></blockquote>");
614
+ continue;
615
+ }
616
+ if (/^\s*[-*+]\s+/.test(line)) {
617
+ var items = [];
618
+ while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) {
619
+ items.push("<li>" + renderInline(lines[i].replace(/^\s*[-*+]\s+/, "")) + "</li>");
620
+ i++;
621
+ }
622
+ out.push("<ul>" + items.join("") + "</ul>");
623
+ continue;
624
+ }
625
+ if (/^\s*\d+\.\s+/.test(line)) {
626
+ var oitems = [];
627
+ while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
628
+ oitems.push("<li>" + renderInline(lines[i].replace(/^\s*\d+\.\s+/, "")) + "</li>");
629
+ i++;
630
+ }
631
+ out.push("<ol>" + oitems.join("") + "</ol>");
632
+ continue;
633
+ }
634
+ if (line.trim() === "") { i++; continue; }
635
+ var para = [];
636
+ while (
637
+ i < lines.length &&
638
+ lines[i].trim() !== "" &&
639
+ !/^(#{1,6}\s|```+|>\s?|\s*[-*+]\s|\s*\d+\.\s|\s*(---|\*\*\*|___)\s*$)/.test(lines[i])
640
+ ) {
641
+ para.push(lines[i]);
642
+ i++;
643
+ }
644
+ if (para.length) out.push("<p>" + renderInline(para.join(" ")) + "</p>");
645
+ }
646
+ return out.join("");
647
+ }
648
+
649
+ function fileRelPath(repoKey, branch, filename, scope) {
650
+ if (scope === "repo") return repoKey + "/repo/" + filename;
651
+ if (scope === "branch-archived") return repoKey + "/branches/_archived/" + branch + "/" + filename;
652
+ return repoKey + "/branches/" + branch + "/" + filename;
653
+ }
654
+
655
+ function fetchFile(relPath) {
656
+ if (state.fileCache.has(relPath)) {
657
+ return Promise.resolve(state.fileCache.get(relPath));
658
+ }
659
+ return fetch("/api/file?path=" + encodeURIComponent(relPath)).then(function (r) {
660
+ if (!r.ok) throw new Error(r.status + " " + r.statusText);
661
+ return r.text();
662
+ }).then(function (text) {
663
+ state.fileCache.set(relPath, text);
664
+ return text;
665
+ });
666
+ }
667
+
668
+ function stat(label, value, small) {
669
+ return el("div", { class: "stat" },
670
+ el("div", { class: "label" }, label),
671
+ el("div", { class: "value" + (small ? " sm" : "") }, String(value))
672
+ );
673
+ }
674
+
675
+ function renderRepoList(repos, filterText) {
676
+ var list = q("#repoList");
677
+ list.innerHTML = "";
678
+ var filtered = repos;
679
+ if (filterText) {
680
+ var ft = filterText.toLowerCase();
681
+ filtered = repos.filter(function (r) {
682
+ var haystack = (r.key + " " + (r.repoRoot || "") + " " + (r.identity || "") + " " + displayName(r)).toLowerCase();
683
+ return haystack.indexOf(ft) !== -1;
684
+ });
685
+ }
686
+ if (filtered.length === 0) {
687
+ list.appendChild(el("div", { class: "empty-state" }, "无匹配仓库"));
688
+ return;
689
+ }
690
+ for (var i = 0; i < filtered.length; i++) {
691
+ (function (r) {
692
+ var card = el("div", { class: "repo-card" + (r.key === state.selectedRepoKey ? " active" : "") });
693
+ card.appendChild(el("div", { class: "name" }, displayName(r)));
694
+ card.appendChild(el("div", { class: "sub mono" }, r.key));
695
+ var badges = el("div", { class: "badges" });
696
+ badges.appendChild(el("span", { class: "tag" }, r.branches.length + " 个活跃分支"));
697
+ if (r.archived.length) badges.appendChild(el("span", { class: "tag archived" }, r.archived.length + " 个归档"));
698
+ card.appendChild(badges);
699
+ card.onclick = function () { selectRepo(r.key); };
700
+ list.appendChild(card);
701
+ })(filtered[i]);
702
+ }
703
+ }
704
+
705
+ function selectRepo(key) {
706
+ state.selectedRepoKey = key;
707
+ var repo = state.tree.repos.find(function (r) { return r.key === key; });
708
+ var def = (repo && repo.branches[0]) || (repo && repo.archived[0]) || null;
709
+ state.selectedBranch = def ? { name: def.name, archived: def.archived } : null;
710
+ renderRepoList(state.tree.repos, q("#search").value);
711
+ renderContent();
712
+ q("#content").scrollTop = 0;
713
+ }
714
+
715
+ function selectBranch(name, archived) {
716
+ state.selectedBranch = { name: name, archived: archived };
717
+ renderContent();
718
+ }
719
+
720
+ function buildFileCard(file, relPath) {
721
+ var card = el("div", { class: "file-card clickable" });
722
+ var header = el("header", null,
723
+ el("span", { class: "fname mono" }, file.name),
724
+ el("span", { class: "fmeta mono" }, (file.size != null ? (file.size + " B") : ""))
725
+ );
726
+ var preview = el("div", { class: "preview" }, el("span", { class: "loading" }, "加载中…"));
727
+ var expandBtn = el("div", { class: "expand-btn" }, "展开全部 →");
728
+ expandBtn.onclick = function (e) { e.stopPropagation(); openModal(relPath); };
729
+ card.appendChild(header);
730
+ card.appendChild(preview);
731
+ card.appendChild(expandBtn);
732
+ card.onclick = function () { openModal(relPath); };
733
+
734
+ fetchFile(relPath).then(function (text) {
735
+ preview.innerHTML = "";
736
+ preview.classList.remove("plain", "empty");
737
+ if (!text || !text.trim()) {
738
+ preview.classList.add("plain", "empty");
739
+ preview.textContent = "(空文件)";
740
+ expandBtn.style.display = "none";
741
+ card.classList.remove("clickable");
742
+ card.onclick = null;
743
+ return;
744
+ }
745
+ var isMd = /\.md$/i.test(file.name);
746
+ if (isMd) {
747
+ var wrap = document.createElement("div");
748
+ wrap.className = "md";
749
+ wrap.innerHTML = renderMd(text);
750
+ preview.appendChild(wrap);
751
+ } else {
752
+ preview.classList.add("plain");
753
+ preview.textContent = text;
754
+ }
755
+ if (preview.scrollHeight > preview.clientHeight + 2) {
756
+ preview.classList.add("truncated");
757
+ } else {
758
+ expandBtn.style.display = "none";
759
+ }
760
+ }).catch(function (err) {
761
+ preview.innerHTML = "";
762
+ preview.classList.add("plain");
763
+ preview.textContent = "加载失败:" + err.message;
764
+ expandBtn.style.display = "none";
765
+ });
766
+
767
+ return card;
768
+ }
769
+
770
+ function renderContent() {
771
+ var content = q("#content");
772
+ var repo = state.tree.repos.find(function (r) { return r.key === state.selectedRepoKey; });
773
+ content.innerHTML = "";
774
+ if (!repo) {
775
+ content.appendChild(el("div", { class: "placeholder" }, "← 从左侧选择一个仓库"));
776
+ return;
777
+ }
778
+
779
+ var header = el("div", { class: "repo-header" },
780
+ el("h2", null, displayName(repo)),
781
+ el("div", { class: "repo-key mono" }, repo.key),
782
+ repo.repoRoot ? el("div", { class: "path mono" }, repo.repoRoot) : null,
783
+ !repo.repoRoot && repo.identity ? el("div", { class: "path mono" }, repo.identity) : null
784
+ );
785
+ content.appendChild(header);
786
+
787
+ var stats = el("div", { class: "stat-row" });
788
+ stats.appendChild(stat("活跃分支", repo.branches.length));
789
+ stats.appendChild(stat("归档分支", repo.archived.length));
790
+ var repoMdFiles = repo.repoLevel.files.filter(function (f) { return f.name.endsWith(".md"); });
791
+ stats.appendChild(stat("仓库级文件", repoMdFiles.length));
792
+ if (repo.updatedAt) stats.appendChild(stat("最近更新", formatDateShort(repo.updatedAt), true));
793
+ if (repo.lastSeenBranch) stats.appendChild(stat("上次所在分支", repo.lastSeenBranch, true));
794
+ content.appendChild(stats);
795
+
796
+ if (repoMdFiles.length) {
797
+ content.appendChild(el("h3", { class: "section-title" }, "仓库级记忆"));
798
+ var repoGrid = el("div", { class: "file-grid" });
799
+ for (var i = 0; i < repoMdFiles.length; i++) {
800
+ var f = repoMdFiles[i];
801
+ repoGrid.appendChild(buildFileCard(f, fileRelPath(repo.key, null, f.name, "repo")));
802
+ }
803
+ content.appendChild(repoGrid);
804
+ }
805
+
806
+ var allBranches = repo.branches.concat(repo.archived);
807
+ content.appendChild(el("h3", { class: "section-title" }, "分支(" + allBranches.length + ")"));
808
+
809
+ if (allBranches.length === 0) {
810
+ content.appendChild(el("div", { class: "empty-state" }, "暂无分支"));
811
+ return;
812
+ }
813
+
814
+ var bar = el("div", { class: "branch-bar" });
815
+ for (var b = 0; b < allBranches.length; b++) {
816
+ (function (branch) {
817
+ var isActive = state.selectedBranch && state.selectedBranch.name === branch.name && state.selectedBranch.archived === branch.archived;
818
+ var tab = el("div", { class: "branch-tab" + (isActive ? " active" : "") },
819
+ branch.name,
820
+ branch.archived ? el("span", { class: "tag archived" }, "已归档") : null
821
+ );
822
+ tab.onclick = function () { selectBranch(branch.name, branch.archived); };
823
+ bar.appendChild(tab);
824
+ })(allBranches[b]);
825
+ }
826
+ content.appendChild(bar);
827
+
828
+ var cur = allBranches.find(function (x) {
829
+ return state.selectedBranch && x.name === state.selectedBranch.name && x.archived === state.selectedBranch.archived;
830
+ });
831
+ if (!cur) return;
832
+
833
+ if (cur.manifest) {
834
+ var bs = el("div", { class: "stat-row" });
835
+ if (cur.manifest.updated_at) bs.appendChild(stat("最近更新", formatDateTime(cur.manifest.updated_at), true));
836
+ if (cur.manifest.setup_completed === true) bs.appendChild(stat("Setup 状态", "已整理", true));
837
+ else if (cur.manifest.setup_completed === false) bs.appendChild(stat("Setup 状态", "lazy(未整理)", true));
838
+ if (cur.manifest.last_update_title) bs.appendChild(stat("最近条目", cur.manifest.last_update_title, true));
839
+ if (cur.manifest.last_seen_head) bs.appendChild(stat("HEAD", cur.manifest.last_seen_head.slice(0, 7), true));
840
+ content.appendChild(bs);
841
+ }
842
+
843
+ var branchMdFiles = cur.files.filter(function (f) { return f.name.endsWith(".md"); });
844
+ if (branchMdFiles.length === 0) {
845
+ content.appendChild(el("div", { class: "empty-state" }, "该分支暂无记忆文件"));
846
+ return;
847
+ }
848
+ var grid = el("div", { class: "file-grid" });
849
+ var scope = cur.archived ? "branch-archived" : "branch-active";
850
+ for (var k = 0; k < branchMdFiles.length; k++) {
851
+ var bf = branchMdFiles[k];
852
+ grid.appendChild(buildFileCard(bf, fileRelPath(repo.key, cur.name, bf.name, scope)));
853
+ }
854
+ content.appendChild(grid);
855
+ }
856
+
857
+ function setModalStatus(text, level) {
858
+ var s = q("#modalStatus");
859
+ s.textContent = text || "";
860
+ s.className = "status" + (level ? " " + level : "");
861
+ }
862
+
863
+ function renderModalView() {
864
+ var body = q("#modalBody");
865
+ body.innerHTML = "";
866
+ var text = modalState.text || "";
867
+ var relPath = modalState.relPath || "";
868
+ if (!text || !text.trim()) {
869
+ body.appendChild(el("span", { class: "loading" }, "(空文件)"));
870
+ return;
871
+ }
872
+ var isMd = /\.md$/i.test(relPath);
873
+ if (isMd) {
874
+ var wrap = document.createElement("div");
875
+ wrap.className = "md";
876
+ wrap.innerHTML = renderMd(text);
877
+ body.appendChild(wrap);
878
+ } else {
879
+ var pre = document.createElement("pre");
880
+ pre.className = "raw";
881
+ pre.textContent = text;
882
+ body.appendChild(pre);
883
+ }
884
+ }
885
+
886
+ function renderModalEditor() {
887
+ var body = q("#modalBody");
888
+ body.innerHTML = "";
889
+ var ta = document.createElement("textarea");
890
+ ta.className = "editor mono";
891
+ ta.id = "modalEditor";
892
+ ta.value = modalState.text || "";
893
+ ta.spellcheck = false;
894
+ body.appendChild(ta);
895
+ // focus on next tick so the modal is fully visible
896
+ setTimeout(function () { ta.focus(); }, 0);
897
+ }
898
+
899
+ function updateModalButtons() {
900
+ var canEdit = !state.readOnly && !!modalState.relPath;
901
+ q("#modalEdit").hidden = !canEdit || modalState.editing;
902
+ q("#modalSave").hidden = !canEdit || !modalState.editing;
903
+ q("#modalCancel").hidden = !canEdit || !modalState.editing;
904
+ }
905
+
906
+ function enterEditMode() {
907
+ if (state.readOnly) return;
908
+ modalState.editing = true;
909
+ setModalStatus("");
910
+ renderModalEditor();
911
+ updateModalButtons();
912
+ }
913
+
914
+ function exitEditMode() {
915
+ modalState.editing = false;
916
+ setModalStatus("");
917
+ renderModalView();
918
+ updateModalButtons();
919
+ }
920
+
921
+ function saveModalEdit() {
922
+ if (!modalState.editing || !modalState.relPath) return;
923
+ var ta = q("#modalEditor");
924
+ if (!ta) return;
925
+ var newText = ta.value;
926
+ var saveBtn = q("#modalSave");
927
+ var cancelBtn = q("#modalCancel");
928
+ saveBtn.disabled = true;
929
+ cancelBtn.disabled = true;
930
+ setModalStatus("保存中…");
931
+ fetch("/api/file?path=" + encodeURIComponent(modalState.relPath), {
932
+ method: "PUT",
933
+ headers: { "content-type": "text/plain; charset=utf-8" },
934
+ body: newText,
935
+ }).then(function (r) {
936
+ if (!r.ok) {
937
+ return r.text().then(function (msg) {
938
+ throw new Error(r.status + " " + (msg || r.statusText));
939
+ });
940
+ }
941
+ return r.json();
942
+ }).then(function () {
943
+ modalState.text = newText;
944
+ state.fileCache.set(modalState.relPath, newText);
945
+ modalState.editing = false;
946
+ renderModalView();
947
+ updateModalButtons();
948
+ setModalStatus("已保存", "success");
949
+ // refresh tree mtime/size in background; don't reset selection
950
+ loadTree();
951
+ }).catch(function (err) {
952
+ setModalStatus("保存失败:" + err.message, "error");
953
+ }).then(function () {
954
+ saveBtn.disabled = false;
955
+ cancelBtn.disabled = false;
956
+ });
957
+ }
958
+
959
+ function openModal(relPath) {
960
+ modalState.relPath = relPath;
961
+ modalState.text = "";
962
+ modalState.editing = false;
963
+ q("#modalTitle").textContent = relPath;
964
+ setModalStatus("");
965
+ updateModalButtons();
966
+ var body = q("#modalBody");
967
+ body.innerHTML = "";
968
+ body.appendChild(el("span", { class: "loading" }, "加载中…"));
969
+ q("#modal").classList.add("open");
970
+ fetchFile(relPath).then(function (text) {
971
+ modalState.text = text || "";
972
+ renderModalView();
973
+ updateModalButtons();
974
+ }).catch(function (err) {
975
+ body.innerHTML = "";
976
+ var pre = document.createElement("pre");
977
+ pre.className = "raw";
978
+ pre.textContent = "加载失败:" + err.message;
979
+ body.appendChild(pre);
980
+ // disable edit on load failure
981
+ q("#modalEdit").hidden = true;
982
+ });
983
+ }
984
+
985
+ function closeModal() {
986
+ q("#modal").classList.remove("open");
987
+ modalState.relPath = null;
988
+ modalState.text = "";
989
+ modalState.editing = false;
990
+ }
991
+
992
+ function loadTree() {
993
+ q("#meta").textContent = "加载中…";
994
+ q("#repoList").innerHTML = '<div class="loading">加载中…</div>';
995
+ return fetch("/api/tree").then(function (r) { return r.json(); }).then(function (data) {
996
+ state.tree = data;
997
+ state.readOnly = !!data.readOnly;
998
+ if (!data.exists) {
999
+ q("#meta").textContent = "未找到存储目录:" + data.storageRoot;
1000
+ q("#repoList").innerHTML = "";
1001
+ q("#repoList").appendChild(el("div", { class: "empty-state" }, "暂无仓库"));
1002
+ return;
1003
+ }
1004
+ var totalBranches = 0;
1005
+ var totalArchived = 0;
1006
+ for (var i = 0; i < data.repos.length; i++) {
1007
+ totalBranches += data.repos[i].branches.length;
1008
+ totalArchived += data.repos[i].archived.length;
1009
+ }
1010
+ var metaParts = [data.repos.length + " 个仓库", totalBranches + " 个活跃分支"];
1011
+ if (totalArchived) metaParts.push(totalArchived + " 个归档");
1012
+ metaParts.push(data.storageRoot);
1013
+ q("#meta").textContent = metaParts.join(" · ");
1014
+ renderRepoList(data.repos, q("#search").value);
1015
+ if (data.repos.length) {
1016
+ var stillExists = state.selectedRepoKey && data.repos.some(function (r) { return r.key === state.selectedRepoKey; });
1017
+ if (!stillExists) selectRepo(data.repos[0].key);
1018
+ else renderContent();
1019
+ }
1020
+ }).catch(function (err) {
1021
+ q("#meta").textContent = "错误:" + err.message;
1022
+ });
1023
+ }
1024
+
1025
+ q("#search").addEventListener("input", function (e) {
1026
+ if (state.tree && state.tree.repos) renderRepoList(state.tree.repos, e.target.value);
1027
+ });
1028
+ q("#reload").addEventListener("click", function () {
1029
+ state.fileCache.clear();
1030
+ loadTree();
1031
+ });
1032
+ q("#modalClose").addEventListener("click", closeModal);
1033
+ q("#modalEdit").addEventListener("click", enterEditMode);
1034
+ q("#modalSave").addEventListener("click", saveModalEdit);
1035
+ q("#modalCancel").addEventListener("click", exitEditMode);
1036
+ q("#modal").addEventListener("click", function (e) { if (e.target.id === "modal") closeModal(); });
1037
+ document.addEventListener("keydown", function (e) {
1038
+ if (e.key === "Escape") {
1039
+ if (modalState.editing) exitEditMode();
1040
+ else closeModal();
1041
+ return;
1042
+ }
1043
+ if ((e.metaKey || e.ctrlKey) && e.key === "s" && q("#modal").classList.contains("open") && modalState.editing) {
1044
+ e.preventDefault();
1045
+ saveModalEdit();
1046
+ }
1047
+ });
1048
+
1049
+ loadTree();
1050
+ </script>
1051
+ </body>
1052
+ </html>