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 +40 -0
- package/README.md +4 -3
- package/dist/server/agent-edit.js +6 -1
- package/dist/server/mcp-edit.js +12 -8
- package/dist/server/scan/compare.js +3 -1
- package/dist/server/scan/context.js +6 -3
- package/dist/server/scan/env.js +4 -1
- package/dist/server/scan/files.js +3 -1
- package/dist/server/scan/path-test.js +19 -4
- package/dist/server/scan/permissions.js +1 -1
- package/dist/server/scan/usage.js +3 -0
- package/dist/server/settings-edit.js +19 -3
- package/dist/server/watch.js +82 -10
- package/dist/ui/assets/index-D5XqP5c_.css +1 -0
- package/dist/ui/assets/{index-BQ3Lhx8z.js → index-Dg9dxngR.js} +2 -2
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BRgGlpjd.css +0 -1
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
|
[](https://www.npmjs.com/package/ccdesk)
|
|
9
9
|
[](https://github.com/ayagmar/ccdesk/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/ayagmar/ccdesk/blob/master/package.json)
|
|
11
|
+
[](https://deepwiki.com/ayagmar/ccdesk)
|
|
11
12
|
[](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
|
|
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
|
-
|
|
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,
|
package/dist/server/mcp-edit.js
CHANGED
|
@@ -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
|
-
|
|
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 ? [
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/server/scan/env.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
const file = await readTextFile(env, path);
|
|
62
|
+
if (!file)
|
|
59
63
|
return false;
|
|
60
|
-
|
|
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-
|
|
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
|
|
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));
|
package/dist/server/watch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
57
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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}
|