agent-memory-git 0.1.8__tar.gz → 0.1.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/PKG-INFO +1 -1
  2. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/pyproject.toml +1 -1
  3. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/pyproject.toml.orig +1 -1
  4. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/ui_server.py +8 -2
  5. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/app.css +150 -125
  6. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/avatar.js +5 -0
  7. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/main.js +1 -7
  8. agent_memory_git-0.1.10/src/agent_memory/webui/js/views/profiles.js +56 -0
  9. agent_memory_git-0.1.10/src/agent_memory/webui/js/views/settings.js +128 -0
  10. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/shell.js +1 -2
  11. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/ui.js +0 -3
  12. agent_memory_git-0.1.8/src/agent_memory/webui/js/views/profiles.js +0 -78
  13. agent_memory_git-0.1.8/src/agent_memory/webui/js/views/settings.js +0 -138
  14. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/LICENSE +0 -0
  15. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/README.md +0 -0
  16. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/PROTOCOL.md +0 -0
  17. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/__init__.py +0 -0
  18. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/__main__.py +0 -0
  19. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/capture.py +0 -0
  20. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/cli.py +0 -0
  21. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/config.py +0 -0
  22. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/context.py +0 -0
  23. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +0 -0
  24. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +0 -0
  25. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +0 -0
  26. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +0 -0
  27. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/personal/2026/10/20261009-thesis-setup-fr6e.md +0 -0
  28. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260804-alex-profile-ujzd.md +0 -0
  29. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260804-reply-language-xdnc.md +0 -0
  30. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260812-ask-before-push-pf3d.md +0 -0
  31. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260820-tests-before-done-odzd.md +0 -0
  32. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260827-etl-orders-export-deadline-pft7.md +0 -0
  33. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/08/20260828-etl-dashboard-72ce.md +0 -0
  34. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/09/20260902-report-eta-is2j.md +0 -0
  35. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/09/20260908-shop-web-stripe-checkout-lgmx.md +0 -0
  36. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/09/20260910-shop-web-deploys-dn73.md +0 -0
  37. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/09/20260915-etl-parquet-partitioning-seh2.md +0 -0
  38. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/09/20260922-design-tokens-source-uvw7.md +0 -0
  39. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/10/20261002-shop-web-v3-freeze-5xtp.md +0 -0
  40. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo/work/2026/10/20261012-payments-runbook-4syw.md +0 -0
  41. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/demo.py +0 -0
  42. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/embed_worker.py +0 -0
  43. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/frontmatter.py +0 -0
  44. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/gitsync.py +0 -0
  45. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/hooks.py +0 -0
  46. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/importers.py +0 -0
  47. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/install.py +0 -0
  48. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/lint.py +0 -0
  49. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/profiles.py +0 -0
  50. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/select.py +0 -0
  51. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/sessions.py +0 -0
  52. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/store.py +0 -0
  53. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/ui.py +0 -0
  54. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/update.py +0 -0
  55. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/util.py +0 -0
  56. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/index.html +0 -0
  57. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/api.js +0 -0
  58. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/filters.js +0 -0
  59. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/format.js +0 -0
  60. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/h.js +0 -0
  61. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/icons.js +0 -0
  62. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/markdown.js +0 -0
  63. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/search.js +0 -0
  64. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/store.js +0 -0
  65. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/insights.js +0 -0
  66. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/list.js +0 -0
  67. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/memory.js +0 -0
  68. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/webui/js/views/overlays.js +0 -0
  69. {agent_memory_git-0.1.8 → agent_memory_git-0.1.10}/src/agent_memory/workdir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-memory-git
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Shared, git-backed memory for Claude Code and Codex.
5
5
  Keywords: claude-code,codex,memory,agents,git
6
6
  Author: Shichao Song
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -614,12 +614,18 @@ class Handler(BaseHTTPRequestHandler):
614
614
  self._run(lambda: getattr(self.server.app, method)(payload))
615
615
 
616
616
 
617
+ class Server(ThreadingHTTPServer):
618
+ # The page is some twenty modules the browser asks for at once; with socketserver's backlog of 5 the
619
+ # kernel resets the overflow, a module fails to load, and the page stays blank.
620
+ request_queue_size = 128
621
+
622
+
617
623
  def make_server(data_dir, port=8790, semantic=True):
618
624
  app = UiApp(data_dir, semantic=semantic)
619
625
  try:
620
- server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
626
+ server = Server(("127.0.0.1", port), Handler)
621
627
  except OSError:
622
- server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
628
+ server = Server(("127.0.0.1", 0), Handler)
623
629
  port = server.server_address[1]
624
630
  server.hosts = {f"127.0.0.1:{port}", f"localhost:{port}"}
625
631
  # a restart after an update keeps the token, so the open page carries on
@@ -112,8 +112,6 @@ body.resizing { cursor: col-resize; user-select: none; }
112
112
  .dot.t-project { background: var(--t-project); } .dot.t-user { background: var(--t-user); }
113
113
  .dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); } .dot.off { background: var(--ink-3); }
114
114
  .type-label { display: inline-flex; align-items: center; gap: 6px; }
115
- .status { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-2); }
116
- .status.warn-text { color: var(--warn); }
117
115
 
118
116
  .seg { display: inline-flex; padding: 2px; border-radius: 9px; background: var(--chip); }
119
117
  .seg > button { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border-radius: 7px; font-size: 13px; color: var(--ink-2); transition: color .15s; }
@@ -184,7 +182,8 @@ body.resizing { cursor: col-resize; user-select: none; }
184
182
  .rail-btn:hover { background: var(--hover); color: var(--ink); }
185
183
  .rail-btn[aria-current="page"] { background: var(--surface); color: var(--accent); box-shadow: 0 0 0 1px var(--line); }
186
184
  .rail-btn .i { width: 19px; height: 19px; }
187
- .rail-btn .badge { position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--panel); }
185
+ .rail-btn .badge, .rail-profile .badge { position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--panel); }
186
+ .rail-profile .badge { top: 5px; right: 5px; }
188
187
 
189
188
  /* the list is 372, 400 or 440px wide by window size, until its edge is dragged (--list-w);
190
189
  whatever the width, the open memory keeps at least 420px */
@@ -444,133 +443,159 @@ table.nums td { padding: 4px 8px; border-top: 1px solid var(--line-soft); text-a
444
443
  table.nums th:first-child, table.nums td:first-child { padding-left: 0; text-align: left; color: var(--ink); }
445
444
  table.nums td:last-child { color: var(--ink); }
446
445
 
447
- .settings { display: grid; grid-template-columns: minmax(0, 1fr); max-width: 768px; gap: 24px; }
448
- @media (max-width: 639px) { .settings { gap: 16px; } }
449
- .group { padding: 16px 20px 4px; border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface); }
450
- @media (max-width: 639px) { .group { padding-inline: 16px; } }
451
- .group h2 { margin-bottom: 4px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
452
- .group-head { display: flex; align-items: center; justify-content: space-between; }
453
- .group-foot { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-top: 1px solid var(--line-soft); }
454
- .group-foot code { font-family: var(--mono); }
455
- .setting { display: grid; grid-template-columns: minmax(0, 1fr); align-items: center; gap: 12px; padding: 14px 0; border-top: 1px solid var(--line-soft); }
456
- .group h2 + .setting, .group .setting:first-of-type { border-top: 0; }
457
- @media (min-width: 640px) {
458
- .setting:not(.below) { grid-template-columns: minmax(220px, 1fr) minmax(0, auto); gap: 24px; }
459
- .setting:not(.below) .setting-control { max-width: 440px; justify-self: end; }
446
+ /* settings: the open profile as a hero, with its picture blurred into a glow of its own colours behind it; a few
447
+ quiet rows of preferences; the installation as one line. Controls here are pills. */
448
+ .settings { --glow: .62; --glow-w: 600px; --glow-h: 400px; --glow-top: -150px; --glow-sat: 1.15;
449
+ --pic-shadow: drop-shadow(0 10px 22px rgb(60 50 120 / .16)) drop-shadow(0 1px 2px rgb(0 0 0 / .08));
450
+ width: 100%; max-width: 680px; margin: 16px auto 0; }
451
+ :root[data-theme="dark"] .settings { --glow: .3; --glow-w: 380px; --glow-h: 280px; --glow-top: -40px; --glow-sat: 2.8; --pic-shadow: drop-shadow(0 10px 24px rgb(0 0 0 / .45)); }
452
+ @media (prefers-color-scheme: dark) {
453
+ :root:not([data-theme]) .settings { --glow: .3; --glow-w: 380px; --glow-h: 280px; --glow-top: -40px; --glow-sat: 2.8; --pic-shadow: drop-shadow(0 10px 24px rgb(0 0 0 / .45)); }
460
454
  }
461
- .setting-name { font-size: 14px; font-weight: 500; }
462
- .setting-help { margin-top: 2px; font-size: 12.5px; color: var(--ink-3); }
463
- .setting-control { min-width: 0; }
464
- .block-code { display: block; padding: 8px 12px; border-radius: var(--r-ctl); background: var(--chip); font-size: 12.5px; line-height: 1.625; overflow-wrap: anywhere; color: var(--ink-2); }
465
- .chip-code { padding: 4px 8px; border-radius: 6px; background: var(--chip); font-size: 12.5px; color: var(--ink-2); }
466
- .code-copy { display: inline-flex; max-width: 100%; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 6px; background: var(--chip); font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); }
467
- .code-copy:hover { color: var(--ink); }
468
- .code-copy span { overflow-wrap: anywhere; }
469
- .code-copy .i { width: 14px; height: 14px; }
470
- .model-state { display: grid; justify-items: start; gap: 6px; }
471
- @media (min-width: 640px) { .model-state { justify-items: end; } }
472
- .tags-input { display: flex; width: 100%; max-width: 360px; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px; border: 1px solid var(--line); border-radius: var(--r-ctl); background: var(--surface); }
473
- .tags-input:focus-within { border-color: var(--accent); }
474
- .tag { display: inline-flex; height: 24px; align-items: center; gap: 2px; padding: 0 2px 0 8px; border-radius: 6px; background: var(--chip); font-size: 12.5px; }
475
- .tag button { display: grid; width: 20px; height: 20px; place-items: center; border-radius: 4px; color: var(--ink-3); }
476
- .tag button:hover { color: var(--ink); }
455
+ .settings .btn { height: 32px; padding: 0 14px; border-radius: 999px; font-weight: 500; }
456
+ .settings .btn .i { width: 15px; height: 15px; }
457
+ .settings .btn.ghost { padding: 0 10px; }
458
+ .settings .input { height: 36px; font-size: 13px; }
459
+ .settings .seg { padding: 3px; border-radius: 999px; }
460
+ .settings .seg > button { padding: 0 13px; border-radius: 999px; }
461
+ .settings .switch { width: 38px; height: 22px; }
462
+ .settings .switch > span { width: 18px; height: 18px; }
463
+
464
+ .hero { position: relative; isolation: isolate; overflow: hidden; border: 1px solid var(--line-soft); border-radius: calc(var(--r-card) * 1.5);
465
+ background: var(--surface); box-shadow: 0 1px 2px rgb(0 0 0 / .03), 0 8px 32px -12px rgb(40 30 90 / .08); }
466
+ .hero-glow { position: absolute; z-index: -1; top: var(--glow-top); left: 50%; width: var(--glow-w); height: var(--glow-h); translate: -50% 0; border-radius: 50%;
467
+ overflow: hidden; filter: blur(60px) saturate(var(--glow-sat)); opacity: var(--glow); pointer-events: none; }
468
+ .hero-glow svg { display: block; width: 100%; height: 100%; }
469
+ .hero-main { display: flex; flex-direction: column; align-items: center; padding: 48px 32px 36px; text-align: center; }
470
+ .pavatar.hero-pic { width: 88px; height: 88px; filter: var(--pic-shadow); }
471
+ .hero-name { margin-top: 20px; font-size: 34px; font-weight: 650; line-height: 1.15; letter-spacing: -0.024em; }
472
+ .hero-sub { margin-top: 6px; font-size: 13.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
473
+ .hero-sub .sep { margin: 0 6px; color: var(--ink-3); }
474
+ .hero-meta { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 4px 20px; margin-top: 20px; }
475
+ .hero-meta-item { position: relative; display: inline-flex; min-width: 0; max-width: 100%; align-items: center; gap: 7px; font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); }
476
+ .hero-meta-item > .i { width: 14px; height: 14px; color: var(--ink-3); }
477
+ .hero-meta-item .v { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
478
+ .hero-meta-item .k { font-family: var(--sans); color: var(--ink-3); }
479
+ button.hero-meta-item { border-radius: 6px; transition: color .15s; }
480
+ button.hero-meta-item:hover { color: var(--ink); }
481
+ .hero-meta-item > .copy-hint { position: absolute; right: -20px; width: 13px; height: 13px; opacity: 0; transition: opacity .15s; }
482
+ button.hero-meta-item:hover .copy-hint, button.hero-meta-item:focus-visible .copy-hint { opacity: 1; }
483
+ .hero-link { flex-shrink: 0; margin-left: -2px; padding: 2px 6px; border-radius: 6px; font-family: var(--sans); font-size: 12.5px; font-weight: 500; color: var(--accent); }
484
+ .hero-link:hover { background: var(--accent-soft); }
485
+ .hero-note { margin-top: 12px; font-size: 12.5px; color: var(--ok); }
486
+ .hero-note.bad { color: var(--bad); }
487
+ .remote-edit { width: 100%; max-width: 520px; margin-top: 16px; padding: 12px; border-radius: var(--r-card); background: color-mix(in srgb, var(--surface) 70%, transparent);
488
+ box-shadow: var(--ring); text-align: left; animation: rise .15s ease-out; }
489
+ .remote-edit label { display: block; margin: 0 0 6px 2px; font-size: 12px; font-weight: 600; color: var(--ink-3); }
490
+ .remote-edit-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 10px; }
491
+ .sync-pill { display: inline-flex; max-width: 100%; height: 38px; align-items: center; margin-top: 24px; padding: 0 4px 0 14px; border-radius: 999px;
492
+ background: color-mix(in srgb, var(--surface) 78%, transparent); box-shadow: var(--ring); backdrop-filter: blur(6px); font-size: 13px; white-space: nowrap; }
493
+ .sync-pill.solo { padding-right: 14px; }
494
+ .sync-state { display: inline-flex; min-width: 0; align-items: center; gap: 8px; overflow: hidden; color: var(--ink-3); text-overflow: ellipsis; }
495
+ .sync-state b { font-weight: 500; color: var(--ink); }
496
+ .sync-sep { width: 1px; height: 18px; flex-shrink: 0; margin: 0 4px 0 12px; background: var(--line); }
497
+ .sync-btn { display: inline-flex; flex-shrink: 0; height: 30px; align-items: center; gap: 6px; padding: 0 12px; border-radius: 999px; font-weight: 500; color: var(--ink-2); transition: background-color .15s, color .15s; }
498
+ .sync-btn:hover { background: var(--hover); color: var(--ink); }
499
+ .sync-btn .i { width: 14px; height: 14px; }
500
+
501
+ .dock { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 12px 14px 12px 20px; border-top: 1px solid var(--line-soft);
502
+ background: color-mix(in srgb, var(--panel) 72%, transparent); }
503
+ .dock-label { margin-right: 4px; font-size: 12.5px; color: var(--ink-3); }
504
+ .pchip { display: inline-flex; height: 34px; align-items: center; gap: 8px; padding: 0 13px 0 5px; border-radius: 999px; background: var(--surface); box-shadow: var(--ring);
505
+ font-size: 13.5px; font-weight: 500; transition: background-color .15s; }
506
+ .pchip:hover { background: var(--hover); }
507
+ .pchip:disabled { opacity: .6; }
508
+ .pchip[aria-pressed="true"] { box-shadow: 0 0 0 1.5px var(--accent), 0 0 0 4px var(--accent-soft); cursor: default; }
509
+ .pchip[aria-pressed="true"]:hover { background: var(--surface); }
510
+ .pchip .pavatar { width: 24px; height: 24px; }
511
+ .pchip .n { font-size: 12px; font-weight: 400; color: var(--ink-3); font-variant-numeric: tabular-nums; }
512
+ .pchip.add { background: transparent; box-shadow: none; font-weight: 400; color: var(--ink-2); }
513
+ .pchip.add:hover { background: var(--hover); color: var(--ink); }
514
+ .pchip.add .plus { display: grid; width: 24px; height: 24px; place-items: center; border-radius: 7px; background: var(--chip); color: var(--ink-2); }
515
+ .pchip.add .plus .i { width: 14px; height: 14px; }
516
+ .dock-remove { height: 30px; padding: 0 10px; border-radius: 999px; font-size: 12.5px; color: var(--ink-3); transition: color .15s, background-color .15s; }
517
+ .dock-remove:hover { background: color-mix(in srgb, var(--bad) 9%, transparent); color: var(--bad); }
518
+ .pform { padding: 20px; border-top: 1px solid var(--line-soft); background: color-mix(in srgb, var(--panel) 72%, transparent); animation: rise .15s ease-out; }
519
+ .pform-top { display: flex; align-items: center; gap: 12px; }
520
+ .pform-top .pavatar { width: 44px; height: 44px; filter: drop-shadow(0 4px 10px rgb(60 50 120 / .14)); }
521
+ .pform-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
522
+ .pform-note { font-size: 12.5px; color: var(--ink-3); }
523
+ .pform-fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; margin-top: 16px; }
524
+ .pfield { display: grid; gap: 6px; }
525
+ .pfield span { font-size: 12px; font-weight: 600; color: var(--ink-3); }
526
+ .pfield span i { font-style: normal; font-weight: 400; }
527
+ .pform-error { margin-top: 12px; font-size: 13px; color: var(--bad); }
528
+ .pform-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
529
+
530
+ .prefs { margin-top: 40px; }
531
+ .pref { display: flex; min-height: 64px; align-items: center; gap: 24px; padding: 14px 4px; }
532
+ .pref + .pref, .pref + .callout, .callout + .pref { border-top: 1px solid var(--line-soft); }
533
+ .prefs .callout { margin: 0; }
534
+ .pref-text { display: grid; flex: 1 0 auto; gap: 1px; }
535
+ .pref-label { font-size: 14px; font-weight: 500; }
536
+ .pref-sub { font-size: 12.5px; color: var(--ink-3); }
537
+ .pref-ctl { display: flex; flex: 0 1 auto; min-width: 0; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; }
538
+ .pref-ctl > .btn.ghost { margin-right: -10px; }
539
+ .tag { display: inline-flex; height: 28px; align-items: center; gap: 2px; padding: 0 3px 0 11px; border-radius: 999px; background: var(--chip); font-size: 13px; }
540
+ .tag button { display: grid; width: 22px; height: 22px; place-items: center; border-radius: 50%; color: var(--ink-3); }
541
+ .tag button:hover { background: var(--hover); color: var(--ink); }
477
542
  .tag .i { width: 12px; height: 12px; }
478
- .tags-input input { height: 24px; min-width: 96px; flex: 1; padding: 0 6px; font-size: 13px; outline: none; }
479
- .tags-input input::placeholder { color: var(--ink-3); }
480
- .checks li { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line-soft); font-size: 13.5px; }
481
- .checks li span { min-width: 0; overflow-wrap: anywhere; }
482
- .checks li:first-child { border-top: 0; }
483
- .checks .i { width: 16px; height: 16px; margin-top: 2px; }
484
-
485
- /* settings: the repository card */
486
- .repo-card { overflow: hidden; border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface); }
487
- .repo-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 14px; padding: 18px 20px; }
488
- .repo-card-head h2 { font-size: 16px; font-weight: 640; letter-spacing: -0.01em; }
489
- .repo-mark { display: grid; width: 40px; height: 40px; flex-shrink: 0; place-items: center; border-radius: 10px; background: var(--accent-soft); color: var(--accent); }
490
- .repo-mark .i { width: 20px; height: 20px; }
491
- .path-copy { display: inline-flex; max-width: 100%; align-items: center; gap: 6px; margin-top: 2px; font-family: var(--mono); font-size: 12.5px; color: var(--ink-3); overflow-wrap: anywhere; text-align: left; }
492
- .path-copy:hover { color: var(--ink); }
493
- .path-copy .i { width: 13px; height: 13px; opacity: 0; transition: opacity .15s; }
494
- .path-copy:hover .i, .path-copy:focus-visible .i { opacity: 1; }
495
- .pill { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: 999px; background: var(--chip); font-size: 12.5px; font-weight: 500; color: var(--ink-2); white-space: nowrap; }
496
- .pill .i { width: 14px; height: 14px; }
497
- .pill.ok { background: color-mix(in srgb, var(--ok) 13%, transparent); color: var(--ok); }
498
- .pill.warn { background: color-mix(in srgb, var(--warn) 13%, transparent); color: var(--warn); }
499
- .repo-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--line-soft); }
500
- .repo-facts > div { padding: 12px 20px; }
501
- .repo-facts > div + div { border-left: 1px solid var(--line-soft); }
502
- .repo-facts dt { font-size: 12px; color: var(--ink-3); }
503
- .repo-facts dd { margin-top: 2px; font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
504
- .remote-row { display: flex; align-items: flex-start; gap: 14px; padding: 16px 20px; border-top: 1px solid var(--line); background: var(--panel); }
505
- .remote-icon { display: grid; width: 32px; height: 32px; flex-shrink: 0; place-items: center; border-radius: 8px; background: var(--chip); color: var(--ink-3); }
506
- .remote-icon.on { background: var(--accent-soft); color: var(--accent); }
507
- .remote-icon .i { width: 17px; height: 17px; }
508
- .remote-title, .remote-label { display: block; font-size: 14px; font-weight: 500; }
509
- .remote-url { display: block; margin-top: 2px; font-size: 12.5px; color: var(--ink-2); overflow-wrap: anywhere; }
510
- .remote-help { margin-top: 3px; font-size: 12.5px; color: var(--ink-3); }
511
- .remote-form { display: flex; gap: 8px; margin-top: 8px; }
512
- .remote-form .input { height: 32px; padding: 0 10px; font-size: 12.5px; }
513
- .remote-note { margin-top: 8px; font-size: 12.5px; color: var(--ok); }
514
- .remote-note.bad { color: var(--bad); }
515
- .remote-row .inline-gap { gap: 4px; }
516
- .health-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; padding: 14px 0; }
517
- .health-icon { display: grid; width: 32px; height: 32px; flex-shrink: 0; place-items: center; border-radius: 50%; background: var(--chip); color: var(--ink-3); }
518
- .health-icon.ok { background: color-mix(in srgb, var(--ok) 13%, transparent); color: var(--ok); }
519
- .health-icon.warn { background: color-mix(in srgb, var(--warn) 13%, transparent); color: var(--warn); }
520
- .health-icon .i { width: 16px; height: 16px; }
521
- .health .checks { border-top: 1px solid var(--line-soft); }
522
- .health .setting-help code { font-family: var(--mono); }
523
- @media (max-width: 639px) {
524
- .repo-card-head, .remote-row { padding-inline: 16px; }
525
- .repo-card-head .grow { flex: 1 1 calc(100% - 56px); } /* name and path on their own line; status and Sync under them */
526
- .repo-facts > div { padding: 10px 16px; }
527
- .remote-row { flex-wrap: wrap; }
528
- .remote-form { flex-wrap: wrap; } .remote-form .input { flex-basis: 100%; }
529
- }
530
-
531
- /* settings: profiles. A profile's picture (avatar.js) is drawn from its name; CSS gives it a size. */
532
- .pavatar { display: block; width: 24px; height: 24px; flex-shrink: 0; }
543
+ .tag-add { display: inline-flex; height: 28px; align-items: center; gap: 4px; padding: 0 10px 0 8px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--line); color: var(--ink-3); cursor: text; }
544
+ .tag-add:focus-within { box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px var(--accent-soft); }
545
+ .tag-add .i { width: 13px; height: 13px; }
546
+ .tag-add input { width: 84px; font-size: 13px; outline: none; color: var(--ink); }
547
+ .tag-add input::placeholder { color: var(--ink-3); }
548
+ .code-copy { display: inline-flex; max-width: 100%; height: 28px; align-items: center; gap: 8px; padding: 0 10px; border-radius: 999px; background: var(--chip); font-family: var(--mono); font-size: 12px; color: var(--ink-2); overflow-wrap: anywhere; }
549
+ .code-copy:hover { color: var(--ink); }
550
+ .code-copy .i { width: 13px; height: 13px; }
551
+
552
+ .doctor { display: flex; flex-direction: column; align-items: center; margin-top: 40px; }
553
+ .doctor-line { display: inline-flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 2px; font-size: 12.5px; color: var(--ink-3); }
554
+ .doctor-line .sep { margin: 0 4px; }
555
+ .doctor-line button { display: inline-flex; height: 28px; align-items: center; gap: 7px; padding: 0 8px; border-radius: 999px; transition: background-color .15s, color .15s; }
556
+ .doctor-line button:hover { background: var(--hover); color: var(--ink); }
557
+ .doctor-sum { color: var(--ink-2); }
558
+ .doctor-sum.warn { color: var(--warn); }
559
+ .doctor-line .i { width: 13px; height: 13px; }
560
+ .doctor-sum .chev { transition: transform .15s; }
561
+ .doctor-sum .chev.turned { transform: rotate(180deg); }
562
+ .checks { width: 100%; margin-top: 12px; padding: 16px 20px 18px; border-radius: var(--r-card); background: var(--panel); animation: rise .15s ease-out; }
563
+ .checks-note { margin-bottom: 10px; font-size: 12px; color: var(--ink-3); }
564
+ .checks-note code { font-size: 11.5px; color: var(--ink-2); }
565
+ .checks ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 24px; }
566
+ .checks li { display: flex; min-width: 0; align-items: flex-start; gap: 8px; font-size: 12.5px; color: var(--ink-2); overflow-wrap: anywhere; }
567
+ .checks li .i { width: 14px; height: 14px; margin-top: 2px; color: var(--ok); }
568
+ .checks li.bad { color: var(--ink); }
569
+ .checks li.bad .i { color: var(--warn); }
570
+
571
+ /* profiles: a picture (avatar.js) is drawn from the name; CSS gives it a size */
572
+ .pavatar { display: block; width: 28px; height: 28px; flex-shrink: 0; }
533
573
  .svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
534
- .pavatar.sm { width: 20px; height: 20px; }
535
- .pavatar.lg { width: 40px; height: 40px; }
536
- .repo-stack { display: grid; gap: 10px; min-width: 0; }
537
- .profiles { display: grid; gap: 8px; }
538
- .ptabs { display: flex; flex-wrap: wrap; gap: 6px; }
539
- .ptab { display: inline-flex; max-width: 100%; height: 36px; align-items: center; gap: 8px; padding: 0 12px 0 6px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); font-size: 13.5px; font-weight: 500; color: var(--ink-2); transition: border-color .15s, background-color .15s, color .15s, box-shadow .15s; }
540
- .ptab:hover { background: var(--hover); color: var(--ink); }
541
- .ptab:disabled { opacity: .6; }
542
- .ptab[aria-checked="true"] { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); background: var(--surface); color: var(--ink); box-shadow: 0 0 0 3px var(--accent-soft); }
543
- .ptab-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
544
- .ptab-count { font-size: 12px; font-weight: 400; color: var(--ink-3); font-variant-numeric: tabular-nums; }
545
- .ptab.add { gap: 6px; padding: 0 12px; border-style: dashed; background: transparent; color: var(--ink-3); }
546
- .ptab.add .i { width: 15px; height: 15px; }
547
- .ptab.add:hover { color: var(--ink); }
548
- .ptab.add[aria-pressed="true"] { border-style: solid; border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); background: var(--surface); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
549
- .profiles-help { font-size: 12.5px; color: var(--ink-3); }
550
- .profiles-help b { font-weight: 600; color: var(--ink-2); }
551
- .repo-title { display: flex; align-items: center; gap: 2px; }
552
- .repo-title .btn.icon { width: 26px; height: 26px; color: var(--ink-3); }
553
- .repo-title .btn.icon .i { width: 14px; height: 14px; }
554
- .pform { display: grid; gap: 16px; padding: 20px; }
555
- .pform-top { display: flex; align-items: center; gap: 14px; }
556
- .pform-top h2 { font-size: 16px; font-weight: 640; letter-spacing: -0.01em; }
557
- .pform-sub { margin-top: 2px; font-size: 13px; color: var(--ink-3); }
558
- .pfield { display: grid; }
559
- .pfield .field-label { justify-content: flex-start; gap: 4px; }
560
- .pfield .field-label .faint { font-weight: 400; }
561
- .pfield .input { height: 36px; }
562
- .field-help { margin-top: 6px; font-size: 12.5px; color: var(--ink-3); }
563
- .pform-error { font-size: 13px; color: var(--bad); }
564
- .pform-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
565
- .rail-profile { display: grid; width: 40px; height: 40px; place-items: center; border-radius: 9px; transition: background-color .15s; }
566
- .rail-profile:hover, .rail-profile:has(+ .menu:popover-open) { background: var(--hover); }
567
- .rail-profile:has(+ .menu:popover-open)::after { display: none; } /* the menu says it already */
574
+ .rail-profile { position: relative; display: grid; width: 40px; height: 40px; place-items: center; border-radius: 9px; transition: background-color .15s, box-shadow .15s; }
575
+ .rail-profile:hover { background: var(--hover); }
576
+ .rail-profile[aria-current="page"] { background: var(--surface); box-shadow: 0 0 0 1px var(--line); }
568
577
  .rail-profile .pavatar { width: 26px; height: 26px; }
569
- .menu-item.tall { height: 36px; }
570
- .menu-note.pad { margin: 0; padding: 6px 8px 4px; }
571
- .tick-space { width: 14px; flex-shrink: 0; }
572
578
  @media (max-width: 639px) {
573
- .pform { padding: 16px; }
579
+ .settings { margin-top: 4px; }
580
+ .hero-main { padding: 40px 20px 28px; }
581
+ .pavatar.hero-pic { width: 80px; height: 80px; }
582
+ .hero-name { margin-top: 18px; font-size: 28px; }
583
+ .hero-meta { flex-direction: column; gap: 6px; }
584
+ .hero-glow { width: min(var(--glow-w), 460px); }
585
+ .sync-pill .wide-only { display: none; }
586
+ .dock { padding: 12px 12px 12px 16px; }
587
+ .dock-label { flex: 1 0 50%; margin: 0; } /* the label and Remove share the first line, the chips the next */
588
+ .dock-remove { order: 1; margin-right: -6px; }
589
+ .dock .grow { display: none; }
590
+ .dock .pchip { order: 2; }
591
+ .pform { padding: 18px 16px; }
592
+ .pform-fields { grid-template-columns: minmax(0, 1fr); }
593
+ .prefs { margin-top: 28px; }
594
+ .pref { flex-wrap: wrap; gap: 10px 16px; min-height: 60px; padding-inline: 2px; }
595
+ .pref-text { flex: 1 1 auto; }
596
+ .pref.wrap .pref-ctl { flex-basis: 100%; justify-content: flex-start; }
597
+ .checks { padding: 14px 16px; }
598
+ .checks ul { grid-template-columns: minmax(0, 1fr); }
574
599
  .rail-profile[data-tip]::after { display: none; }
575
600
  }
576
601
 
@@ -52,6 +52,11 @@ function face(n) {
52
52
  return `<g transform="${move}" opacity=".9">${eye(40 - spread)}${eye(40 + spread)}${mouth}</g>`;
53
53
  }
54
54
 
55
+ /** The picture's colours alone, without its face or corners: Settings blurs it into a glow behind the profile. */
56
+ export function glow(name) {
57
+ return raw(`<svg viewBox="0 0 80 80" preserveAspectRatio="none" aria-hidden="true">${marble(hash(name))}</svg>`);
58
+ }
59
+
55
60
  /** The picture for a profile called `name`, sized by CSS (.pavatar); its rounded corners are #pav-clip. */
56
61
  export function avatar(name, cls = "") {
57
62
  return raw(`<svg class="pavatar ${cls}" viewBox="0 0 80 80" aria-hidden="true"><g clip-path="url(#pav-clip)">${marble(hash(name))}${face(hash(`${name}face`))}</g></svg>`);
@@ -137,7 +137,6 @@ const ACTIONS = {
137
137
  },
138
138
  toggleHealth: () => { settings.healthOpen = !settings.healthOpen; app.changed(); },
139
139
  useProfile: (id) => { closeMenus(); app.useProfile(id); },
140
- manageProfiles: () => { closeMenus(); app.go("settings"); },
141
140
  profileNew: async () => { await app.newProfile(); setTimeout(() => document.getElementById("pf-folder")?.focus()); },
142
141
  profileCancel: () => app.cancelProfile(),
143
142
  profileRemove: () => app.removeProfile(),
@@ -185,14 +184,9 @@ document.addEventListener("beforetoggle", (e) => {
185
184
  const trigger = document.querySelector(`[popovertarget="${menu.id}"]`);
186
185
  if (!trigger) return;
187
186
  const r = trigger.getBoundingClientRect(), w = parseFloat(getComputedStyle(menu).width) || 288;
188
- if (menu.dataset.align === "side" && innerWidth >= 640) { // beside a trigger low in the rail, growing upwards
189
- Object.assign(menu.style, { left: `${r.right + 8}px`, top: "auto", bottom: `${Math.max(8, innerHeight - r.bottom)}px`, maxHeight: `${Math.max(160, r.bottom - 16)}px` });
190
- return;
191
- }
192
- const left = menu.dataset.align === "end" || menu.dataset.align === "side" ? r.right - w : r.left;
187
+ const left = menu.dataset.align === "end" ? r.right - w : r.left;
193
188
  menu.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, left))}px`;
194
189
  menu.style.top = `${r.bottom + 4}px`;
195
- menu.style.bottom = "auto";
196
190
  menu.style.maxHeight = `${Math.max(160, innerHeight - r.bottom - 16)}px`;
197
191
  if (menu.querySelector(".menu-find")) { app.menuQuery = ""; app.changed(); }
198
192
  }, true);
@@ -0,0 +1,56 @@
1
+ // Profiles: separate memory repositories, each its own folder and remote. Settings opens on the active one, with
2
+ // the others in its foot; the rail's last button is the active profile's picture, which leads there. A profile's name is picked for it when it is made, and its picture is
3
+ // drawn from the name.
4
+ import { avatar } from "../avatar.js";
5
+ import { attrs, html } from "../h.js";
6
+ import { icon } from "../icons.js";
7
+ import { app } from "../store.js";
8
+
9
+ /** Profiles to list, or none when the page cannot change them (AGENT_MEMORY_DATA picks the folder). */
10
+ export const shown = () => (app.profiles && !app.profiles.locked ? app.profiles.profiles : []);
11
+ export const active = () => shown().find((p) => p.active) ?? null;
12
+
13
+ // ---------------------------------------------------------------- the foot of Settings' hero
14
+
15
+ /** Every profile, in the order they were made so none moves when another is picked (the open one marked), New
16
+ * profile, and removing the open one. */
17
+ export function dock() {
18
+ const list = shown();
19
+ return html`<div class="dock" role="group" aria-label="Profiles">
20
+ <span class="dock-label">Profiles</span>
21
+ ${list.map((p) => html`<button type="button"${attrs({ class: "pchip", "data-key": `pchip-${p.id}`, "data-act": "useProfile", "data-arg": p.id, "aria-pressed": String(p.active), disabled: !!app.switching && !p.active, title: p.active ? `${p.name} is open` : `Switch to ${p.name}` })}>
22
+ ${avatar(p.name)}${p.name}<span class="n">${p.memories}</span></button>`)}
23
+ <button type="button" class="pchip add" data-act="profileNew"><span class="plus">${icon("plus")}</span>New profile</button>
24
+ <span class="grow"></span>
25
+ ${list.length > 1 ? html`<button type="button" class="dock-remove" data-act="profileRemove">Remove profile</button>` : ""}
26
+ </div>`;
27
+ }
28
+
29
+ /** In place of the foot: the name and picture the new profile will have, its folder and an optional remote. */
30
+ export function newProfile() {
31
+ const f = app.pform;
32
+ return html`<form class="pform" data-profile-form aria-label="New profile">
33
+ <div class="pform-top">${avatar(f.name)}<div><div class="pform-name">${f.name}</div><div class="pform-note">Name and picture are picked for you</div></div></div>
34
+ <div class="pform-fields">
35
+ <label class="pfield"><span>Folder</span><input id="pf-folder" class="input mono" placeholder="~/agent-memory-work" autocomplete="off" spellcheck="false" data-input="pform:folder"${attrs({ value: f.folder, disabled: f.busy })}></label>
36
+ <label class="pfield"><span>Remote <i>(optional)</i></span><input class="input mono" placeholder="git@github.com:you/agent-memory-work.git" autocomplete="off" spellcheck="false" data-input="pform:remote"${attrs({ value: f.remote, disabled: f.busy })}></label>
37
+ </div>
38
+ ${f.error ? html`<p class="pform-error" role="alert">${f.error}</p>` : ""}
39
+ <div class="pform-actions">
40
+ <button type="submit" class="btn primary"${attrs({ disabled: f.busy || !f.folder.trim() })}>${f.busy ? html`${icon("refresh-cw", "spin")}Creating…` : "Create and switch"}</button>
41
+ <button type="button" class="btn ghost" data-act="profileCancel"${attrs({ disabled: f.busy })}>Cancel</button>
42
+ </div>
43
+ </form>`;
44
+ }
45
+
46
+ // ---------------------------------------------------------------- the rail
47
+
48
+ /** The rail's last button: the active profile's picture, which opens Settings, where profiles are switched. Without
49
+ * profiles (AGENT_MEMORY_DATA picks the folder, or an older server) the rail keeps `fallback`, a Settings button. */
50
+ export function railProfile(fallback) {
51
+ const now = active();
52
+ if (!now) return fallback;
53
+ const update = app.update?.available ? app.update.latest : "";
54
+ const label = `${now.name} · Settings${update ? ` · version ${update} is out` : ""}`;
55
+ return html`<button type="button" class="rail-profile"${attrs({ "aria-label": label, "data-tip": label, "data-tip-side": "right", "aria-current": app.page === "settings" ? "page" : null, "data-act": "go", "data-arg": "settings" })}>${avatar(now.name)}${update ? html`<i class="badge" aria-hidden="true"></i>` : ""}</button>`;
56
+ }
@@ -0,0 +1,128 @@
1
+ // Settings: the open profile as a hero (its picture, name, where it lives and one sync state, with the other
2
+ // profiles in its foot), then a few quiet rows of preferences, and the installation's health as one line.
3
+ import { avatar, glow } from "../avatar.js";
4
+ import { since } from "../format.js";
5
+ import { attrs, cx, html } from "../h.js";
6
+ import { icon } from "../icons.js";
7
+ import { app } from "../store.js";
8
+ import { active, dock, newProfile, shown } from "./profiles.js";
9
+ import { segmented, toggle } from "./ui.js";
10
+
11
+ const copyCode = (text) => html`<button type="button" class="code-copy" title="Copy" data-act="copy" data-arg="${text}">${text}${icon("copy")}</button>`;
12
+ const spin = (on) => icon("refresh-cw", on ? "spin" : "");
13
+
14
+ // ---------------------------------------------------------------- the hero
15
+
16
+ function where(s) {
17
+ const st = app.settings;
18
+ const folder = s?.dataDir ?? app.data?.dataDir ?? "";
19
+ const remote = s?.remote
20
+ ? html`<span class="hero-meta-item">${icon("cloud")}<span class="v" title="${s.remoteUrl || s.remote}">${s.remote}</span><button type="button" class="hero-link" data-act="remoteEdit">Change</button></span>`
21
+ : html`<span class="hero-meta-item">${icon("cloud")}<span class="k">No remote</span><button type="button" class="hero-link" data-act="remoteEdit">Add</button></span>`;
22
+ return html`<div class="hero-meta">
23
+ <button type="button" class="hero-meta-item" title="Copy the path" data-act="copy" data-arg="${folder}">${icon("folder")}<span class="v">${folder}</span>${icon("copy", "copy-hint")}</button>
24
+ ${st.remoteEdit ? "" : remote}
25
+ </div>`;
26
+ }
27
+
28
+ function remoteEdit(s) {
29
+ const st = app.settings;
30
+ return html`<div class="remote-edit">
31
+ <label for="remote-url">${s?.remote ? "Remote" : "Connect a remote"}</label>
32
+ <input id="remote-url" class="input mono" placeholder="git@github.com:you/agent-memory-data.git" autocomplete="off" spellcheck="false" data-input="remoteDraft"${attrs({ value: st.remoteDraft, disabled: st.remoteBusy })}>
33
+ <div class="remote-edit-row">
34
+ <button type="button" class="btn primary" data-act="remoteSave"${attrs({ disabled: st.remoteBusy || !st.remoteDraft.trim() })}>${st.remoteBusy ? html`${spin(true)}Connecting…` : s?.remote ? "Save" : "Connect"}</button>
35
+ <button type="button" class="btn ghost" data-act="remoteCancel"${attrs({ disabled: st.remoteBusy })}>Cancel</button>
36
+ ${s?.remote ? html`<span class="grow"></span><button type="button" class="btn ghost bad-text" data-act="remoteDisconnect"${attrs({ disabled: st.remoteBusy })}>Disconnect</button>` : ""}
37
+ </div>
38
+ </div>`;
39
+ }
40
+
41
+ function syncPill(s) {
42
+ const changed = s?.lastCommit ? html`<span class="wide-only">changed </span>${since(s.lastCommit)}` : "no changes yet";
43
+ if (!s?.remote) return html`<div class="sync-pill solo" role="status"><span class="sync-state"><i class="dot off"></i><span><b>On this machine only</b> · ${changed}</span></span></div>`;
44
+ const state = app.unpushed
45
+ ? html`<span class="sync-state"><i class="dot warn"></i><b>${app.unpushed === 1 ? "1 change" : `${app.unpushed} changes`} not pushed</b></span>`
46
+ : html`<span class="sync-state"><i class="dot ok"></i><span><b>Synced</b> · ${changed}</span></span>`;
47
+ return html`<div class="sync-pill" role="status">${state}<span class="sync-sep"></span>
48
+ <button type="button" class="sync-btn" data-act="syncNow"${attrs({ disabled: app.settings.syncing })}>${spin(app.settings.syncing)}Sync now</button></div>`;
49
+ }
50
+
51
+ function hero() {
52
+ const s = app.settings.s, st = app.settings, p = active();
53
+ const name = p?.name ?? s?.name ?? "agent-memory";
54
+ const count = `${app.M.length} ${app.M.length === 1 ? "memory" : "memories"}`;
55
+ const role = !p ? "" : app.profiles.demo ? "The profile the page shows" : "New sessions use this profile";
56
+ const note = st.remoteNote ? html`<p${attrs({ class: cx("hero-note", !st.remoteNote.ok && "bad"), role: "status" })}>${st.remoteNote.text}</p>` : "";
57
+ return html`<section class="hero" data-key="hero" aria-label="${p ? "The open profile" : "The data repository"}">
58
+ <div class="hero-glow" aria-hidden="true">${glow(name)}</div>
59
+ <div class="hero-main">
60
+ ${avatar(name, "hero-pic")}
61
+ <h2 class="hero-name">${name}</h2>
62
+ <p class="hero-sub">${count}${role ? html`<span class="sep">·</span>${role}` : ""}</p>
63
+ ${where(s)}
64
+ ${st.remoteEdit ? remoteEdit(s) : ""}
65
+ ${note}
66
+ ${syncPill(s)}
67
+ </div>
68
+ ${shown().length ? (app.pform ? newProfile() : dock()) : ""}
69
+ </section>`;
70
+ }
71
+
72
+ // ---------------------------------------------------------------- the quiet rows
73
+
74
+ const pref = (label, sub, control, cls = "") => html`<div${attrs({ class: cx("pref", cls) })}>
75
+ <div class="pref-text"><span class="pref-label">${label}</span>${sub ? html`<span class="pref-sub">${sub}</span>` : ""}</div>
76
+ <div class="pref-ctl">${control}</div>
77
+ </div>`;
78
+
79
+ function prefs() {
80
+ const s = app.settings.s, st = app.settings, u = app.update;
81
+ const words = s?.keywordStopwords ?? [];
82
+ const tags = html`${words.map((w) => html`<span class="tag" data-key="tag-${w}">${w}<button type="button"${attrs({ "aria-label": `Remove ${w}`, "data-act": "removeWord", "data-arg": w })}>${icon("x", "", 2.4)}</button></span>`)}
83
+ <label class="tag-add">${icon("plus")}<input id="add-word" placeholder="Add a word" aria-label="Add a word that is never a keyword" data-input="word"${attrs({ value: st.word })}></label>`;
84
+ // searching by meaning only needs a row when something is to be done about it
85
+ const meaning = !s ? "" : !s.model.ready
86
+ ? pref("Search by meaning", `Off · the model is ${s.model.size}`, copyCode(s.model.fetch))
87
+ : !s.semantic ? pref("Search by meaning", "Restart mem ui to turn it on", "") : "";
88
+ let update;
89
+ if (u?.available && u.oneClick) {
90
+ update = html`<button type="button" class="btn primary" data-act="runUpdate"${attrs({ disabled: !!app.updating })}>
91
+ ${app.updating ? html`${spin(true)}${app.updating === "restarting" ? "Restarting…" : "Updating…"}` : html`${icon("circle-arrow-up")}Update to ${u.latest}`}</button>`;
92
+ } else if (u?.available) update = copyCode(u.command);
93
+ else update = html`<button type="button" class="btn ghost" data-act="checkNow"${attrs({ disabled: st.lookingUp })}>${spin(st.lookingUp)}Check now</button>`;
94
+ const said = !u ? "Checking for updates…" : u.available ? `${u.latest} is out` : u.latest ? "Up to date" : u.auto ? "Could not reach PyPI" : "";
95
+ const theme = segmented("Theme", app.theme, [{ value: "system", label: "System" }, { value: "light", label: "Light" }, { value: "dark", label: "Dark" }], "setTheme");
96
+ return html`<section class="prefs" aria-label="Preferences">
97
+ ${pref("Excluded words", "Never become keyword chips or filters", tags, "wrap")}
98
+ ${meaning}
99
+ ${pref("Theme", "", theme)}
100
+ ${pref(`agent-memory ${u?.current ?? s?.version ?? ""}`, said, update)}
101
+ ${st.failed ? html`<div class="callout bad"><div><div class="callout-title">${st.failed.error}</div>${st.failed.log ? html`<pre>${st.failed.log}</pre>` : ""}</div></div>` : ""}
102
+ ${pref("Check for updates daily", "PyPI is sent only the package name", toggle(u?.auto ?? true, "Check for updates daily", "autoUpdate"))}
103
+ </section>`;
104
+ }
105
+
106
+ // ---------------------------------------------------------------- the installation, one line
107
+
108
+ function doctor() {
109
+ const { checks, checking, healthOpen } = app.settings;
110
+ const failing = (checks ?? []).filter((c) => !c.ok);
111
+ const summary = !checks ? "Checking the installation…" : failing.length ? `${failing.length} of ${checks.length} checks failing` : `All ${checks.length} checks pass`;
112
+ const list = (items) => html`<ul>${items.map((c) => html`<li${attrs({ class: c.ok ? null : "bad" })}>${icon(c.ok ? "check" : "triangle-alert")}<span>${c.text}</span></li>`)}</ul>`;
113
+ return html`<section class="doctor" aria-label="Installation">
114
+ <div class="doctor-line">
115
+ <button type="button"${attrs({ class: cx("doctor-sum", failing.length && "warn"), "data-act": "toggleHealth", "aria-expanded": String(!!healthOpen), disabled: !checks?.length })}>
116
+ <i${attrs({ class: cx("dot", checks ? (failing.length ? "warn" : "ok") : "off") })}></i>${summary}${checks?.length ? icon("chevron-down", healthOpen ? "chev turned" : "chev") : ""}
117
+ </button>
118
+ <span class="sep">·</span>
119
+ <button type="button" class="doctor-again" data-act="runChecks"${attrs({ disabled: checking })}>${spin(checking)}Run again</button>
120
+ </div>
121
+ ${healthOpen && checks ? html`<div class="checks"><p class="checks-note">The same checks as <code>mem doctor</code>.</p>${list(checks)}</div>`
122
+ : failing.length ? html`<div class="checks">${list(failing)}</div>` : ""}
123
+ </section>`;
124
+ }
125
+
126
+ export function settingsPage() {
127
+ return html`<div class="settings">${hero()}${prefs()}${doctor()}</div>`;
128
+ }
@@ -35,8 +35,7 @@ function rail() {
35
35
  </button>
36
36
  ${railButton("insights", "Insights", "chart-column")}
37
37
  <div class="spacer"></div>
38
- ${railProfile()}
39
- ${railButton("settings", settings, "settings-2")}
38
+ ${railProfile(railButton("settings", settings, "settings-2"))}
40
39
  </nav>`;
41
40
  }
42
41
 
@@ -35,6 +35,3 @@ export const select = (label, value, items, act, cls = "", placeholder = "") =>
35
35
  /** A menu that opens under its trigger (the trigger names it with popovertarget). */
36
36
  export const menu = (id, content, cls = "", align = "start") =>
37
37
  html`<div${attrs({ id, popover: "", class: `menu ${cls}`, "data-align": align, "data-live": "style" })}>${content}</div>`;
38
-
39
- /** A note that something is good, waiting or wrong. */
40
- export const status = (tone, text) => html`<span class="status"><i class="dot ${tone}"></i>${text}</span>`;
@@ -1,78 +0,0 @@
1
- // Profiles: separate memory repositories, each its own folder and remote. Settings lists them above the
2
- // repository card, which shows the active one; the rail offers a quick switch once there are several. A
3
- // profile's name is picked for it when it is made, and its picture is drawn from the name.
4
- import { avatar } from "../avatar.js";
5
- import { plural } from "../format.js";
6
- import { attrs, cx, html } from "../h.js";
7
- import { icon } from "../icons.js";
8
- import { app } from "../store.js";
9
- import { menu } from "./ui.js";
10
-
11
- /** Profiles to list, or none when the page cannot change them (AGENT_MEMORY_DATA picks the folder). */
12
- export const shown = () => (app.profiles && !app.profiles.locked ? app.profiles.profiles : []);
13
- export const active = () => shown().find((p) => p.active) ?? null;
14
-
15
- // ---------------------------------------------------------------- the strip above the repository card
16
-
17
- export function profileStrip() {
18
- const list = shown();
19
- if (!list.length) return "";
20
- const adding = !!app.pform, now = active();
21
- return html`<div class="profiles">
22
- <div class="ptabs" role="radiogroup" aria-label="Profiles">
23
- ${list.map((p) => html`<button type="button" role="radio"${attrs({ class: "ptab", "data-key": `ptab-${p.id}`, "aria-checked": String(p.active && !adding), "data-act": "useProfile", "data-arg": p.id, disabled: !!app.switching, title: p.active ? `${p.name} · ${p.dataDir}` : `Switch to ${p.name}` })}>
24
- ${avatar(p.name)}<span class="ptab-name">${p.name}</span><span class="ptab-count">${p.memories}</span>
25
- </button>`)}
26
- <button type="button"${attrs({ class: cx("ptab", "add"), "aria-pressed": String(adding), "data-act": "profileNew" })}>${icon("plus")}New profile</button>
27
- </div>
28
- ${list.length > 1 && now && !adding ? html`<p class="profiles-help">${app.profiles.demo
29
- ? html`Set up for real, new Claude Code and Codex sessions would use <b>${now.name}</b>; the demo is not connected to them.`
30
- : html`New Claude Code and Codex sessions use <b>${now.name}</b>. Sessions already running keep the profile they started in.`}</p>` : ""}
31
- </div>`;
32
- }
33
-
34
- // ---------------------------------------------------------------- a new profile
35
-
36
- /** The card that takes the repository card's place while a profile is being added. */
37
- export function profileNewCard() {
38
- const f = app.pform;
39
- return html`<section class="repo-card">
40
- <form class="pform" data-profile-form>
41
- <div class="pform-top">
42
- ${avatar(f.name, "lg")}
43
- <div class="grow">
44
- <h2>${f.name}</h2>
45
- <p class="pform-sub">A new profile: a separate set of memories in its own repository. Its name and picture are picked for it.</p>
46
- </div>
47
- </div>
48
- <label class="pfield"><span class="field-label">Folder</span>
49
- <input id="pf-folder" class="input mono" placeholder="~/agent-memory-work" autocomplete="off" spellcheck="false" data-input="pform:folder"${attrs({ value: f.folder, disabled: f.busy })}>
50
- <span class="field-help">A new or empty folder becomes a memory repository. One that already is one is used as it is.</span>
51
- </label>
52
- <label class="pfield"><span class="field-label">Remote <span class="faint">(optional)</span></span>
53
- <input class="input mono" placeholder="git@github.com:you/agent-memory-work.git" autocomplete="off" spellcheck="false" data-input="pform:remote"${attrs({ value: f.remote, disabled: f.busy })}>
54
- <span class="field-help">Leave it empty to keep these memories on this machine. With a new folder, a remote that already has memories is cloned; an empty one gets the new repository pushed to it.</span>
55
- </label>
56
- ${f.error ? html`<p class="pform-error" role="alert">${f.error}</p>` : ""}
57
- <div class="pform-actions">
58
- <button type="submit" class="btn primary sm"${attrs({ disabled: f.busy || !f.folder.trim() })}>${f.busy ? html`${icon("refresh-cw", "spin")}Creating…` : "Create and switch"}</button>
59
- <button type="button" class="btn ghost sm" data-act="profileCancel"${attrs({ disabled: f.busy })}>Cancel</button>
60
- </div>
61
- </form>
62
- </section>`;
63
- }
64
-
65
- // ---------------------------------------------------------------- the rail
66
-
67
- export function railProfile() {
68
- const list = shown(), now = active();
69
- if (list.length < 2 || !now) return "";
70
- return html`<button type="button" class="rail-profile" popovertarget="menu-profiles"${attrs({ "aria-label": `Profile: ${now.name}`, "data-tip": `Profile: ${now.name}`, "data-tip-side": "right" })}>${avatar(now.name)}</button>
71
- ${menu("menu-profiles", html`<div class="menu-list" role="menu">
72
- <div class="menu-note pad">${app.profiles.demo ? "The profile the page shows." : "New sessions use the profile picked here."}</div>
73
- ${list.map((p) => html`<button type="button" role="menuitemradio"${attrs({ class: "menu-item tall", "aria-checked": String(p.active), "data-act": "useProfile", "data-arg": p.id })}>
74
- ${avatar(p.name, "sm")}<span class="grow truncate">${p.name}</span><span class="count">${plural(p.memories, "memory", "memories")}</span>${p.active ? icon("check", "accent") : html`<span class="tick-space"></span>`}
75
- </button>`)}
76
- </div>
77
- <div class="menu-foot"><button type="button" class="menu-wide" data-act="manageProfiles">Manage profiles</button></div>`, "", "side")}`;
78
- }
@@ -1,138 +0,0 @@
1
- // Settings: the profiles and the data repository first (where it is, its remote, sync), then search, general
2
- // settings, and health as one line that opens when asked or when something fails.
3
- import { plural, since } from "../format.js";
4
- import { attrs, cx, html } from "../h.js";
5
- import { icon } from "../icons.js";
6
- import { app } from "../store.js";
7
- import { avatar } from "../avatar.js";
8
- import { active, profileNewCard, profileStrip, shown } from "./profiles.js";
9
- import { segmented, status, toggle } from "./ui.js";
10
-
11
- /** A setting: its name and help on the left, the control on the right. */
12
- const row = (label, help, control) => html`<div class="setting">
13
- <div><div class="setting-name">${label}</div>${help ? html`<div class="setting-help">${help}</div>` : ""}</div>
14
- <div class="setting-control">${control}</div>
15
- </div>`;
16
-
17
- const copyCode = (text) => html`<button type="button" class="code-copy" title="Copy" data-act="copy" data-arg="${text}"><span>${text}</span>${icon("copy")}</button>`;
18
-
19
- function syncState(s) {
20
- if (!s?.remote) return html`<span class="pill">${icon("laptop")}On this machine only</span>`;
21
- if (app.unpushed) return html`<span class="pill warn">${plural(app.unpushed, "change")} not pushed</span>`;
22
- return html`<span class="pill ok">${icon("check", "", 2.5)}Synced</span>`;
23
- }
24
-
25
- function remote(s) {
26
- const st = app.settings;
27
- const note = st.remoteNote ? html`<p${attrs({ class: cx("remote-note", !st.remoteNote.ok && "bad") })} role="status">${st.remoteNote.text}</p>` : "";
28
- if (st.remoteEdit) {
29
- return html`<div class="remote-row editing">
30
- <span class="remote-icon">${icon("cloud")}</span>
31
- <div class="grow">
32
- <label class="remote-label" for="remote-url">${s?.remote ? "Change the remote" : "Connect a remote"}</label>
33
- <div class="remote-form">
34
- <input id="remote-url" class="input mono" placeholder="git@github.com:you/agent-memory-data.git" autocomplete="off" spellcheck="false" data-input="remoteDraft"${attrs({ value: st.remoteDraft, disabled: st.remoteBusy })}>
35
- <button type="button" class="btn primary sm" data-act="remoteSave"${attrs({ disabled: st.remoteBusy || !st.remoteDraft.trim() })}>${st.remoteBusy ? html`${icon("refresh-cw", "spin")}Connecting…` : s?.remote ? "Save" : "Connect"}</button>
36
- <button type="button" class="btn ghost sm" data-act="remoteCancel"${attrs({ disabled: st.remoteBusy })}>Cancel</button>
37
- </div>
38
- <p class="remote-help">An empty private repository works: this one is pushed to it. One that other machines already push to is pulled from first.</p>
39
- ${note}
40
- </div>
41
- </div>`;
42
- }
43
- if (!s?.remote) {
44
- return html`<div class="remote-row">
45
- <span class="remote-icon">${icon("cloud")}</span>
46
- <div class="grow"><div class="remote-title">No remote</div>
47
- <p class="remote-help">Every change is still committed here. Add a private remote to keep your other machines in sync.</p>${note}</div>
48
- <button type="button" class="btn sm" data-act="remoteEdit">${icon("plus")}Add remote</button>
49
- </div>`;
50
- }
51
- return html`<div class="remote-row">
52
- <span class="remote-icon on">${icon("cloud")}</span>
53
- <div class="grow"><div class="remote-title">Remote</div><code class="remote-url">${s.remoteUrl || s.remote}</code>${note}</div>
54
- <div class="inline-gap">
55
- <button type="button" class="btn ghost sm" data-act="remoteEdit">Change</button>
56
- <button type="button" class="btn ghost sm bad-text" data-act="remoteDisconnect">Disconnect</button>
57
- </div>
58
- </div>`;
59
- }
60
-
61
- function repository() {
62
- if (app.pform) return profileNewCard();
63
- const s = app.settings.s, p = active();
64
- const facts = [
65
- ["Memories", String(app.M.length)],
66
- ["Last change", s?.lastCommit ? since(s.lastCommit) : "never"],
67
- ];
68
- const head = html`<header class="repo-card-head">
69
- ${p ? avatar(p.name, "lg") : html`<span class="repo-mark">${icon("git-branch")}</span>`}
70
- <div class="grow">
71
- <h2 class="repo-title">${p?.name ?? s?.name ?? "Data repository"}${p && shown().length > 1 ? html`<button type="button" class="btn ghost icon" aria-label="Remove this profile" data-tip="Remove this profile" data-act="profileRemove">${icon("trash")}</button>` : ""}</h2>
72
- <button type="button" class="path-copy" title="Copy the path" data-act="copy" data-arg="${s?.dataDir ?? ""}">${s?.dataDir ?? app.data?.dataDir}${icon("copy")}</button>
73
- </div>
74
- ${syncState(s)}
75
- ${s?.remote ? html`<button type="button" class="btn sm" data-act="syncNow"${attrs({ disabled: app.settings.syncing })}>${icon("refresh-cw", app.settings.syncing ? "spin" : "")}Sync now</button>` : ""}
76
- </header>`;
77
- return html`<section class="repo-card" data-key="repo-card">
78
- ${head}
79
- <dl class="repo-facts">${facts.map(([k, v]) => html`<div><dt>${k}</dt><dd>${v}</dd></div>`)}</dl>
80
- ${remote(s)}
81
- </section>`;
82
- }
83
-
84
- function search() {
85
- const s = app.settings.s;
86
- const words = s?.keywordStopwords ?? [];
87
- const model = html`<div class="model-state">${s?.model.ready
88
- ? status("ok", `Ready${s.semantic ? "" : " (restart mem ui to use it)"}`)
89
- : html`${status("off", "Not downloaded")}${s ? copyCode(s.model.fetch) : ""}`}</div>`;
90
- return html`<section class="group"><h2>Search</h2>
91
- ${row("Search by meaning", `${s?.model.name ?? "A local embedding model"}, on the CPU (${s?.model.size ?? "about 625 MB"}), across Chinese and English.`, model)}
92
- ${row("Never use as keywords", "Words such as your own name, kept out of keyword chips and filters.", html`<div class="tags-input">
93
- ${words.map((w) => html`<span class="tag" data-key="tag-${w}">${w}<button type="button"${attrs({ "aria-label": `Remove ${w}`, "data-act": "removeWord", "data-arg": w })}>${icon("x")}</button></span>`)}
94
- <input id="add-word" placeholder="Add a word" data-input="word"${attrs({ value: app.settings.word })}>
95
- </div>`)}
96
- </section>`;
97
- }
98
-
99
- function general() {
100
- const u = app.update, st = app.settings, s = st.s;
101
- let control;
102
- if (u?.available && u.oneClick) {
103
- control = html`<button type="button" class="btn primary sm" data-act="runUpdate"${attrs({ disabled: !!app.updating })}>
104
- ${app.updating ? html`${icon("refresh-cw", "spin")}${app.updating === "restarting" ? "Restarting…" : "Updating…"}` : html`${icon("circle-arrow-up")}Update to ${u.latest}`}</button>`;
105
- } else if (u?.available) control = copyCode(u.command);
106
- else control = html`<button type="button" class="btn sm" data-act="checkNow"${attrs({ disabled: st.lookingUp })}>${icon("refresh-cw", st.lookingUp ? "spin" : "")}Check now</button>`;
107
- const help = !u ? "Checking for updates…"
108
- : u.available ? `Version ${u.latest} is out.${u.oneClick ? " Updating also refreshes the hooks and restarts this page." : u.method === "dev" ? " This is a working copy; in it, run:" : " Update with:"}`
109
- : u.latest ? `Up to date${u.checked ? `, checked ${since(u.checked)}` : ""}.` : u.auto ? "Could not reach PyPI to check." : "Automatic checks are off.";
110
- const theme = segmented("Theme", app.theme, [{ value: "system", label: "System" }, { value: "light", label: "Light" }, { value: "dark", label: "Dark" }], "setTheme");
111
- return html`<section class="group"><h2>General</h2>
112
- ${row("Theme", "Follow the system, or pick one.", theme)}
113
- ${row(`agent-memory ${u?.current ?? s?.version ?? ""}`, help, control)}
114
- ${st.failed ? html`<div class="callout bad"><div><div class="callout-title">${st.failed.error}</div>${st.failed.log ? html`<pre>${st.failed.log}</pre>` : ""}</div></div>` : ""}
115
- ${row("Check for updates", "Once a day this page asks PyPI for the latest version; only the package name is sent.", toggle(u?.auto ?? true, "Check for updates automatically", "autoUpdate"))}
116
- </section>`;
117
- }
118
-
119
- function health() {
120
- const { checks, checking, healthOpen } = app.settings;
121
- const failing = (checks ?? []).filter((c) => !c.ok);
122
- const shown = healthOpen ? checks ?? [] : failing;
123
- const summary = !checks ? "Checking the installation…"
124
- : failing.length ? `${failing.length} of ${checks.length} checks failing` : `All ${checks.length} checks pass`;
125
- return html`<section class="group health">
126
- <div class="health-summary">
127
- <span${attrs({ class: cx("health-icon", checks && (failing.length ? "warn" : "ok")) })}>${icon(failing.length ? "triangle-alert" : "check", "", 2.5)}</span>
128
- <div class="grow"><div class="setting-name">${summary}</div><div class="setting-help">The same checks as <code>mem doctor</code>.</div></div>
129
- ${checks?.length ? html`<button type="button" class="btn ghost sm" data-act="toggleHealth">${healthOpen ? "Hide" : "Show all"}</button>` : ""}
130
- <button type="button" class="btn sm" data-act="runChecks"${attrs({ disabled: checking })}>${icon("refresh-cw", checking ? "spin" : "")}Run again</button>
131
- </div>
132
- ${shown.length ? html`<ul class="checks">${shown.map((c) => html`<li>${icon(c.ok ? "check" : "triangle-alert", c.ok ? "ok-text" : "warn-text")}<span>${c.text}</span></li>`)}</ul>` : ""}
133
- </section>`;
134
- }
135
-
136
- export function settingsPage() {
137
- return html`<div class="settings"><div class="repo-stack">${profileStrip()}${repository()}</div>${search()}${general()}${health()}</div>`;
138
- }