ccdesk 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/ayagmar/ccdesk/compare/v0.2.1...v0.2.2) (2026-10-12)
4
+
5
+ ### Bug Fixes
6
+
7
+ * **agents:** refuse agent text with values its review would hide ([3a9b39f](https://github.com/ayagmar/ccdesk/commit/3a9b39f2634cf4a10a9fc9a7ba7c627dddebd9da))
8
+ * **compare:** ignore key order when comparing settings ([db71cde](https://github.com/ayagmar/ccdesk/commit/db71cde8c19c4bae292e5f41cd8d8c0cf7c578e4))
9
+ * **context:** count a linked rule imported by its target's path once ([63830ef](https://github.com/ayagmar/ccdesk/commit/63830ef9096c2a86005cb60bad3d1c872921cdc1))
10
+ * **context:** count a rule in a linked folder once when it is also imported ([168039b](https://github.com/ayagmar/ccdesk/commit/168039b738524b1d72f5e48edfcb51bea069a506))
11
+ * **context:** prefer the real path the scan recorded when deduplicating ([d5a3e75](https://github.com/ayagmar/ccdesk/commit/d5a3e7566668156de9a3739b9fa51a39301cf697))
12
+ * **env:** make a relative CLAUDE_CONFIG_DIR absolute ([2939488](https://github.com/ayagmar/ccdesk/commit/2939488c5ded1869367ca769218f17be8880fcd4))
13
+ * **hooks:** refuse a replacement hook with hidden header values ([c40e7b4](https://github.com/ayagmar/ccdesk/commit/c40e7b4dfeda44fe104ad24cbd790a846da31266))
14
+ * **mcp:** name the chosen profile in the claude commands ccdesk shows ([4d99fbd](https://github.com/ayagmar/ccdesk/commit/4d99fbd37087fa0a6332bc997975efb510d123b5))
15
+ * **mcp:** run claude with the profile chosen by --config-dir ([02c6af0](https://github.com/ayagmar/ccdesk/commit/02c6af0bd56235874dd1a46dd1a3aaf124f299b8))
16
+ * **path-test:** apply startup's rules to a subfolder's linked rules ([49d426c](https://github.com/ayagmar/ccdesk/commit/49d426c07beb5ce8e6538c2091b5e3c989d356e1))
17
+ * **path-test:** keep AGENTS.md out when a subfolder's CLAUDE.md is skipped ([c05ed89](https://github.com/ayagmar/ccdesk/commit/c05ed89f04db3a83b1dad2e35b803cbb4af2e4a3))
18
+ * **path-test:** match a linked rule's target against excludes only as startup does ([dfb6a79](https://github.com/ayagmar/ccdesk/commit/dfb6a79ae70c7eb4c94f4905c76b78f081c8afce))
19
+ * **path-test:** skip subfolder instructions and rules over 4 MiB ([3696d74](https://github.com/ayagmar/ccdesk/commit/3696d74ec070dda64184b100b9eecef181e52884))
20
+ * **scan:** cite the docs section on MCP rules with parentheses ([901c372](https://github.com/ayagmar/ccdesk/commit/901c3729ed4089bdfd5dd19a6265a5995aae1458))
21
+ * **scan:** treat a name starting with two dots as inside its folder ([af6b025](https://github.com/ayagmar/ccdesk/commit/af6b02549e55a24394197da08326412bb1890059))
22
+ * **settings:** refuse hook events Claude Code does not know ([b6561e6](https://github.com/ayagmar/ccdesk/commit/b6561e6f9b5a86a9fa27b3138cb570f493edb0d5))
23
+ * **usage:** do not start a count for a page that already left ([4984a58](https://github.com/ayagmar/ccdesk/commit/4984a585d6a6ccd3914f2e06b9f274bd4b66eb21))
24
+ * **watch:** answer a wait whose watcher closed while it was starting ([5c63129](https://github.com/ayagmar/ccdesk/commit/5c63129bdb60d9bc5f6d13a7665e4f273dcfb406))
25
+ * **watch:** answer waiting pages when the watcher closes ([ad1c5ea](https://github.com/ayagmar/ccdesk/commit/ad1c5ea1080362858755e5f2c09e4bf19a3682e2))
26
+ * **watch:** refresh when a file inside an installed plugin changes ([a09a99e](https://github.com/ayagmar/ccdesk/commit/a09a99e38c37c765c5117138324d50174ad7b142))
27
+ * **watch:** refresh when a missing import is created ([279b033](https://github.com/ayagmar/ccdesk/commit/279b03353f414ed9faeaf0a86450c49da56aea1a))
28
+ * **watch:** refresh when a missing import's folder appears later ([91f6444](https://github.com/ayagmar/ccdesk/commit/91f644452c3975dd09b2f1fbb86b2c96cf5da561))
29
+ * **watch:** watch ~/.claude/plugins only for the install list ([382b6b0](https://github.com/ayagmar/ccdesk/commit/382b6b0990c82f71dab37cfd8840bd1aec42c47e))
30
+ * **watch:** watch only the plugin files the scan reads ([8d12adc](https://github.com/ayagmar/ccdesk/commit/8d12adcfa0cdf74097273516d04130a99cffe578))
31
+
32
+ ## [0.2.1](https://github.com/ayagmar/ccdesk/compare/v0.2.0...v0.2.1) (2026-10-12)
33
+
34
+ ### Features
35
+
36
+ * **ui:** add a text size control and raise the default sizes ([1a9fe82](https://github.com/ayagmar/ccdesk/commit/1a9fe82b35cc4743936b7bf3c7a6d4244c86779a)), references [#root](https://github.com/ayagmar/ccdesk/issues/root)
37
+ * **ui:** open the docs from the ccdesk logo ([834986a](https://github.com/ayagmar/ccdesk/commit/834986a40df7801e6b4cf12588fd69f65411458a))
38
+
39
+ ### Bug Fixes
40
+
41
+ * **ui:** keep the section links on one row with the larger text ([f349f19](https://github.com/ayagmar/ccdesk/commit/f349f19c5e81ba3c498ff9f0751f4e3d5d99ade1))
42
+
3
43
  ## [0.2.0](https://github.com/ayagmar/ccdesk/compare/v0.1.0...v0.2.0) (2026-10-12)
4
44
 
5
45
  ### Features
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  [![npm](https://img.shields.io/npm/v/ccdesk?style=flat-square&color=141414)](https://www.npmjs.com/package/ccdesk)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/ayagmar/ccdesk/ci.yml?branch=master&style=flat-square&label=ci)](https://github.com/ayagmar/ccdesk/actions/workflows/ci.yml)
10
10
  [![node](https://img.shields.io/node/v/ccdesk?style=flat-square&color=141414)](https://github.com/ayagmar/ccdesk/blob/master/package.json)
11
+ [![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue)](https://deepwiki.com/ayagmar/ccdesk)
11
12
  [![license](https://img.shields.io/github/license/ayagmar/ccdesk?style=flat-square&color=141414)](https://github.com/ayagmar/ccdesk/blob/master/LICENSE)
12
13
 
13
14
  See and change everything Claude Code loads on your machine: instruction files, settings, permissions, hooks, MCP servers, subagents, skills, plugins and auto memory.
@@ -16,7 +17,7 @@ See and change everything Claude Code loads on your machine: instruction files,
16
17
  npx ccdesk
17
18
  ```
18
19
 
19
- Each piece lives in its own file, at one of six scopes: managed, user, project, local, auto memory or a plugin. ccdesk opens in your browser on the directory you ran it from, lists everything that applies there in the order Claude Code loads it, and names the file behind each piece. When something is wrong, change it on the same page: you review the diff first, and every edit has a backup and an undo. It is not a session viewer or a usage dashboard.
20
+ Each piece lives in its own file, at one of six scopes: managed, user, project, local, auto memory or a plugin. ccdesk opens in your browser on the directory you ran it from, lists everything that applies there in the order Claude Code loads it, and names the file behind each piece. When something is wrong, change it on the same page: you review the diff first, and every file ccdesk writes is backed up and can be undone. It is not a session viewer or a usage dashboard.
20
21
 
21
22
  **Docs:** [ayagmar.github.io/ccdesk](https://ayagmar.github.io/ccdesk/)
22
23
 
@@ -74,7 +75,7 @@ Each piece lives in its own file, at one of six scopes: managed, user, project,
74
75
  - The old file is backed up to `~/.claude/backups/ccdesk/` first. Undo is one click, and **Recent changes** can restore any earlier edit.
75
76
  - A settings file that is a symlink, such as one in a dotfiles repository, is written through and stays a link.
76
77
  - A file with other hard links is not saved: replacing it would split it from its other names. Edit it in your editor.
77
- - ccdesk never writes `~/.claude.json`. User and local MCP server changes go through Claude Code's own `claude mcp` command, so they need `claude` on your PATH.
78
+ - ccdesk never writes `~/.claude.json`. User and local MCP server changes go through Claude Code's own `claude mcp` command, so they need `claude` on your PATH. ccdesk keeps no backup of these and cannot undo them; **Recent changes** lists each one with the commands that reverse it.
78
79
 
79
80
  Details: [Recent changes, backups and undo](https://ayagmar.github.io/ccdesk/recent-changes/)
80
81
 
@@ -104,7 +105,7 @@ npx ccdesk [options]
104
105
  -h, --help Show this help
105
106
  ```
106
107
 
107
- ccdesk reads `~/.claude` and `~/.claude.json`, or `$CLAUDE_CONFIG_DIR` when it is set, as Claude Code does. To look at another profile without exporting the variable, pass `--config-dir <path>`. Edits made in another editor or by Claude show up within a second. Requires Node.js 22.19 or newer. See [Platform notes](https://ayagmar.github.io/ccdesk/platforms/) for Windows, macOS and WSL.
108
+ ccdesk reads `~/.claude` and `~/.claude.json`, or `$CLAUDE_CONFIG_DIR` when it is set, as Claude Code does. To look at another profile without exporting the variable, pass `--config-dir <path>`. Edits made in another editor or by Claude show up within a second. Changes to `~/.claude.json`, such as trust decisions and MCP approvals, show up when you return to the tab. Requires Node.js 22.19 or newer. See [Platform notes](https://ayagmar.github.io/ccdesk/platforms/) for Windows, macOS and WSL.
108
109
 
109
110
  ## Privacy and safety
110
111
 
@@ -40,7 +40,12 @@ async function plan(env, edit) {
40
40
  // from it would lose them; it can still be deleted, as a whole.
41
41
  if (edit.text !== null && before !== raw)
42
42
  throw new RefusedError(`${path} has env, header or URL values ccdesk does not show; edit it in your editor instead.`);
43
- const errors = edit.text === null ? [] : agentProblems(edit.text);
43
+ let errors = edit.text === null ? [] : agentProblems(edit.text);
44
+ // Nor does it write values its review would hide.
45
+ if (edit.text !== null && errors.length === 0 && shownText(edit.text) !== edit.text)
46
+ errors = [
47
+ "ccdesk does not write env, header or URL values it would then hide; add them in your editor instead.",
48
+ ];
44
49
  return {
45
50
  path,
46
51
  raw,
@@ -44,10 +44,12 @@ function secretsIn(args) {
44
44
  }
45
45
  });
46
46
  }
47
- export const runClaude = async (args, cwd) => {
47
+ export const runClaude = async (args, cwd, configDir) => {
48
48
  const program = await claudeProgram();
49
+ // Set only for a chosen profile: Claude Code also moves .claude.json into a CLAUDE_CONFIG_DIR.
50
+ const env = configDir === undefined ? process.env : { ...process.env, CLAUDE_CONFIG_DIR: configDir };
49
51
  return new Promise((done, fail) => {
50
- execFile(program, args, { cwd, timeout: 30_000 }, (error, stdout, stderr) => {
52
+ execFile(program, args, { cwd, env, timeout: 30_000 }, (error, stdout, stderr) => {
51
53
  if (error?.code === "ENOENT")
52
54
  return fail(new RefusedError("ccdesk changes your user and local MCP servers through the claude command, which is not on PATH."));
53
55
  // Node's own message repeats the command, with the server's env and header values in it.
@@ -181,7 +183,9 @@ async function planClaudeJson(env, edit, run) {
181
183
  name,
182
184
  JSON.stringify(definition),
183
185
  ];
184
- const addCommand = (name, definition) => `claude mcp add-json --scope ${edit.scope} -- ${quoted(name)} ${quoted(JSON.stringify(definition))}`;
186
+ // The commands shown and recorded name the profile, so pasting one changes the same servers.
187
+ const claude = env.configDir === undefined ? "claude" : `CLAUDE_CONFIG_DIR=${quoted(env.configDir)} claude`;
188
+ const addCommand = (name, definition) => `${claude} mcp add-json --scope ${edit.scope} -- ${quoted(name)} ${quoted(JSON.stringify(definition))}`;
185
189
  const before = Object.keys(servers).length > 0 ? shown(servers) : null;
186
190
  // Recent changes lists the edit with the servers it touched, values hidden as the page shows
187
191
  // them, and the commands that put them back from the folder claude ran in.
@@ -194,7 +198,7 @@ async function planClaudeJson(env, edit, run) {
194
198
  shownBefore: shown(touched(servers)),
195
199
  shownAfter: shown(touched(edited(servers, edit))),
196
200
  undoCommands: [
197
- ...(add ? [`${at}claude mcp remove --scope ${edit.scope} -- ${quoted(add.name)}`] : []),
201
+ ...(add ? [`${at}${claude} mcp remove --scope ${edit.scope} -- ${quoted(add.name)}`] : []),
198
202
  ...(remove ? [`${at}${addCommand(remove.name, redactSecrets(servers[remove.name]))}`] : []),
199
203
  ],
200
204
  });
@@ -211,20 +215,20 @@ async function planClaudeJson(env, edit, run) {
211
215
  errors,
212
216
  // Shown with secrets hidden; the command that runs has the values you typed.
213
217
  commands: [
214
- ...(remove ? [`claude mcp remove --scope ${edit.scope} -- ${quoted(remove.name)}`] : []),
218
+ ...(remove ? [`${claude} mcp remove --scope ${edit.scope} -- ${quoted(remove.name)}`] : []),
215
219
  ...(add ? [addCommand(add.name, redactSecrets(add.definition))] : []),
216
220
  ],
217
221
  },
218
222
  save: async () => {
219
223
  if (remove)
220
- await run(["mcp", "remove", "--scope", edit.scope, "--", remove.name], cwd);
224
+ await run(["mcp", "remove", "--scope", edit.scope, "--", remove.name], cwd, env.configDir);
221
225
  // The change is made by then; a record that cannot be written only leaves it unlisted.
222
226
  if (!add) {
223
227
  await record().catch(() => undefined);
224
228
  return { target: env.claudeJson };
225
229
  }
226
230
  try {
227
- await run(addArgs(add.name, add.definition), cwd);
231
+ await run(addArgs(add.name, add.definition), cwd, env.configDir);
228
232
  }
229
233
  catch (error) {
230
234
  if (!remove)
@@ -234,7 +238,7 @@ async function planClaudeJson(env, edit, run) {
234
238
  // belongs to; a replaced server has no hidden values (see checked), so the command shows
235
239
  // nothing the page does not.
236
240
  const original = servers[remove.name];
237
- await run(addArgs(remove.name, original), cwd).catch(() => {
241
+ await run(addArgs(remove.name, original), cwd, env.configDir).catch(() => {
238
242
  throw new RefusedError(`ccdesk removed ${remove.name}, then could not add it back. In bash, zsh or Git Bash, put it back with: ${edit.scope === "local" ? `cd ${quoted(cwd)} && ` : ""}${addCommand(remove.name, original)} (${error instanceof Error ? error.message : String(error)})`);
239
243
  });
240
244
  throw error;
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import {} from "../../shared/compare.js";
3
4
  import {} from "./env.js";
4
5
  import { effectiveSettings, readSettingsLayers } from "./settings.js";
@@ -28,7 +29,8 @@ export async function compareSettings(env, a, b) {
28
29
  const settings = keys.map((key) => {
29
30
  const x = left.get(key) ?? null;
30
31
  const y = right.get(key) ?? null;
31
- return { key, a: x, b: y, same: JSON.stringify(x?.value) === JSON.stringify(y?.value) };
32
+ // The order keys are written in does not change a setting.
33
+ return { key, a: x, b: y, same: isDeepStrictEqual(x?.value, y?.value) };
32
34
  });
33
35
  return { a: resolve(a), b: resolve(b), settings };
34
36
  }
@@ -1,7 +1,7 @@
1
1
  import { basename, join } from "node:path";
2
2
  import {} from "../../shared/types.js";
3
3
  import {} from "./env.js";
4
- import { isWithin, parseFrontmatter, readTextFile } from "./files.js";
4
+ import { isWithin, parseFrontmatter, readTextFile, resolvedPath } from "./files.js";
5
5
  /**
6
6
  * Imports can name any file, so a cloned repo's CLAUDE.md could point at private files.
7
7
  * ccdesk shows an import's text only from the project, your Claude folders and ~/.agents.
@@ -71,8 +71,11 @@ export async function buildContext(input) {
71
71
  note: "ccdesk could not find this style's file",
72
72
  }, null));
73
73
  }
74
- // A file both imported and loaded on its own (a rule, an AGENTS.md) is counted once, as itself.
75
- const loaded = new Set(instructions.filter((i) => i.status === "loaded").map((i) => i.path));
74
+ // A file both imported and loaded on its own (a rule, an AGENTS.md) is counted once, as itself,
75
+ // also when it is loaded through a link, to it or to its folder, and imported by its real path.
76
+ const loaded = new Set((await Promise.all(instructions
77
+ .filter((i) => i.status === "loaded")
78
+ .map(async (i) => [i.path, i.realPath ?? (await resolvedPath(i.path))]))).flat());
76
79
  const added = new Set();
77
80
  // An imported file comes before the text that imports it ("Claude reads the imported file
78
81
  // first, then the rest"), and its own imports before it.
@@ -3,10 +3,13 @@ import { join, resolve } from "node:path";
3
3
  /** `configDirFlag`, from `--config-dir`, stands in for CLAUDE_CONFIG_DIR and wins over it. */
4
4
  export function scanEnvFromProcess(configDirFlag) {
5
5
  const home = homedir();
6
- const configDir = configDirFlag !== undefined ? resolve(configDirFlag) : process.env.CLAUDE_CONFIG_DIR;
6
+ // Made absolute here, from the folder ccdesk started in, because claude runs elsewhere.
7
+ const chosen = configDirFlag ?? (process.env.CLAUDE_CONFIG_DIR || undefined);
8
+ const configDir = chosen === undefined ? undefined : resolve(chosen);
7
9
  return {
8
10
  home,
9
11
  claudeDir: configDir ?? join(home, ".claude"),
12
+ configDir,
10
13
  // Claude Code keeps .claude.json beside the config dir it was given.
11
14
  claudeJson: configDir ? join(configDir, ".claude.json") : join(home, ".claude.json"),
12
15
  managedDir: managedDirFor(process.platform),
@@ -152,7 +152,9 @@ export function wildcard(pattern, flags = "") {
152
152
  }
153
153
  export function isWithin(parent, child) {
154
154
  const rel = relative(parent, child);
155
- return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel));
155
+ // Only the parent segment leads out; a name such as ..cache is inside.
156
+ const up = rel === ".." || rel.startsWith(`..${sep}`);
157
+ return rel === "" || (!up && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel));
156
158
  }
157
159
  export async function isDir(path) {
158
160
  try {
@@ -1,7 +1,7 @@
1
1
  import { dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from "node:path";
2
2
  import {} from "../../shared/types.js";
3
3
  import {} from "./env.js";
4
- import { isFile, isWithin, listDir, parseFrontmatter, readTextFile, stringsIn, toStringList, walkMarkdown, } from "./files.js";
4
+ import { isFile, isWithin, listDir, MAX_TEXT_BYTES, parseFrontmatter, readTextFile, resolvedPath, stringsIn, toStringList, walkMarkdown, } from "./files.js";
5
5
  import { scanDirectory } from "./index.js";
6
6
  import { applySkillOverrides, readSkill } from "./inventory.js";
7
7
  /** Globs are written with forward slashes on every platform. */
@@ -46,6 +46,8 @@ viewOf = (cwd) => scanDirectory(env, cwd)) {
46
46
  if (glob)
47
47
  matches.push({ kind: "rule", path: rule.path, scope: rule.scope, glob });
48
48
  }
49
+ const realCwd = await resolvedPath(cwd);
50
+ const approved = view.trust.externalImports === "approved";
49
51
  // Folders below the start directory load their own instructions, rules and skills on demand.
50
52
  const mode = view.agentsMdMode;
51
53
  const startSkills = new Set(view.inventory.skills.filter((s) => s.scope !== "plugin").map((s) => s.name));
@@ -53,11 +55,14 @@ viewOf = (cwd) => scanDirectory(env, cwd)) {
53
55
  for (let d = dirname(target); d !== cwd && isWithin(cwd, d); d = dirname(d))
54
56
  between.unshift(d);
55
57
  for (const folder of between) {
58
+ /** Whether the file is there; an excluded one or one over the limit is there but not loaded. */
56
59
  const nested = async (name, scope) => {
57
60
  const path = join(folder, name);
58
- if (excluded(path) || !(await readTextFile(env, path)))
61
+ const file = await readTextFile(env, path);
62
+ if (!file)
59
63
  return false;
60
- matches.push({ kind: "nested", path, scope });
64
+ if (!excluded(path) && file.bytes <= MAX_TEXT_BYTES)
65
+ matches.push({ kind: "nested", path, scope });
61
66
  return true;
62
67
  };
63
68
  // managed-only leaves out files at launch, but a subfolder's CLAUDE.md and rules still load
@@ -73,9 +78,19 @@ viewOf = (cwd) => scanDirectory(env, cwd)) {
73
78
  await nested("AGENTS.md", "project");
74
79
  for (const path of await walkMarkdown(join(folder, ".claude", "rules"))) {
75
80
  const text = await readTextFile(env, path);
76
- if (!text || excluded(path))
81
+ if (!text || text.bytes > MAX_TEXT_BYTES || excluded(path))
82
+ continue;
83
+ // As at startup: excluded by its target's path too, and a rule linked from outside the
84
+ // project waits for your approval, then loads only without paths
85
+ // (docs: memory#share-rules-across-projects-with-symlinks).
86
+ const real = await resolvedPath(path);
87
+ const outside = !isWithin(realCwd, real) && !isWithin(await resolvedPath(folder), real);
88
+ const linked = text.realPath ?? (outside ? real : undefined);
89
+ if (linked !== undefined && excluded(linked))
77
90
  continue;
78
91
  const globs = toStringList(parseFrontmatter(text.text).data.paths);
92
+ if (!isWithin(realCwd, real) && (!approved || globs.length > 0))
93
+ continue;
79
94
  const glob = globs.length === 0 ? undefined : firstMatch(globs, slashed(relative(folder, target)));
80
95
  if (globs.length === 0 || glob)
81
96
  matches.push({ kind: "nested", path, scope: "project", ...(glob ? { glob } : {}) });
@@ -17,7 +17,7 @@ export function trustWaits({ trust, inConfigHome, localFromRepository, profile,
17
17
  }
18
18
  /**
19
19
  * An MCP rule with a parameter, which Claude Code skips when it reads a settings file; such a rule
20
- * works only from --disallowedTools (docs: permissions#match-specific-tool-uses).
20
+ * works only from --disallowedTools (docs: permissions#match-by-input-parameter).
21
21
  */
22
22
  export function isSkippedMcpRule(rule) {
23
23
  return /^mcp__[^(]*\(/.test(rule);
@@ -36,6 +36,9 @@ export class CountStopped extends Error {
36
36
  export function readUsage(env, dir, rules, { signal, timeLimitMs = TIME_LIMIT_MS, maxCachedCalls = MAX_CACHED_CALLS, } = {}) {
37
37
  const key = JSON.stringify([env.claudeDir, resolve(dir), rules, timeLimitMs, maxCachedCalls]);
38
38
  let run = waiting.get(key);
39
+ // Nobody would wait for a new count, so none starts to hold up the queue.
40
+ if (!run && signal?.aborted)
41
+ return Promise.reject(stoppedBecause(signal));
39
42
  if (!run) {
40
43
  const stop = new AbortController();
41
44
  const promise = queue.then(() => {
@@ -1,5 +1,6 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import {} from "ajv";
3
+ import HOOK_EVENTS from "../shared/hook-events.json" with { type: "json" };
3
4
  import { allowedScopes, ignoredReason } from "../shared/scopes.js";
4
5
  import schema from "../shared/settings.schema.json" with { type: "json" };
5
6
  import {} from "../shared/types.js";
@@ -72,7 +73,9 @@ async function planKey(env, file, key, value, checksHiddenValues = false) {
72
73
  // reformatted instead of diffing its raw text.
73
74
  const reformats = raw !== null && raw.trim() !== "" && raw !== `${JSON.stringify(data, null, 2)}\n`;
74
75
  const base = { path, before, revision: revisionOf(raw), reformats };
75
- const errors = nextValue === undefined ? [] : schemaErrors(validate, nextValue, key);
76
+ const errors = nextValue === undefined
77
+ ? []
78
+ : [...schemaErrors(validate, nextValue, key), ...newHookEvents(key, data[key], nextValue)];
76
79
  if (errors.length > 0)
77
80
  return {
78
81
  path,
@@ -128,6 +131,18 @@ async function planPermissionEdit(env, edit) {
128
131
  return Object.keys(permissions).length > 0 ? permissions : undefined;
129
132
  });
130
133
  }
134
+ /**
135
+ * Hook events an edit brings in that Claude Code does not know, which the schema allows. Ones the
136
+ * file already has stay, so a file written for another Claude Code version can still be edited.
137
+ */
138
+ function newHookEvents(key, before, after) {
139
+ if (key !== "hooks" || !isRecord(after))
140
+ return [];
141
+ const had = isRecord(before) ? Object.keys(before) : [];
142
+ return Object.keys(after)
143
+ .filter((event) => !HOOK_EVENTS.includes(event) && !had.includes(event))
144
+ .map((event) => `${event} is not a hook event Claude Code knows.`);
145
+ }
131
146
  /** The matcher a group is written with; a missing or empty one matches everything. */
132
147
  function matcherOf(group) {
133
148
  return isRecord(group) && typeof group.matcher === "string" ? group.matcher : "";
@@ -144,6 +159,9 @@ async function planHookEdit(env, edit) {
144
159
  return Array.isArray(groups) ? groups : [];
145
160
  };
146
161
  const { remove, add } = edit;
162
+ // Checked before any branch, so neither a new hook nor a replacement writes them.
163
+ if (add && hasHiddenValues(add.definition))
164
+ throw new RefusedError("ccdesk does not write header values it would then hide; add this hook in the file instead.");
147
165
  if (remove) {
148
166
  const groups = groupsOf(remove.event);
149
167
  const group = groups[remove.at.group];
@@ -175,8 +193,6 @@ async function planHookEdit(env, edit) {
175
193
  delete hooks[remove.event];
176
194
  }
177
195
  if (add) {
178
- if (hasHiddenValues(add.definition))
179
- throw new RefusedError("ccdesk does not write header values it would then hide; add this hook in the file instead.");
180
196
  const groups = groupsOf(add.event);
181
197
  const matcher = add.matcher ?? "";
182
198
  const group = groups.find((g) => matcherOf(g) === matcher && isRecord(g) && Array.isArray(g.hooks));
@@ -1,11 +1,12 @@
1
1
  import { watch } from "node:fs";
2
2
  import { stat } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
3
+ import { basename, dirname, isAbsolute, join } from "node:path";
4
4
  import { storeDirName } from "../shared/memory.js";
5
5
  import {} from "../shared/types.js";
6
6
  import {} from "./scan/env.js";
7
- import { isNetworkPath, mainWorktreeRoot } from "./scan/files.js";
7
+ import { isDir, isFile, isNetworkPath, mainWorktreeRoot } from "./scan/files.js";
8
8
  import { scanDirectory } from "./scan/index.js";
9
+ import { manifestCommandMap, manifestPaths } from "./scan/plugins.js";
9
10
  /** How long one request waits for a change before answering "nothing changed". */
10
11
  const WAIT_MS = 25_000;
11
12
  /** Editors write a file in several steps; one change covers them all. */
@@ -31,7 +32,8 @@ async function targets(env, dir, extra) {
31
32
  const memoryKey = (await mainWorktreeRoot(dir)) ?? dir;
32
33
  const out = [
33
34
  ...claude(env.claudeDir),
34
- { path: join(env.claudeDir, "plugins"), recursive: false },
35
+ // Only the install list: Windows reports a write deep in a plugin as a change to `cache`.
36
+ { path: join(env.claudeDir, "plugins"), recursive: false, names: ["installed_plugins.json"] },
35
37
  { path: join(env.claudeDir, "projects", storeDirName(memoryKey), "memory"), recursive: false },
36
38
  { path: env.managedDir, recursive: false },
37
39
  { path: join(env.managedDir, "managed-settings.d"), recursive: false },
@@ -53,11 +55,15 @@ async function targets(env, dir, extra) {
53
55
  out.push(...claude(join(memoryKey, ".claude")));
54
56
  return out.filter((t) => !isNetworkPath(t.path));
55
57
  }
56
- /** Files a scan read that the fixed list does not cover: a custom memory folder, imports. */
57
- export function dependencies(view) {
58
+ /**
59
+ * Files a scan read that the fixed list does not cover: a custom memory folder, imports, installed
60
+ * plugins. A missing import is watched too, so creating it shows up, even when its folder is missing
61
+ * as well.
62
+ */
63
+ export async function dependencies(env, view) {
58
64
  const files = view.instructions.flatMap((i) => [
59
65
  i.realPath ?? i.path,
60
- ...i.imports.filter((r) => r.exists).map((r) => r.resolved),
66
+ ...i.imports.map((r) => r.resolved),
61
67
  ]);
62
68
  const byFolder = new Map();
63
69
  for (const file of files) {
@@ -65,10 +71,69 @@ export function dependencies(view) {
65
71
  names.add(basename(file));
66
72
  byFolder.set(dirname(file), names);
67
73
  }
74
+ const plugins = await Promise.all(view.inventory.plugins.map((p) => pluginTargets(env, view, p)));
68
75
  return [
69
76
  ...[...byFolder].map(([path, names]) => ({ path, recursive: false, names: [...names] })),
70
77
  ...(view.memory ? [{ path: view.memory.dir, recursive: false }] : []),
78
+ ...plugins.flat(),
79
+ ];
80
+ }
81
+ /** A plugin's folders the scan reads from, when they are not named in its manifest. */
82
+ const PLUGIN_FOLDERS = ["agents", "skills", "commands", "output-styles", "hooks"];
83
+ /** Manifest keys that name files or folders inside the plugin. */
84
+ const PLUGIN_KEYS = ["agents", "skills", "commands", "outputStyles", "hooks", "mcpServers"];
85
+ /**
86
+ * What the scan reads in a plugin: its manifest, .mcp.json, a skill at its root, its component
87
+ * folders and the paths its manifest names. Not the whole plugin, which can hold an MCP server's
88
+ * node_modules and files the server writes while it runs.
89
+ */
90
+ async function pluginTargets(env, view, plugin) {
91
+ const root = plugin.installPath;
92
+ // A path from installed_plugins.json that is not absolute names no folder Claude Code would load.
93
+ if (!isAbsolute(root))
94
+ return [];
95
+ const named = await Promise.all(PLUGIN_KEYS.map((key) => manifestPaths(env, plugin, key)));
96
+ const commands = await manifestCommandMap(env, plugin);
97
+ // Without a skills folder, each folder at the root holding SKILL.md is a skill.
98
+ const rootSkills = view.inventory.skills
99
+ .filter((s) => s.plugin === plugin.id && s.folder && dirname(s.folder) === root)
100
+ .map((s) => s.folder);
101
+ const paths = [
102
+ ...PLUGIN_FOLDERS.map((name) => join(root, name)),
103
+ ...named.flatMap((list) => list ?? []),
104
+ ...commands.map((c) => c.path),
105
+ ...rootSkills,
106
+ ];
107
+ const out = [
108
+ {
109
+ path: root,
110
+ recursive: false,
111
+ names: [".claude-plugin", ".mcp.json", "SKILL.md", ...PLUGIN_FOLDERS],
112
+ },
113
+ { path: join(root, ".claude-plugin"), recursive: false, names: ["plugin.json"] },
71
114
  ];
115
+ for (const path of new Set(paths)) {
116
+ if (await isDir(path))
117
+ out.push({ path, recursive: true });
118
+ else if (await isFile(path))
119
+ out.push({ path: dirname(path), recursive: false, names: [basename(path)] });
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * `target`, or when it does not exist yet, the nearest folder above it that does, watched for the
125
+ * next missing name only. Watching needs a folder that exists, and the watchers are rebuilt after
126
+ * every change, so each folder that appears moves the watch one level closer.
127
+ */
128
+ async function nearestWatchable(target) {
129
+ if (isNetworkPath(target.path) || (await stat(target.path).catch(() => null)))
130
+ return target;
131
+ for (let missing = target.path; dirname(missing) !== missing; missing = dirname(missing)) {
132
+ const parent = dirname(missing);
133
+ if (await stat(parent).catch(() => null))
134
+ return { path: parent, recursive: false, names: [basename(missing)] };
135
+ }
136
+ return target;
72
137
  }
73
138
  /** One set of file watchers per directory a page is looking at, shared by its requests. */
74
139
  export class Watcher {
@@ -91,7 +156,7 @@ export class Watcher {
91
156
  // Only a watched directory's view can be known to be current later.
92
157
  if (subscription)
93
158
  this.latest.set(view.path, { view, startedAt, stateFile });
94
- const extra = dependencies(view);
159
+ const extra = await dependencies(this.env, view);
95
160
  this.scanned.set(view.path, extra);
96
161
  if (subscription && JSON.stringify(extra) !== JSON.stringify(subscription.extra)) {
97
162
  subscription.extra = extra;
@@ -128,8 +193,10 @@ export class Watcher {
128
193
  async wait(dir, since, signal, timeoutMs = WAIT_MS) {
129
194
  const subscription = await this.subscribe(dir);
130
195
  clearTimeout(subscription.idle);
196
+ // Closed while its watchers were starting: there is nothing left to wait on.
197
+ const closed = this.subscriptions.get(dir) !== subscription;
131
198
  try {
132
- if (subscription.changedAt <= since && !signal.aborted)
199
+ if (!closed && subscription.changedAt <= since && !signal.aborted)
133
200
  await new Promise((resolve) => {
134
201
  const done = () => {
135
202
  clearTimeout(timer);
@@ -143,7 +210,8 @@ export class Watcher {
143
210
  });
144
211
  }
145
212
  finally {
146
- if (subscription.waiters.size === 0)
213
+ // A dropped subscription has nothing left to clean up later.
214
+ if (subscription.waiters.size === 0 && this.subscriptions.get(dir) === subscription)
147
215
  subscription.idle = setTimeout(() => this.drop(dir), IDLE_MS);
148
216
  }
149
217
  return { changed: subscription.changedAt > since, at: subscription.changedAt };
@@ -178,10 +246,14 @@ export class Watcher {
178
246
  w.close();
179
247
  this.subscriptions.delete(dir);
180
248
  this.latest.delete(dir);
249
+ // Pages still waiting are answered now, so no timer outlives the watcher.
250
+ for (const wake of [...subscription.waiters])
251
+ wake();
181
252
  }
182
253
  /** (Re)creates the watchers, so a folder created since, such as a new .claude, is watched. */
183
254
  async rewatch(dir, subscription) {
184
- const list = await targets(this.env, dir, subscription.extra);
255
+ const extra = await Promise.all(subscription.extra.map(nearestWatchable));
256
+ const list = await targets(this.env, dir, extra);
185
257
  for (const w of subscription.watchers)
186
258
  w.close();
187
259
  subscription.watchers = [];
@@ -0,0 +1 @@
1
+ @font-face{font-family:Hanken Grotesk Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper:#fbfbf7;--panel:#fff;--sunk:#f1f0ea;--ink:#141414;--ink-2:#4a4a45;--muted:#6b6a63;--rule:#dcdad1;--rule-strong:#141414;--sel:#fff3c4;--sel-ink:#141414;--hover:#f4f3ec;--focus:#0072b2;--ok:#1d6b47;--warn:#8a5a00;--warn-bg:#fff1d6;--bad:#a3361c;--bad-bg:#fde7e0;--on-bad:#fff;--line-managed:#b84f00;--line-user:#0072b2;--line-project:#007a5a;--line-local:#e69f00;--line-memory:#cc79a7;--line-plugin:#5f5f5a;--on-managed:#fff;--on-user:#fff;--on-project:#fff;--on-local:#141414;--on-memory:#141414;--on-plugin:#fff;--sans:"Hanken Grotesk Variable", "Helvetica Neue", Helvetica, "Nimbus Sans", "Liberation Sans", Arial, sans-serif;--mono:"JetBrains Mono Variable", ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--row:32px;--head:92px;--rail:248px;--inspector:clamp(440px, 30vw, 600px);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#131312;--panel:#1a1a18;--sunk:#222220;--ink:#ece8de;--ink-2:#c7c2b6;--muted:#938f84;--rule:#2c2b28;--rule-strong:#ece8de;--sel:#33302a;--sel-ink:#fff6e0;--hover:#1d1d1b;--focus:#f2c14e;--ok:#6fcf97;--warn:#f2c14e;--warn-bg:#2b2512;--bad:#ff8a73;--bad-bg:#371b15;--on-bad:#131312;--line-managed:#ff7a3d;--line-user:#4aa8e8;--line-project:#2fbf8f;--line-local:#f2b233;--line-memory:#e391bd;--line-plugin:#8d95a5;--on-managed:#131312;--on-user:#131312;--on-project:#131312;--on-plugin:#131312;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}:root[data-theme=dark]{--paper:#131312;--panel:#1a1a18;--sunk:#222220;--ink:#ece8de;--ink-2:#c7c2b6;--muted:#938f84;--rule:#2c2b28;--rule-strong:#ece8de;--sel:#33302a;--sel-ink:#fff6e0;--hover:#1d1d1b;--focus:#f2c14e;--ok:#6fcf97;--warn:#f2c14e;--warn-bg:#2b2512;--bad:#ff8a73;--bad-bg:#371b15;--on-bad:#131312;--line-managed:#ff7a3d;--line-user:#4aa8e8;--line-project:#2fbf8f;--line-local:#f2b233;--line-memory:#e391bd;--line-plugin:#8d95a5;--on-managed:#131312;--on-user:#131312;--on-project:#131312;--on-plugin:#131312;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--paper);color:var(--ink);font:15px / 1.45 var(--sans);font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;margin:0}::selection{background:var(--sel);color:var(--sel-ink)}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}*{scrollbar-width:thin;scrollbar-color:var(--rule) transparent}button,input{font:inherit;color:inherit}button{cursor:pointer}.mono,code,pre,kbd{font-family:var(--mono);font-size:13.5px}kbd{border:1px solid var(--rule);color:var(--muted);background:var(--panel);border-bottom-width:2px;border-radius:4px;padding:0 5px;font-size:12px}.muted{color:var(--muted)}.small{font-size:13.5px}.icon{flex-shrink:0;display:block}.app{grid-template-columns:var(--rail) minmax(0, 1fr);height:100%;display:grid;overflow:hidden}.main{flex-direction:column;min-width:0;display:flex;overflow:hidden}.workspace{grid-template-columns:minmax(0, 1fr) var(--inspector);height:100%;min-height:0;display:grid}.workspace.no-details{grid-template-columns:minmax(0,1fr)}.work-main{min-width:0;padding:0 28px 64px;overflow-y:auto;container-type:inline-size}.rail{background:var(--panel);border-right:1px solid var(--rule);flex-direction:column;gap:2px;padding:14px 10px 32px;display:flex;overflow-y:auto}.brand{letter-spacing:-.01em;align-items:center;gap:8px;padding:2px 6px 14px;font-size:16px;font-weight:700;display:flex}.brand-link{color:inherit;align-items:center;gap:8px;text-decoration:none;display:inline-flex}.brand-link:hover span{text-underline-offset:3px;text-decoration:underline}.brand .text-size{margin-left:auto}.brand .text-size button{justify-content:center;width:24px;padding:0}.brand .text-size button.smaller{font-size:11px}.brand .text-size button.larger{font-size:15px}html[data-text-size=smaller] #root{zoom:.9}html[data-text-size=larger] #root{zoom:1.15}.rail-group{color:var(--muted);justify-content:space-between;align-items:center;margin:14px 6px 4px;font-size:13px;font-weight:600;display:flex}.rail-group.toggle{text-align:left;background:0 0;border:0;padding:0}.rail-group.toggle:hover{color:var(--ink)}.rail-list{margin:0;padding:0;list-style:none}.rail-item{text-align:left;background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;width:100%;height:30px;padding:0 8px;font-size:14.5px;display:flex}.rail-item:hover{background:var(--hover)}.rail-item.current{background:var(--sel);color:var(--sel-ink);font-weight:600}.rail-item.gone .rail-label{text-decoration:line-through;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted)}.rail-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.rail-count{font-family:var(--mono);color:var(--muted);font-size:12.5px}.rail-item.current .rail-count{color:inherit;opacity:.75}.rail-filter,.filter,.palette-input{border:1px solid var(--rule);background:var(--paper);height:30px;color:var(--muted);border-radius:6px;align-items:center;gap:6px;padding:0 8px;display:flex}.rail-filter{margin:12px 0 0}.rail-filter input,.filter input,.palette-input input{min-width:0;color:var(--ink);background:0 0;border:0;outline:0;flex:1}.rail-filter:focus-within,.filter:focus-within{border-color:var(--focus)}input::placeholder{color:var(--muted)}.dir-head{z-index:3;background:var(--paper);height:var(--head);grid-template-rows:32px 28px;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px 16px;padding:16px 0 8px;display:grid;position:sticky;top:0}.dir-title{align-items:center;gap:10px;min-width:0;display:flex}.dir-title h1{letter-spacing:-.01em;white-space:nowrap;text-overflow:ellipsis;min-width:0;margin:0;font-size:19px;font-weight:700;overflow:hidden}.dir-title .flag,.session-line{color:var(--muted);align-items:center;gap:8px;margin:4px 0 12px;font-size:14px;display:flex}.section-index{scrollbar-width:none;grid-column:1/-1;gap:2px;display:flex;overflow-x:auto}.section-index a{height:26px;color:var(--ink-2);white-space:nowrap;border-radius:6px;align-items:center;gap:6px;padding:0 6px;font-size:14px;text-decoration:none;display:inline-flex}.section-index a:hover{background:var(--hover);color:var(--ink)}.section-index a.empty{color:var(--muted)}.section-index kbd{padding:0 4px}@container (width<=1100px){.section-index kbd{display:none}}@container (width<=840px){.section-index .seg-count{display:none}}@container (width<=680px){.section-index a{padding:0 3px;font-size:12.5px}}.seg-count{font-family:var(--mono);color:var(--muted);font-size:12px}[aria-pressed=true] .seg-count{color:inherit;opacity:.75}.tt-section{scroll-margin-top:var(--head)}.via-agents{vertical-align:middle;font-family:var(--mono);margin-left:8px}.vis-personal{color:var(--muted)}.toolbar{align-items:center;gap:8px;display:flex}.filter{width:240px}@container (width<=960px){.filter{width:140px}.palette-label{display:none}}.palette-button{border:1px solid var(--rule);background:var(--panel);border-radius:6px;align-items:center;gap:8px;height:30px;padding:0 10px;font-size:14px;display:flex}.palette-button:hover{border-color:var(--ink-2)}.route{border-top:2px solid var(--rule-strong);border-bottom:1px solid var(--rule);margin:4px 0 8px}.route-stops{margin:0;padding:0;list-style:none;display:flex;position:relative}.stop{flex:2 1 0;min-width:0}.stop.idle{flex:1 1 0}.route-stops:before{content:"";background:var(--ink);height:3px;position:absolute;top:25px;left:22px;right:22px}.stop button{text-align:left;background:0 0;border:0;border-radius:6px;grid-template-rows:22px auto auto;justify-items:start;gap:4px;width:100%;padding:14px 8px 12px;display:grid;position:relative}.stop button:hover{background:var(--hover)}.stop button.on{background:var(--sunk)}.stop-node{background:var(--paper);border-radius:999px;padding:0 6px 0 0;display:flex}.stop button:hover .stop-node,.stop button.on .stop-node{background:0 0}.stop-name{font-size:14.5px;font-weight:700}.stop-facts{font-family:var(--mono);color:var(--muted);font-size:12.5px}.stop.idle .stop-name{color:var(--muted);font-weight:500}.stop.lit .bullet{box-shadow:0 0 0 2px var(--paper), 0 0 0 4px var(--ink)}.stop button.on .stop-name{text-underline-offset:3px;text-decoration:underline;text-decoration-thickness:2px}.bullet{width:22px;height:22px;font:700 11.5px / 1 var(--sans);background:var(--bullet);color:var(--bullet-ink);border:2px solid var(--bullet);border-radius:999px;flex-shrink:0;place-items:center;transition:box-shadow .16s cubic-bezier(.16,1,.3,1);display:inline-grid}.bullet.hollow{background:var(--paper);color:var(--bullet)}.bullet.hollow.bullet-local,.bullet.hollow.bullet-memory{color:var(--ink)}.bullet-managed{--bullet:var(--line-managed);--bullet-ink:var(--on-managed)}.bullet-user{--bullet:var(--line-user);--bullet-ink:var(--on-user)}.bullet-project{--bullet:var(--line-project);--bullet-ink:var(--on-project)}.bullet-local{--bullet:var(--line-local);--bullet-ink:var(--on-local)}.bullet-memory{--bullet:var(--line-memory);--bullet-ink:var(--on-memory)}.bullet-plugin{--bullet:var(--line-plugin);--bullet-ink:var(--on-plugin)}.tt .bullet{vertical-align:middle}.tt .bullet,.palette .bullet,.layers .bullet{width:20px;height:20px;font-size:10.5px}.tt-section{margin-top:26px}.tt-head{border-top:2px solid var(--rule-strong);flex-wrap:wrap;align-items:center;gap:10px;padding:8px 0 6px;display:flex}.tt-head h2{letter-spacing:-.005em;margin:0;font-size:15px;font-weight:700}.tt-count{font-family:var(--mono);color:var(--muted);font-size:13px}.tt-empty{color:var(--muted);margin:4px 0 0;padding:6px 0}.tt{border-collapse:collapse;table-layout:fixed;width:100%}.tt th{top:var(--head);z-index:1;background:var(--paper);text-align:left;color:var(--muted);border-bottom:1px solid var(--rule-strong);white-space:nowrap;height:26px;box-shadow:0 -16px 0 var(--paper);padding:0 8px;font-size:13px;font-weight:600;position:sticky}.tt td{height:var(--row);border-bottom:1px solid var(--rule);white-space:nowrap;text-overflow:ellipsis;vertical-align:middle;padding:0 8px;overflow:hidden}.tt tbody tr{cursor:pointer}.tt tbody tr:hover td{background:var(--hover)}.tt tbody tr.selected td{background:var(--sel);color:var(--sel-ink)}.tt tbody tr.selected .row-name{font-weight:700}.tt tbody tr.dim td{color:var(--muted)}.tt td.num{text-align:right;font-family:var(--mono);color:var(--muted);font-size:13px}.tt td.num.used{color:var(--ink)}.tt th:first-child,.tt td:first-child{padding-left:2px}.row-name{font:inherit;text-overflow:ellipsis;white-space:nowrap;text-align:left;vertical-align:middle;background:0 0;border:0;max-width:100%;padding:0;overflow:hidden}.via{vertical-align:middle;color:var(--muted);margin-left:6px;display:inline-flex}.value,.why,.desc{color:var(--ink-2)}.tt-instructions th:first-child,.tt-instructions th:nth-child(2){width:40px}.tt-instructions th:nth-child(3){width:32%}.tt-instructions th:nth-child(4){width:86px}.tt-instructions th:nth-child(6){text-align:right;width:58px}.tt-instructions th:nth-child(7){width:108px}.tt-settings th:first-child{width:76px}.tt-settings th:nth-child(2),.tt-settings th:nth-child(4){width:30%}.tt-memory th:first-child{width:40px}.tt-memory th:nth-child(2){width:24%}.tt-memory th:nth-child(3){width:98px}.tt-memory th:nth-child(5){width:200px}.tt-memory th:nth-child(6){text-align:right;width:82px}.tt-hooks th:first-child,.tt-permissions th:first-child,.tt-mcp th:first-child,.tt-extensions th:first-child{width:40px}.tt-hooks th:nth-child(2){width:140px}.tt-hooks th:nth-child(3){width:26%}.tt-permissions th:nth-child(2){width:120px}.tt-permissions th:nth-child(4){width:34%}.tt-mcp th:nth-child(2){width:26%}.tt-mcp th:nth-child(3){width:92px}.tt-extensions th:nth-child(2){width:104px}.tt-extensions th:nth-child(3){width:30%}.tt-tools th:first-child{width:40px}.tt-tools th:nth-child(2){width:190px}.tt-tools th:nth-child(3){width:150px}.tt-tools th:nth-child(4){width:190px}.tt-tools th:nth-child(5){text-align:right;width:64px}.tt-dirs th:nth-child(2),.tt-dirs th:nth-child(3){text-align:right;width:96px}.tt-dirs th:nth-child(4){text-align:right;width:110px}.tt-shared th:first-child{width:40px}.tt-shared th:nth-child(2){width:30%}.tt-shared th:nth-child(3){width:130px}.bullets>*+*{margin-left:3px}.beat{color:var(--muted)}.beaten{font-family:var(--mono);align-items:center;gap:4px;margin-right:10px;font-size:13px;display:inline-flex}.beaten s{text-decoration-thickness:1.5px}.status{font-size:13.5px;font-weight:600}.status-loaded{color:var(--ok)}.status-skipped{color:var(--muted)}.status-on-match{color:var(--ink-2)}.vis{font-size:13.5px}.vis-tracked{color:var(--ok);font-weight:600}.vis-untracked{color:var(--warn)}.vis-ignored,.vis-no-git{color:var(--muted)}.type{align-items:center;gap:6px;font-size:14px;display:inline-flex}.type-dot{background:var(--ink-2);flex-shrink:0;width:8px;height:8px}.type-user{border-radius:999px}.type-feedback{clip-path:polygon(50% 0,100% 100%,0 100%)}.type-reference{transform:rotate(45deg)scale(.85)}.type-unknown{box-shadow:inset 0 0 0 1.5px var(--muted);background:0 0}.checks>*+*{margin-left:4px}.flag{background:var(--sunk);color:var(--ink-2);white-space:nowrap;border-radius:4px;align-items:center;gap:4px;padding:0 6px;font-size:12.5px;line-height:18px;display:inline-flex}.flag .icon{width:13px;height:13px}.flag.warn{background:var(--warn-bg);color:var(--warn)}.flag.bad{background:var(--bad-bg);color:var(--bad)}.segmented{border:1px solid var(--rule);scrollbar-width:none;border-radius:6px;min-width:0;max-width:100%;margin:0 0 0 6px;padding:0;display:inline-flex;overflow-x:auto}.segmented button{white-space:nowrap;border:0;border-right:1px solid var(--rule);background:var(--panel);flex:none;align-items:center;gap:5px;height:24px;padding:0 9px;font-size:13.5px;display:inline-flex}.segmented button:last-child{border-right:0}.segmented button[aria-pressed=true]{background:var(--ink);color:var(--paper)}.segmented.small button{height:22px;font-size:13px}.inspector{border-left:1px solid var(--rule);background:var(--panel);min-width:0;overflow-y:auto}.insp-head{z-index:2;background:var(--panel);border-bottom:2px solid var(--rule-strong);align-items:center;gap:10px;padding:16px 18px 12px;display:flex;position:sticky;top:0}.insp-head h2{overflow-wrap:anywhere;flex:1;min-width:0;margin:0;font-size:16px;font-weight:700}.insp-body{padding:14px 18px 40px}.verdict{color:var(--ink-2);margin:0 0 14px}.verdict.ok{color:var(--ink)}.verdict.off{color:var(--muted)}.fields{grid-template-columns:104px minmax(0,1fr);gap:6px 12px;margin:0 0 16px;font-size:14px;display:grid}.fields dt{color:var(--muted)}.fields dd{min-width:0;margin:0}.path{flex-wrap:wrap;align-items:center;gap:2px 6px;max-width:100%;display:inline-flex}.path .mono{overflow-wrap:anywhere}.copied{color:var(--ok);flex-basis:100%;font-size:13px}.wrap{overflow-wrap:anywhere;white-space:normal}.icon-button{width:28px;height:28px;color:var(--muted);background:0 0;border:0;border-radius:6px;flex-shrink:0;place-items:center;display:inline-grid}.icon-button:hover{background:var(--hover);color:var(--ink)}.icon-button.small{width:22px;height:22px}.sub{margin:0 0 16px}.sub h3,.sub-title{margin:0 0 6px;font-size:14px;font-weight:700}.plain{gap:4px;margin:0;padding:0;list-style:none;display:grid}.plain li{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.layers{border-collapse:collapse;width:100%;margin-bottom:14px}.layers th{text-align:left;width:40px;padding:4px 0}.layers td{border-bottom:1px solid var(--rule);overflow-wrap:anywhere;padding:4px 8px}.layers tr.wins td{font-weight:600}.layers tr.lost td{color:var(--muted);text-decoration:line-through}.layers tr.unset td{color:var(--muted)}.contents{border-top:1px solid var(--rule-strong);margin-top:8px}.contents-bar{justify-content:space-between;align-items:center;padding:8px 0;font-size:14px;font-weight:700;display:flex}pre.raw{background:var(--sunk);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:6px;margin:0 0 12px;padding:12px;line-height:1.5;overflow-x:auto}.prose{overflow-wrap:anywhere;max-width:68ch;font-size:14.5px;line-height:1.6}.prose h1,.prose h2,.prose h3{margin:18px 0 6px;font-size:14px}.prose p,.prose ul,.prose ol{margin:8px 0}.prose ul,.prose ol{padding-left:20px}.prose code{background:var(--sunk);border-radius:3px;padding:1px 4px}.prose pre{background:var(--sunk);border-radius:6px;padding:10px;overflow-x:auto}.prose pre code{background:0 0;padding:0}.prose a,.link{color:var(--focus);text-underline-offset:2px}.prose table{border-collapse:collapse;display:block;overflow-x:auto}.prose td,.prose th{border:1px solid var(--rule);padding:3px 6px}.link{text-align:left;background:0 0;border:0;padding:0;text-decoration:underline}.actions{margin:4px 0 16px}.quiet-danger{border:1px solid var(--rule);background:var(--panel);height:30px;color:var(--bad);border-radius:6px;align-items:center;gap:6px;padding:0 10px;font-size:14px;display:inline-flex}.quiet-danger:hover{border-color:var(--bad)}.confirm{border:1px solid var(--bad);background:var(--bad-bg);border-radius:8px;padding:12px}.confirm p{margin:0 0 10px;font-size:14px}.confirm-buttons{gap:8px;display:flex}.confirm-buttons button{border:1px solid var(--rule);background:var(--panel);border-radius:6px;height:30px;padding:0 12px;font-size:14px}.confirm-buttons button.danger{background:var(--bad);border-color:var(--bad);color:var(--on-bad);font-weight:600}button:disabled{opacity:.6;cursor:progress}.page{height:100%;padding:22px 28px 64px;overflow-y:auto}.page-head h1{letter-spacing:-.015em;margin:0 0 4px;font-size:22px}.lede{color:var(--ink-2);max-width:72ch;margin:0}.page .tt th{top:-22px}.stores>*+*{margin-left:12px}.palette-backdrop{z-index:20;background:color-mix(in srgb, var(--paper) 55%, transparent);place-items:start center;padding-top:12vh;display:grid;position:fixed;inset:0}.palette{background:var(--panel);border:1px solid var(--rule-strong);border-radius:10px;width:min(640px,100vw - 32px);animation:.18s cubic-bezier(.16,1,.3,1) rise;overflow:hidden;box-shadow:0 18px 48px -12px #00000059}.palette-input{border:0;border-bottom:1px solid var(--rule);background:var(--panel);border-radius:0;height:46px;padding:0 14px}.palette-results{max-height:50vh;margin:0;padding:6px;list-style:none;overflow-y:auto}.palette-results [role=option]{height:var(--row);cursor:pointer;white-space:nowrap;border-radius:6px;align-items:center;gap:10px;padding:0 10px;display:flex;overflow:hidden}.palette-results [role=option].active{background:var(--sel);color:var(--sel-ink)}.palette-empty{color:var(--muted);padding:8px 10px}.toast-region{z-index:30;position:fixed}.toast{z-index:30;background:var(--ink);color:var(--paper);border-radius:8px;align-items:center;gap:14px;padding:8px 8px 8px 14px;font-size:14.5px;animation:.22s cubic-bezier(.16,1,.3,1) rise;display:flex;position:fixed;bottom:22px;left:50%;transform:translate(-50%);box-shadow:0 12px 32px -10px #0006}.toast button{border:1px solid color-mix(in srgb, var(--paper) 40%, transparent);color:inherit;background:0 0;border-radius:6px;align-items:center;gap:6px;height:28px;padding:0 10px;font-weight:600;display:inline-flex}@keyframes rise{0%{opacity:0;translate:0 6px}}.loading{color:var(--muted);flex-direction:column;gap:12px;padding:48px 28px;display:flex}.loading-line{background:linear-gradient(90deg, var(--ink) 0 30%, var(--rule) 30% 100%);background-size:200% 100%;width:220px;height:3px;animation:1.2s cubic-bezier(.65,0,.35,1) infinite travel;display:block}@keyframes travel{0%{background-position:100% 0}to{background-position:-40% 0}}.problem{max-width:64ch;padding:48px 28px}.problem h1{margin:0 0 8px;font-size:18px}.tt-note{color:var(--muted);font-size:13.5px}.perm{font-size:13.5px;font-weight:600}.perm-allow{color:var(--ok)}.perm-ask{color:var(--warn)}.perm-deny{color:var(--bad)}.fix{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.fix .head-action{height:22px;margin-left:0;padding:0 8px}.tt-head .fix{margin-left:4px}.view-switch{flex-shrink:0;margin-left:0}.view-switch button{height:28px}.filter.hidden{display:none}.section-index.hidden{visibility:hidden}.context{border-top:2px solid var(--rule-strong);margin-top:4px;padding-top:12px}.context-summary{margin:0 0 4px;font-size:15px}.context>.muted{max-width:80ch;margin:0 0 12px}.context-large{white-space:normal;margin:0 0 12px;padding:6px 10px;line-height:1.5}.context-bar{gap:2px;height:8px;margin:0 0 8px;display:flex}.context-bar span{border-radius:2px;min-width:3px}.bar-managed{background:var(--line-managed)}.bar-user{background:var(--line-user)}.bar-project{background:var(--line-project)}.bar-local{background:var(--line-local)}.bar-memory{background:var(--line-memory)}.context-list{margin:0;padding:0;list-style:none}.context-list li{border-bottom:1px solid var(--rule)}.context-list summary{min-height:var(--row);cursor:pointer;grid-template-columns:28px 26px minmax(0,1fr) 84px 64px 64px;align-items:center;gap:8px;padding:6px 4px;list-style:none;display:grid}.context-list summary::-webkit-details-marker{display:none}.context-list summary:hover{background:var(--hover)}.context-list summary .num{font-family:var(--mono);text-align:right;color:var(--ink-2);font-size:13px}.context-list summary .num:first-child{text-align:left;color:var(--muted)}.context-name{flex-direction:column;min-width:0;display:flex}.context-name .mono{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.context-list details[open] summary{background:var(--sunk)}.context-list pre.raw{max-height:60vh;margin:8px 0 12px;overflow-y:auto}.context-hidden{margin:8px 4px 12px}.sharing{flex-wrap:wrap;gap:6px 24px;margin:0 0 4px;font-size:13.5px;display:flex}.sharing-group{align-items:baseline;gap:8px;min-width:0;display:flex}.sharing dt{white-space:nowrap}.sharing dd{color:var(--ink-2);flex-wrap:wrap;gap:2px 14px;margin:0;display:flex}.sharing dd span{white-space:nowrap}.problems{background:var(--warn-bg);color:var(--warn);border-radius:6px;align-items:flex-start;gap:8px;margin:0 0 12px;padding:8px 12px;font-size:13.5px;display:flex}.problems ul{overflow-wrap:anywhere;margin:0}@media (prefers-reduced-motion:reduce){*,:before{transition:none!important;animation:none!important}}@media (width>=1181px) and (width<=1400px){:root{--rail:220px;--inspector:360px}.tt-instructions :is(th,td):nth-child(6){display:none}}@media (width<=1180px){.workspace{grid-template-columns:minmax(0,1fr)}.inspector{width:min(var(--inspector), 100vw);z-index:10;position:fixed;top:0;bottom:0;right:0;box-shadow:-16px 0 40px -16px #00000059}.rail-keys{display:none}}@media (width<=760px){.app{grid-template-rows:auto minmax(0,1fr);grid-template-columns:minmax(0,1fr)}.rail{border-right:0;border-bottom:1px solid var(--rule);max-height:34vh}.work-main,.page{padding-left:16px;padding-right:16px}.filter{flex:160px;width:auto}.toolbar{flex-wrap:wrap;width:100%}.section-index{flex-wrap:wrap}.sharing-group{flex-direction:column;gap:2px}.dir-head{grid-template-rows:auto;grid-template-columns:minmax(0,1fr);height:auto;padding-top:18px;position:static}.dir-title{flex-wrap:wrap}.tt-section{scroll-margin-top:0}.palette-button{display:none}.context-list summary{grid-template-columns:22px 26px minmax(0,1fr) 56px}.context-list summary .num:nth-child(4),.context-list summary .num:last-child{display:none}.route-stops{flex-direction:column}.route-stops:before{width:3px;height:auto;inset:22px auto 22px 19px}.stop button{grid-template-rows:auto;grid-template-columns:30px auto 1fr;align-items:center;padding:6px 8px}.tt th:nth-child(n){width:auto}.tt th:first-child:nth-child(n),.tt-instructions th:nth-child(2):nth-child(n){width:40px}.tt-settings th:first-child:nth-child(n){width:64px}.tt-compare,.tt-compare :is(thead,tbody){display:block}.tt-compare tr{grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.tt-compare th:first-child{display:none}.tt-compare td:first-child{border-bottom:0;grid-column:1/-1;padding-bottom:0}.tt-instructions th:first-child,.tt-instructions td:first-child,.tt-instructions :is(th,td):is(:nth-child(5),:nth-child(6)),.tt-settings :is(th,td):nth-child(4),.tt-memory :is(th,td):is(:nth-child(3),:nth-child(4),:nth-child(6)),.tt-permissions :is(th,td):nth-child(4),.tt-tools :is(th,td):is(:nth-child(4),:nth-child(6)),.tt-hooks :is(th,td):nth-child(4),.tt-mcp :is(th,td):nth-child(4),.tt-extensions :is(th,td):nth-child(4),.tt-shared :is(th,td):nth-child(n+4){display:none}.tt-instructions th:nth-child(4):nth-child(n){width:72px}.tt-instructions th:nth-child(7):nth-child(n){width:84px}.tt th{box-shadow:none;position:static}}.head-action{border:1px solid var(--rule);background:var(--panel);border-radius:6px;align-items:center;gap:6px;height:26px;margin-left:auto;padding:0 10px;font-size:13.5px;display:inline-flex}.head-action:hover{border-color:var(--ink-2)}.editor{gap:10px;margin:0 0 18px;display:grid}.editor>p{margin:0}.hint{color:var(--ink-2);font-size:14px}.editor .segmented{justify-self:start;margin-left:0}.field{color:var(--muted);gap:4px;font-size:13.5px;display:grid}.field input:not([type=checkbox]),.field select,.field textarea,.path-form input{border:1px solid var(--rule);background:var(--paper);width:100%;color:var(--ink);border-radius:6px;padding:6px 8px;font-size:14px}.field input:not([type=checkbox]),.field select,.path-form input{height:32px}.field textarea{resize:vertical;line-height:1.5}.field input:focus,.field select:focus,.field textarea:focus,.path-form input:focus{outline:2px solid var(--focus);outline-offset:-1px}.check{color:var(--ink);align-items:center;gap:8px;font-size:14px;display:inline-flex}.check input{width:16px;height:16px;accent-color:var(--ink);margin:0}.editor-buttons{flex-wrap:wrap;gap:8px;display:flex}.editor-buttons button,.path-form button{border:1px solid var(--rule);background:var(--panel);border-radius:6px;height:30px;padding:0 12px;font-size:14px}.editor-buttons button:hover,.path-form button:hover{border-color:var(--ink-2)}.editor-buttons button.primary{background:var(--ink);border-color:var(--ink);color:var(--paper);font-weight:600}.key-picker{position:relative}.key-results{border:1px solid var(--rule);background:var(--panel);border-radius:6px;max-height:280px;margin-top:4px;padding:4px;overflow-y:auto}.key-results [role=option]{cursor:pointer;border-radius:4px;gap:1px;padding:6px 8px;font-size:13.5px;display:grid}.key-results [role=option] .muted{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.key-results [role=option].active{background:var(--sel);color:var(--sel-ink)}.review{border-top:1px solid var(--rule-strong);gap:8px;padding-top:10px;display:grid}.suggestions{border:0;flex-wrap:wrap;align-items:center;gap:4px;margin:0;padding:0;display:flex}.suggestions .muted{margin-right:4px}.suggestions button{border:1px solid var(--rule);background:var(--panel);border-radius:4px;height:24px;padding:0 8px;font-size:13px}.suggestions button[aria-pressed=true]{background:var(--ink);border-color:var(--ink);color:var(--paper)}.docs-link{align-items:center;gap:4px;display:inline-flex}.facet-select{border:1px solid var(--rule);background:var(--panel);max-width:260px;height:26px;color:var(--ink);border-radius:6px;padding:0 6px;font-size:13.5px}.catalog-files{white-space:nowrap}.catalog-files .bullet+.bullet{margin-left:2px}.docs-text>:first-child{margin-top:0}.docs-text>:last-child{margin-bottom:0}.tool-switch-rule{flex-wrap:wrap;align-items:center;gap:6px;padding:4px 0;display:flex}.tool-switch-rule .head-action{margin-left:auto}.tt-head .memory-review{flex-basis:100%}.memory-review-file{gap:4px;display:grid}.editor-buttons button.danger{background:var(--bad);border-color:var(--bad);color:var(--on-bad);font-weight:600}.review .sub-title{overflow-wrap:anywhere;margin:0}pre.diff{background:var(--sunk);border-radius:6px;max-height:360px;margin:0;padding:8px 0;line-height:1.5;overflow-x:auto}pre.diff span{white-space:pre;padding:0 10px;display:block}.diff-add{background:color-mix(in srgb, var(--ok) 16%, transparent);color:var(--ink)}.diff-remove{background:color-mix(in srgb, var(--bad) 16%, transparent);color:var(--ink);text-decoration:line-through;-webkit-text-decoration-color:color-mix(in srgb, var(--bad) 60%, transparent);text-decoration-color:color-mix(in srgb, var(--bad) 60%, transparent)}.diff-same{color:var(--muted)}.rail-empty{color:var(--muted);margin:2px 8px;font-size:14px}.rail-keys{color:var(--muted);gap:6px;margin:auto 8px 0;padding:24px 0 0;font-size:13px;list-style:none;display:grid}.visually-hidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.tt th .sort{height:100%;font:inherit;color:inherit;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.tt th .sort:hover,.tt th[aria-sort] .sort{color:var(--ink)}.sort-mark{font-family:var(--mono);opacity:0;font-size:12px;transition:opacity .12s}.tt th .sort:hover .sort-mark,.tt th .sort:focus-visible .sort-mark,.sort-mark.on{opacity:1}.show-more{border:0;border-bottom:1px solid var(--rule);text-align:left;background:0 0;width:100%;margin:0;padding:7px 8px;font-size:13.5px;font-weight:600;display:block}.show-more:hover{background:var(--hover)}.tt-head .filter{height:28px;margin-left:auto}.tool-outcome{font-size:13.5px}.tool-denied{color:var(--bad);font-weight:600}.tool-asks{color:var(--warn)}.tool-allowed{color:var(--ok);font-weight:600}.tool-free{color:var(--muted)}.diff-skip{color:var(--muted);font-style:italic}.tt-settings.no-overrides :is(th,td):nth-child(4){display:none}.head-action+.head-action{margin-left:0}.tt-compare th:first-child{width:28%}.tt-compare tbody tr{cursor:default}.tt-compare td{vertical-align:top;height:auto;padding-top:5px;padding-bottom:5px}.cmp-value,.cmp-from{text-overflow:ellipsis;display:block;overflow:hidden}.cmp-from{color:var(--muted);font-size:13px}.cmp-from>*+*{margin-left:8px}.change-day{max-width:980px;margin-top:22px}.change-day h2{border-top:2px solid var(--rule-strong);margin:0 0 4px;padding-top:8px;font-size:14px;font-weight:700}.changes{grid-template-columns:minmax(0,1fr);max-width:980px;display:grid}.changes .change{border-top:1px solid var(--rule);grid-template-columns:minmax(0,1fr);gap:10px;padding:10px 0;display:grid}.change-row{grid-template-columns:11rem minmax(0,1fr) 6rem 9rem;grid-template-areas:"what file when action";align-items:baseline;gap:6px 12px;font-size:14px;display:grid}.change-what{grid-area:what;font-weight:600}.change-file{grid-area:file;gap:2px;min-width:0;display:grid}.change-file .muted{font-size:12.5px}.change-row .num{grid-area:when}.change-row .head-action{grid-area:action;justify-self:end;margin-left:0}@media (width<=720px){.change-row{grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"what when""file file""action action"}.change-row .head-action{justify-self:start}}.path-tester{border-top:1px solid var(--rule);gap:8px;max-width:760px;margin-top:28px;padding-top:14px;display:grid}.path-tester .sub-title{margin:0}.path-form{gap:8px;display:flex}.path-form input{flex:1;min-width:0}.path-matches{gap:6px;font-size:14px;display:grid}.path-matches li{flex-wrap:wrap;align-items:baseline;gap:4px 10px;display:flex}