agent-memory-git 0.1.9__tar.gz → 0.1.11__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 (70) hide show
  1. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/PKG-INFO +3 -3
  2. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/README.md +2 -2
  3. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/pyproject.toml +1 -1
  4. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/pyproject.toml.orig +1 -1
  5. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/PROTOCOL.md +1 -1
  6. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/cli.py +15 -4
  7. agent_memory_git-0.1.11/src/agent_memory/hooks.py +141 -0
  8. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/install.py +5 -1
  9. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/ui_server.py +6 -1
  10. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/app.css +159 -124
  11. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/avatar.js +5 -0
  12. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/main.js +2 -8
  13. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/store.js +22 -5
  14. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/overlays.js +3 -0
  15. agent_memory_git-0.1.11/src/agent_memory/webui/js/views/profiles.js +66 -0
  16. agent_memory_git-0.1.11/src/agent_memory/webui/js/views/settings.js +128 -0
  17. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/ui.js +0 -3
  18. agent_memory_git-0.1.9/src/agent_memory/hooks.py +0 -122
  19. agent_memory_git-0.1.9/src/agent_memory/webui/js/views/profiles.js +0 -85
  20. agent_memory_git-0.1.9/src/agent_memory/webui/js/views/settings.js +0 -138
  21. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/LICENSE +0 -0
  22. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/__init__.py +0 -0
  23. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/__main__.py +0 -0
  24. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/capture.py +0 -0
  25. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/config.py +0 -0
  26. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/context.py +0 -0
  27. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +0 -0
  28. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +0 -0
  29. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +0 -0
  30. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +0 -0
  31. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/10/20261009-thesis-setup-fr6e.md +0 -0
  32. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260804-alex-profile-ujzd.md +0 -0
  33. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260804-reply-language-xdnc.md +0 -0
  34. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260812-ask-before-push-pf3d.md +0 -0
  35. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260820-tests-before-done-odzd.md +0 -0
  36. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260827-etl-orders-export-deadline-pft7.md +0 -0
  37. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260828-etl-dashboard-72ce.md +0 -0
  38. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260902-report-eta-is2j.md +0 -0
  39. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260908-shop-web-stripe-checkout-lgmx.md +0 -0
  40. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260910-shop-web-deploys-dn73.md +0 -0
  41. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260915-etl-parquet-partitioning-seh2.md +0 -0
  42. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260922-design-tokens-source-uvw7.md +0 -0
  43. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/10/20261002-shop-web-v3-freeze-5xtp.md +0 -0
  44. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/10/20261012-payments-runbook-4syw.md +0 -0
  45. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/demo.py +0 -0
  46. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/embed_worker.py +0 -0
  47. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/frontmatter.py +0 -0
  48. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/gitsync.py +0 -0
  49. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/importers.py +0 -0
  50. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/lint.py +0 -0
  51. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/profiles.py +0 -0
  52. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/select.py +0 -0
  53. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/sessions.py +0 -0
  54. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/store.py +0 -0
  55. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/ui.py +0 -0
  56. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/update.py +0 -0
  57. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/util.py +0 -0
  58. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/index.html +0 -0
  59. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/api.js +0 -0
  60. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/filters.js +0 -0
  61. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/format.js +0 -0
  62. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/h.js +0 -0
  63. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/icons.js +0 -0
  64. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/markdown.js +0 -0
  65. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/search.js +0 -0
  66. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/insights.js +0 -0
  67. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/list.js +0 -0
  68. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/memory.js +0 -0
  69. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/shell.js +0 -0
  70. {agent_memory_git-0.1.9 → agent_memory_git-0.1.11}/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.9
3
+ Version: 0.1.11
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
@@ -58,8 +58,8 @@ Description-Content-Type: text/markdown
58
58
  - **Memory in git.** One Markdown file per memory in a git repository you own, with where it came
59
59
  from recorded by the hooks. Every change is a commit you can review or revert; add a private
60
60
  remote to sync machines, or keep it all on one. Here is [an example](https://github.com/Ki-Seki/agent-memory-data-demo).
61
- [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep work and personal
62
- memories in repositories of their own.
61
+ [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep separate sets of
62
+ memories in repositories of their own, and a switch reaches running sessions too.
63
63
  - **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
64
64
  - **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
65
65
  what changed week by week.
@@ -37,8 +37,8 @@
37
37
  - **Memory in git.** One Markdown file per memory in a git repository you own, with where it came
38
38
  from recorded by the hooks. Every change is a commit you can review or revert; add a private
39
39
  remote to sync machines, or keep it all on one. Here is [an example](https://github.com/Ki-Seki/agent-memory-data-demo).
40
- [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep work and personal
41
- memories in repositories of their own.
40
+ [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep separate sets of
41
+ memories in repositories of their own, and a switch reaches running sessions too.
42
42
  - **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
43
43
  - **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
44
44
  what changed week by week.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.9"
3
+ version = "0.1.11"
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.9"
3
+ version = "0.1.11"
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
  # Shared memory
2
2
 
3
- You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. If that block is missing, the memory system is not active in this session; ignore this section.
3
+ You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. If the active profile changes while the session runs, a new index arrives with your next message and replaces the earlier one: from then on use only its memories and paths. If that block is missing, the memory system is not active in this session; ignore this section.
4
4
 
5
5
  ## Reading
6
6
 
@@ -249,25 +249,36 @@ def _access_note():
249
249
  print(f"updated {' and '.join(tools)} so agents can reach every profile's folder")
250
250
 
251
251
 
252
+ SWITCH_NOTE = "they keep what they've already read; start a new session for a clean slate"
253
+
254
+
255
+ def _switched(name):
256
+ print(f"sessions now use {name}; running ones switch with their next message")
257
+ if config.load().get("switch_note", True):
258
+ print(SWITCH_NOTE)
259
+
260
+
252
261
  def cmd_profile(args):
253
262
  if profiles.locked():
254
263
  sys.exit("AGENT_MEMORY_DATA picks the data repository here, so profiles do not apply")
255
264
  try:
256
265
  if args.action == "use":
257
266
  pid = profiles.use(args.profile)
258
- print(f"new sessions now use {profiles.listing()[0][pid]['name']}")
267
+ _switched(profiles.listing()[0][pid]["name"])
259
268
  elif args.action == "add":
260
269
  pid, note = profiles.add(args.folder, remote=args.remote, switch=not args.keep)
261
270
  found, active = profiles.listing()
262
271
  print(note)
263
- print(f"added {found[pid]['name']}" + ("; new sessions now use it" if active == pid else ""))
272
+ print(f"added {found[pid]['name']}")
273
+ if active == pid:
274
+ _switched(found[pid]["name"])
264
275
  _access_note()
265
276
  elif args.action == "remove":
266
277
  found, _ = profiles.listing()
267
278
  pid = profiles.find(args.profile, found)
268
279
  active = profiles.remove(pid)
269
280
  print(f"removed {found[pid]['name']}; {profiles.short(found[pid]['data_dir'])} is left as it is")
270
- print(f"new sessions use {profiles.listing()[0][active]['name']}")
281
+ _switched(profiles.listing()[0][active]["name"])
271
282
  _access_note()
272
283
  else:
273
284
  for p in profiles.summary()["profiles"]:
@@ -387,7 +398,7 @@ def main(argv=None):
387
398
  sp.set_defaults(fn=cmd_profile)
388
399
 
389
400
  sp = sub.add_parser("hook", help=argparse.SUPPRESS)
390
- sp.add_argument("event", choices=["session-start", "stop", "session-end"])
401
+ sp.add_argument("event") # any name: an event from a newer install is logged and ignored, never refused
391
402
  sp.add_argument("--tool", required=True, choices=["claude-code", "codex"])
392
403
  sp.set_defaults(fn=cmd_hook)
393
404
 
@@ -0,0 +1,141 @@
1
+ """Hook entry points shared by Claude Code and Codex.
2
+
3
+ Both tools send a JSON payload on stdin with at least `session_id` and `cwd`,
4
+ and accept `hookSpecificOutput.additionalContext` from SessionStart hooks.
5
+ A hook must never break the host session, so every failure is logged and
6
+ swallowed.
7
+ """
8
+
9
+ import json
10
+ import secrets
11
+ import sys
12
+ import time
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ from . import capture, config, gitsync, profiles, select, store, workdir
17
+ from .context import detect
18
+ from .util import log, log_exc
19
+
20
+ PULL_INTERVAL = 60
21
+ SWITCHED = (
22
+ "The active memory profile changed while this session was running. This index replaces the one shown "
23
+ "earlier: from now on use only its memories directory, its staging directory and the memories it lists, "
24
+ "and set aside memories you read under the earlier one.\n\n"
25
+ )
26
+
27
+
28
+ def _payload():
29
+ try:
30
+ data = sys.stdin.read()
31
+ return json.loads(data) if data.strip() else {}
32
+ except (OSError, ValueError):
33
+ return {}
34
+
35
+
36
+ def _session_id(payload):
37
+ return str(payload.get("session_id") or f"nosession-{secrets.token_hex(4)}")
38
+
39
+
40
+ def session_start(payload, tool, data_dir):
41
+ sid = _session_id(payload)
42
+ ctx = detect(payload.get("cwd"))
43
+ sdir = capture.session_dir(data_dir, tool, sid)
44
+ sdir.mkdir(parents=True, exist_ok=True)
45
+ capture.write_session(
46
+ sdir,
47
+ {
48
+ "tool": tool,
49
+ "session_id": sid,
50
+ "session_title": payload.get("session_title"),
51
+ "model": payload.get("model"),
52
+ "transcript_path": payload.get("transcript_path"),
53
+ "ctx": ctx,
54
+ "shown_at": time.time(),
55
+ },
56
+ )
57
+
58
+ recovered = capture.recover_orphans(data_dir, keep=sdir)
59
+ stamp = Path(data_dir) / ".cache" / "last-pull"
60
+ if gitsync.has_remote(data_dir):
61
+ last = stamp.stat().st_mtime if stamp.exists() else 0
62
+ if recovered or time.time() - last > PULL_INTERVAL:
63
+ stamp.parent.mkdir(parents=True, exist_ok=True)
64
+ stamp.touch()
65
+ gitsync.spawn_sync(data_dir, push=recovered)
66
+
67
+ paths = {
68
+ "memories": str(store.memories_dir(data_dir)),
69
+ "staging": str(sdir),
70
+ "mem": str(config.BIN),
71
+ }
72
+ return select.render(
73
+ store.load_all(data_dir),
74
+ ctx,
75
+ paths,
76
+ throwaway=workdir.is_throwaway(ctx.get("cwd")),
77
+ profile=profiles.active_name(),
78
+ )
79
+
80
+
81
+ def _shown(sdir):
82
+ """When this staging folder's profile last gave the session its index; 0 when it never did."""
83
+ info = capture.read_session(sdir)
84
+ if isinstance(info.get("shown_at"), (int, float)):
85
+ return info["shown_at"]
86
+ try: # sessions that started before shown_at existed got their index when the folder was made
87
+ return datetime.fromisoformat(info["started_at"]).timestamp()
88
+ except (KeyError, TypeError, ValueError):
89
+ return 0
90
+
91
+
92
+ def prompt(payload, tool, data_dir):
93
+ """The active profile's index when the session last got another profile's; None when nothing changed.
94
+ Running sessions follow the active profile, so a switch reaches them with their next message."""
95
+ sid = payload.get("session_id")
96
+ if not sid:
97
+ return None
98
+ seen = [(_shown(sdir), d) for d in profiles.dirs() if (sdir := capture.session_dir(d, tool, str(sid))).is_dir()]
99
+ if not seen or max(seen, key=lambda s: s[0])[1] == data_dir:
100
+ return None # no index yet (SessionStart gives it), or this profile's is the latest
101
+ return SWITCHED + session_start(payload, tool, data_dir)
102
+
103
+
104
+ def flush(payload, tool, final):
105
+ """Commit what the session staged, into each profile it staged in: a switch can leave some in the one
106
+ before, written while the session still had that profile's index."""
107
+ sid = _session_id(payload)
108
+ for data_dir in profiles.dirs():
109
+ sdir = capture.session_dir(data_dir, tool, sid)
110
+ if not sdir.is_dir() or not (data_dir / ".git").exists():
111
+ continue
112
+ if payload.get("transcript_path"):
113
+ capture.write_session(sdir, {"transcript_path": payload["transcript_path"]})
114
+ committed = capture.flush(data_dir, sdir, final=final, lock_timeout=1.0 if final else 5.0)
115
+ if committed and gitsync.has_remote(data_dir):
116
+ gitsync.spawn_sync(data_dir, push=True)
117
+
118
+
119
+ def _context(event, text):
120
+ return json.dumps({"hookSpecificOutput": {"hookEventName": event, "additionalContext": text}}, ensure_ascii=False)
121
+
122
+
123
+ def main(event, tool):
124
+ payload = _payload()
125
+ data_dir = config.data_dir() # the active profile's: running sessions follow it too
126
+ if not data_dir or not (data_dir / ".git").exists():
127
+ return 0
128
+ try:
129
+ if event == "session-start":
130
+ print(_context("SessionStart", session_start(payload, tool, data_dir)))
131
+ elif event == "prompt":
132
+ text = prompt(payload, tool, data_dir)
133
+ if text:
134
+ print(_context("UserPromptSubmit", text))
135
+ elif event in ("stop", "session-end"):
136
+ flush(payload, tool, final=event == "session-end")
137
+ else:
138
+ log(data_dir, f"unknown hook event {event!r}")
139
+ except Exception:
140
+ log_exc(data_dir, f"hook {event} ({tool})")
141
+ return 0
@@ -35,6 +35,7 @@ PATH_LINE = 'export PATH="$HOME/.local/bin:$PATH"'
35
35
 
36
36
  HOOK_EVENTS = [
37
37
  ("SessionStart", "session-start", 15),
38
+ ("UserPromptSubmit", "prompt", 10),
38
39
  ("Stop", "stop", 15),
39
40
  ("SessionEnd", "session-end", 3),
40
41
  ] # Codex clamps SessionEnd to 3s
@@ -44,7 +45,9 @@ HOOK_EVENTS = [
44
45
 
45
46
 
46
47
  def hook_command(event, tool):
47
- return " ".join(shlex.quote(a) for a in config.mem_command()) + f" hook {event} --tool {tool}"
48
+ command = " ".join(shlex.quote(a) for a in config.mem_command()) + f" hook {event} --tool {tool}"
49
+ # a mem from before this event exits 2 on it, and exit 2 from UserPromptSubmit blocks the prompt
50
+ return command + " || true" if event == "prompt" else command
48
51
 
49
52
 
50
53
  def _is_ours(group):
@@ -318,6 +321,7 @@ def _codex_plan(plan, data_dirs, state):
318
321
  handler = {"type": "command", "command": hook_command(name, "codex"), "timeout": timeout}
319
322
  if event == "SessionStart":
320
323
  handler["statusMessage"] = "Loading shared memory"
324
+ if event in ("SessionStart", "UserPromptSubmit"):
321
325
  handler["additionalContextLimit"] = 0 # keep the index inline, never spill to disk
322
326
  groups.append({"hooks": [handler]})
323
327
  hooks[event] = groups
@@ -173,6 +173,7 @@ class UiApp:
173
173
  "remoteUrl": re.sub(r"(://)[^/@]+@", r"\1", url) or None, # without any credentials in it
174
174
  "sync": self.sync_status(),
175
175
  "keywordStopwords": config.load().get("keyword_stopwords", []),
176
+ "switchNote": config.load().get("switch_note", True),
176
177
  "model": {
177
178
  "name": ui.MODEL["name"],
178
179
  "ready": ui.model_ready(),
@@ -185,7 +186,7 @@ class UiApp:
185
186
  }
186
187
 
187
188
  def set_settings(self, p):
188
- """Change keyword_stopwords, check_updates, or both."""
189
+ """Change keyword_stopwords, check_updates, switch_note, or several."""
189
190
  cfg = config.load()
190
191
  if "keyword_stopwords" in p:
191
192
  words = p["keyword_stopwords"]
@@ -196,6 +197,10 @@ class UiApp:
196
197
  if not isinstance(p["check_updates"], bool):
197
198
  raise ApiError(400, "check_updates must be true or false")
198
199
  cfg["check_updates"] = p["check_updates"]
200
+ if "switch_note" in p:
201
+ if not isinstance(p["switch_note"], bool):
202
+ raise ApiError(400, "switch_note must be true or false")
203
+ cfg["switch_note"] = p["switch_note"]
199
204
  config.save(cfg)
200
205
  if "keyword_stopwords" in p:
201
206
  self.refresh() # keywords are chosen when the state is built
@@ -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; }
@@ -445,135 +443,159 @@ table.nums td { padding: 4px 8px; border-top: 1px solid var(--line-soft); text-a
445
443
  table.nums th:first-child, table.nums td:first-child { padding-left: 0; text-align: left; color: var(--ink); }
446
444
  table.nums td:last-child { color: var(--ink); }
447
445
 
448
- .settings { display: grid; grid-template-columns: minmax(0, 1fr); max-width: 768px; gap: 24px; }
449
- @media (max-width: 639px) { .settings { gap: 16px; } }
450
- .group { padding: 16px 20px 4px; border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface); }
451
- @media (max-width: 639px) { .group { padding-inline: 16px; } }
452
- .group h2 { margin-bottom: 4px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
453
- .group-head { display: flex; align-items: center; justify-content: space-between; }
454
- .group-foot { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-top: 1px solid var(--line-soft); }
455
- .group-foot code { font-family: var(--mono); }
456
- .setting { display: grid; grid-template-columns: minmax(0, 1fr); align-items: center; gap: 12px; padding: 14px 0; border-top: 1px solid var(--line-soft); }
457
- .group h2 + .setting, .group .setting:first-of-type { border-top: 0; }
458
- @media (min-width: 640px) {
459
- .setting:not(.below) { grid-template-columns: minmax(220px, 1fr) minmax(0, auto); gap: 24px; }
460
- .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)); }
461
454
  }
462
- .setting-name { font-size: 14px; font-weight: 500; }
463
- .setting-help { margin-top: 2px; font-size: 12.5px; color: var(--ink-3); }
464
- .setting-control { min-width: 0; }
465
- .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); }
466
- .chip-code { padding: 4px 8px; border-radius: 6px; background: var(--chip); font-size: 12.5px; color: var(--ink-2); }
467
- .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); }
468
- .code-copy:hover { color: var(--ink); }
469
- .code-copy span { overflow-wrap: anywhere; }
470
- .code-copy .i { width: 14px; height: 14px; }
471
- .model-state { display: grid; justify-items: start; gap: 6px; }
472
- @media (min-width: 640px) { .model-state { justify-items: end; } }
473
- .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); }
474
- .tags-input:focus-within { border-color: var(--accent); }
475
- .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; }
476
- .tag button { display: grid; width: 20px; height: 20px; place-items: center; border-radius: 4px; color: var(--ink-3); }
477
- .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); }
478
542
  .tag .i { width: 12px; height: 12px; }
479
- .tags-input input { height: 24px; min-width: 96px; flex: 1; padding: 0 6px; font-size: 13px; outline: none; }
480
- .tags-input input::placeholder { color: var(--ink-3); }
481
- .checks li { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line-soft); font-size: 13.5px; }
482
- .checks li span { min-width: 0; overflow-wrap: anywhere; }
483
- .checks li:first-child { border-top: 0; }
484
- .checks .i { width: 16px; height: 16px; margin-top: 2px; }
485
-
486
- /* settings: the repository card */
487
- .repo-card { overflow: hidden; border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface); }
488
- .repo-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 14px; padding: 18px 20px; }
489
- .repo-card-head h2 { font-size: 16px; font-weight: 640; letter-spacing: -0.01em; }
490
- .repo-mark { display: grid; width: 40px; height: 40px; flex-shrink: 0; place-items: center; border-radius: 10px; background: var(--accent-soft); color: var(--accent); }
491
- .repo-mark .i { width: 20px; height: 20px; }
492
- .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; }
493
- .path-copy:hover { color: var(--ink); }
494
- .path-copy .i { width: 13px; height: 13px; opacity: 0; transition: opacity .15s; }
495
- .path-copy:hover .i, .path-copy:focus-visible .i { opacity: 1; }
496
- .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; }
497
- .pill .i { width: 14px; height: 14px; }
498
- .pill.ok { background: color-mix(in srgb, var(--ok) 13%, transparent); color: var(--ok); }
499
- .pill.warn { background: color-mix(in srgb, var(--warn) 13%, transparent); color: var(--warn); }
500
- .repo-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--line-soft); }
501
- .repo-facts > div { padding: 12px 20px; }
502
- .repo-facts > div + div { border-left: 1px solid var(--line-soft); }
503
- .repo-facts dt { font-size: 12px; color: var(--ink-3); }
504
- .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; }
505
- .remote-row { display: flex; align-items: flex-start; gap: 14px; padding: 16px 20px; border-top: 1px solid var(--line); background: var(--panel); }
506
- .remote-icon { display: grid; width: 32px; height: 32px; flex-shrink: 0; place-items: center; border-radius: 8px; background: var(--chip); color: var(--ink-3); }
507
- .remote-icon.on { background: var(--accent-soft); color: var(--accent); }
508
- .remote-icon .i { width: 17px; height: 17px; }
509
- .remote-title, .remote-label { display: block; font-size: 14px; font-weight: 500; }
510
- .remote-url { display: block; margin-top: 2px; font-size: 12.5px; color: var(--ink-2); overflow-wrap: anywhere; }
511
- .remote-help { margin-top: 3px; font-size: 12.5px; color: var(--ink-3); }
512
- .remote-form { display: flex; gap: 8px; margin-top: 8px; }
513
- .remote-form .input { height: 32px; padding: 0 10px; font-size: 12.5px; }
514
- .remote-note { margin-top: 8px; font-size: 12.5px; color: var(--ok); }
515
- .remote-note.bad { color: var(--bad); }
516
- .remote-row .inline-gap { gap: 4px; }
517
- .health-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; padding: 14px 0; }
518
- .health-icon { display: grid; width: 32px; height: 32px; flex-shrink: 0; place-items: center; border-radius: 50%; background: var(--chip); color: var(--ink-3); }
519
- .health-icon.ok { background: color-mix(in srgb, var(--ok) 13%, transparent); color: var(--ok); }
520
- .health-icon.warn { background: color-mix(in srgb, var(--warn) 13%, transparent); color: var(--warn); }
521
- .health-icon .i { width: 16px; height: 16px; }
522
- .health .checks { border-top: 1px solid var(--line-soft); }
523
- .health .setting-help code { font-family: var(--mono); }
524
- @media (max-width: 639px) {
525
- .repo-card-head, .remote-row { padding-inline: 16px; }
526
- .repo-card-head .grow { flex: 1 1 calc(100% - 56px); } /* name and path on their own line; status and Sync under them */
527
- .repo-facts > div { padding: 10px 16px; }
528
- .remote-row { flex-wrap: wrap; }
529
- .remote-form { flex-wrap: wrap; } .remote-form .input { flex-basis: 100%; }
530
- }
531
-
532
- /* settings: profiles. A profile's picture (avatar.js) is drawn from its name; CSS gives it a size. */
533
- .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; }
534
573
  .svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
535
- .pavatar.sm { width: 20px; height: 20px; }
536
- .pavatar.lg { width: 40px; height: 40px; }
537
- .repo-stack { display: grid; gap: 10px; min-width: 0; }
538
- .profiles { display: grid; gap: 8px; }
539
- .ptabs { display: flex; flex-wrap: wrap; gap: 6px; }
540
- .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; }
541
- .ptab:hover { background: var(--hover); color: var(--ink); }
542
- .ptab:disabled { opacity: .6; }
543
- .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); }
544
- .ptab-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
545
- .ptab-count { font-size: 12px; font-weight: 400; color: var(--ink-3); font-variant-numeric: tabular-nums; }
546
- .ptab.add { gap: 6px; padding: 0 12px; border-style: dashed; background: transparent; color: var(--ink-3); }
547
- .ptab.add .i { width: 15px; height: 15px; }
548
- .ptab.add:hover { color: var(--ink); }
549
- .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); }
550
- .profiles-help { font-size: 12.5px; color: var(--ink-3); }
551
- .profiles-help b { font-weight: 600; color: var(--ink-2); }
552
- .repo-title { display: flex; align-items: center; gap: 2px; }
553
- .repo-title .btn.icon { width: 26px; height: 26px; color: var(--ink-3); }
554
- .repo-title .btn.icon .i { width: 14px; height: 14px; }
555
- .pform { display: grid; gap: 16px; padding: 20px; }
556
- .pform-top { display: flex; align-items: center; gap: 14px; }
557
- .pform-top h2 { font-size: 16px; font-weight: 640; letter-spacing: -0.01em; }
558
- .pform-sub { margin-top: 2px; font-size: 13px; color: var(--ink-3); }
559
- .pfield { display: grid; }
560
- .pfield .field-label { justify-content: flex-start; gap: 4px; }
561
- .pfield .field-label .faint { font-weight: 400; }
562
- .pfield .input { height: 36px; }
563
- .field-help { margin-top: 6px; font-size: 12.5px; color: var(--ink-3); }
564
- .pform-error { font-size: 13px; color: var(--bad); }
565
- .pform-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
566
574
  .rail-profile { position: relative; display: grid; width: 40px; height: 40px; place-items: center; border-radius: 9px; transition: background-color .15s, box-shadow .15s; }
567
- .rail-profile:hover, .rail-profile:has(+ .menu:popover-open) { background: var(--hover); }
575
+ .rail-profile:hover { background: var(--hover); }
568
576
  .rail-profile[aria-current="page"] { background: var(--surface); box-shadow: 0 0 0 1px var(--line); }
569
- .rail-profile:has(+ .menu:popover-open)::after { display: none; } /* the menu says it already */
570
577
  .rail-profile .pavatar { width: 26px; height: 26px; }
571
- .menu-item.tall { height: 36px; }
572
- .menu-sep { height: 1px; margin: 4px -4px; background: var(--line); }
573
- .menu-note.pad { margin: 0; padding: 6px 8px 4px; }
574
- .tick-space { width: 14px; flex-shrink: 0; }
575
578
  @media (max-width: 639px) {
576
- .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); }
577
599
  .rail-profile[data-tip]::after { display: none; }
578
600
  }
579
601
 
@@ -595,6 +617,19 @@ table.nums td:last-child { color: var(--ink); }
595
617
  .dialog::backdrop { background: rgb(0 0 0 / .3); }
596
618
  .dialog-title { font-size: 15px; font-weight: 600; }
597
619
  .dialog-body { margin-top: 8px; font-size: 13.5px; white-space: pre-line; color: var(--ink-2); }
620
+ .sline { display: grid; grid-template-columns: auto auto; justify-content: start; margin-top: 14px; font-size: 12px; color: var(--ink-3); }
621
+ .sline > span { padding-right: 14px; }
622
+ .sline > .next:not(.sline-name) { padding-left: 14px; border-left: 1.5px dashed var(--ink-3); }
623
+ .sline > .sline-name.next { padding-left: 15.5px; }
624
+ .sline > .sline-when { padding-bottom: 4px; }
625
+ .sline-msgs { display: flex; align-items: center; gap: 5px; height: 22px; }
626
+ .sline-msgs i { width: 20px; height: 11px; border-radius: 6px; overflow: hidden; box-shadow: 0 0 0 0.5px var(--line); }
627
+ .sline-msgs i svg { display: block; width: 100%; height: 100%; }
628
+ .sline-msgs b { font-weight: 400; margin-left: 2px; }
629
+ .sline-name { display: flex; align-items: center; gap: 6px; margin-top: 6px; color: var(--ink-2); }
630
+ .sline-name .pavatar { width: 16px; height: 16px; }
631
+ .dialog-check { display: flex; align-items: center; gap: 8px; margin-top: 14px; padding: 0; border: 0; background: none; color: var(--ink-2); font: inherit; font-size: 13px; cursor: pointer; }
632
+ .dialog-check[aria-checked="true"] .check { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
598
633
  .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
599
634
  .dialog.wide { width: min(1040px, calc(100vw - 32px)); max-height: min(760px, calc(100vh - 48px)); padding: 0; }
600
635
  .dialog.wide[open] { display: flex; flex-direction: column; }