@youtyan/code-viewer 0.1.37 → 0.1.39

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/README.md CHANGED
@@ -149,6 +149,21 @@ than the current directory, or `--server <url>` to target a specific server.
149
149
  run `annotate start` again to begin a new session, or pass `--session <id>`
150
150
  to target a specific one.
151
151
 
152
+ ### Agent Skill
153
+
154
+ The package bundles an [Agent Skill](https://agentskills.io) (the SKILL.md
155
+ open standard) that teaches AI coding agents when and how to use
156
+ `annotate`. Install it into the current project:
157
+
158
+ ```sh
159
+ npx -y @youtyan/code-viewer skill install # Claude Code (.claude/skills/)
160
+ npx -y @youtyan/code-viewer skill install --agent codex,gemini # other agents
161
+ npx -y @youtyan/code-viewer skill install --agent all # claude, codex, gemini, cursor, .agents
162
+ ```
163
+
164
+ Or install it once for all projects with `--global` (`~/.claude/skills/`
165
+ etc). Running the same command again updates an existing installation.
166
+
152
167
  ## Development
153
168
 
154
169
  ```sh
@@ -1624,6 +1624,176 @@ var init_annotate_cli = __esm(() => {
1624
1624
  init_server_registry();
1625
1625
  });
1626
1626
 
1627
+ // web-src/server/root.ts
1628
+ import { existsSync as existsSync4 } from "node:fs";
1629
+ import { dirname, join as join4, normalize } from "node:path";
1630
+ import { fileURLToPath } from "node:url";
1631
+ function findRoot(start) {
1632
+ let current = start;
1633
+ for (let i = 0;i < 5; i++) {
1634
+ if (existsSync4(join4(current, "package.json")) && existsSync4(join4(current, "web"))) {
1635
+ return normalize(current);
1636
+ }
1637
+ const parent = dirname(current);
1638
+ if (parent === current)
1639
+ break;
1640
+ current = parent;
1641
+ }
1642
+ return normalize(join4(start, "..", ".."));
1643
+ }
1644
+ var ROOT;
1645
+ var init_root = __esm(() => {
1646
+ ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
1647
+ });
1648
+
1649
+ // web-src/server/skill-cli.ts
1650
+ var exports_skill_cli = {};
1651
+ __export(exports_skill_cli, {
1652
+ runSkillCli: () => runSkillCli,
1653
+ parseSkillArgs: () => parseSkillArgs,
1654
+ installSkill: () => installSkill,
1655
+ SKILL_HELP: () => SKILL_HELP,
1656
+ AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
1657
+ });
1658
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
1659
+ import { homedir as homedir2 } from "node:os";
1660
+ import { join as join5, resolve } from "node:path";
1661
+ function parseAgentList(value) {
1662
+ if (value === "all")
1663
+ return [...AGENT_NAMES];
1664
+ const names = value.split(",").map((name) => name.trim()).filter(Boolean);
1665
+ if (names.length === 0)
1666
+ return null;
1667
+ const result = [];
1668
+ for (const name of names) {
1669
+ if (!(name in AGENT_SKILL_DIRS))
1670
+ return null;
1671
+ const agent = name;
1672
+ if (!result.includes(agent))
1673
+ result.push(agent);
1674
+ }
1675
+ return result;
1676
+ }
1677
+ function parseSkillArgs(argv) {
1678
+ if (argv.length === 0 || argv.includes("--help") || argv[0] === "help") {
1679
+ return { ok: true, args: { kind: "help" } };
1680
+ }
1681
+ const [command, ...rest] = argv;
1682
+ if (command !== "install") {
1683
+ return { ok: false, error: `unknown skill command: ${command}` };
1684
+ }
1685
+ let global = false;
1686
+ let cwd;
1687
+ let agents = ["claude"];
1688
+ for (let i = 0;i < rest.length; i++) {
1689
+ const arg = rest[i];
1690
+ if (arg === "--global") {
1691
+ global = true;
1692
+ } else if (arg === "--cwd") {
1693
+ cwd = rest[++i];
1694
+ if (!cwd)
1695
+ return { ok: false, error: "--cwd requires a directory" };
1696
+ } else if (arg === "--agent") {
1697
+ const value = rest[++i];
1698
+ if (!value)
1699
+ return { ok: false, error: "--agent requires a list" };
1700
+ const parsed = parseAgentList(value);
1701
+ if (!parsed) {
1702
+ return {
1703
+ ok: false,
1704
+ error: `unknown agent in "${value}" (valid: ${AGENT_NAMES.join(", ")}, all)`
1705
+ };
1706
+ }
1707
+ agents = parsed;
1708
+ } else {
1709
+ return { ok: false, error: `unknown option: ${arg}` };
1710
+ }
1711
+ }
1712
+ return { ok: true, args: { kind: "install", agents, global, cwd } };
1713
+ }
1714
+ function installSkill(args, deps) {
1715
+ if (!existsSync5(join5(deps.sourceDir, "SKILL.md"))) {
1716
+ return {
1717
+ ok: false,
1718
+ error: `bundled skill not found at ${deps.sourceDir}`
1719
+ };
1720
+ }
1721
+ const base = args.global ? deps.homeDir : resolve(args.cwd ?? deps.projectDir);
1722
+ const results = [];
1723
+ for (const agent of args.agents) {
1724
+ const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
1725
+ const action = existsSync5(target) ? "updated" : "installed";
1726
+ try {
1727
+ mkdirSync3(target, { recursive: true });
1728
+ cpSync(deps.sourceDir, target, { recursive: true });
1729
+ } catch (error) {
1730
+ return { ok: false, error: String(error) };
1731
+ }
1732
+ results.push({ agent, action, target });
1733
+ }
1734
+ return { ok: true, results };
1735
+ }
1736
+ function runSkillCli(argv) {
1737
+ const parsed = parseSkillArgs(argv);
1738
+ if (parsed.ok === false) {
1739
+ console.error(parsed.error);
1740
+ console.error('Run "code-viewer skill --help" for usage.');
1741
+ process.exit(1);
1742
+ }
1743
+ if (parsed.args.kind === "help") {
1744
+ console.log(SKILL_HELP);
1745
+ return;
1746
+ }
1747
+ const result = installSkill(parsed.args, {
1748
+ sourceDir: join5(ROOT, "skills", SKILL_NAME),
1749
+ homeDir: homedir2(),
1750
+ projectDir: process.cwd()
1751
+ });
1752
+ if (result.ok === false) {
1753
+ console.error(result.error);
1754
+ process.exit(1);
1755
+ }
1756
+ for (const entry of result.results) {
1757
+ console.log(`${entry.action} (${entry.agent}): ${entry.target}`);
1758
+ }
1759
+ if (result.results.some((entry) => entry.action === "installed")) {
1760
+ console.log("Re-run the same command anytime to update the skill.");
1761
+ }
1762
+ }
1763
+ var SKILL_NAME = "code-viewer-annotate", AGENT_SKILL_DIRS, AGENT_NAMES, SKILL_HELP;
1764
+ var init_skill_cli = __esm(() => {
1765
+ init_root();
1766
+ AGENT_SKILL_DIRS = {
1767
+ claude: ".claude",
1768
+ codex: ".codex",
1769
+ gemini: ".gemini",
1770
+ cursor: ".cursor",
1771
+ agents: ".agents"
1772
+ };
1773
+ AGENT_NAMES = Object.keys(AGENT_SKILL_DIRS);
1774
+ SKILL_HELP = `code-viewer skill — manage the bundled agent skill
1775
+
1776
+ Usage:
1777
+ code-viewer skill install [--agent <list>] [--global] [--cwd <dir>]
1778
+
1779
+ Installs the ${SKILL_NAME} skill (SKILL.md for AI coding agents) into the
1780
+ skills directory of each selected agent in the current project, or into the
1781
+ home directory equivalents with --global. Running install again overwrites
1782
+ the files, so the same command also updates an existing installation.
1783
+
1784
+ Options:
1785
+ --agent <list> comma separated agents: ${AGENT_NAMES.join(", ")}, or all
1786
+ (default: claude)
1787
+ --global install into the home directory (~/.claude/skills/ etc)
1788
+ --cwd <dir> project directory to install into (ignored with --global)
1789
+
1790
+ Examples:
1791
+ code-viewer skill install
1792
+ code-viewer skill install --agent claude,codex,gemini
1793
+ code-viewer skill install --agent all --global
1794
+ `;
1795
+ });
1796
+
1627
1797
  // web-src/core/directory-name.ts
1628
1798
  function normalizeNewDirectoryName(name) {
1629
1799
  if (typeof name !== "string")
@@ -1650,7 +1820,7 @@ var init_routes = __esm(() => {
1650
1820
 
1651
1821
  // web-src/server/cache.ts
1652
1822
  import { lstatSync as lstatSync2 } from "node:fs";
1653
- import { join as join4 } from "node:path";
1823
+ import { join as join6 } from "node:path";
1654
1824
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
1655
1825
  return !!cached && now - cached.storedAt <= ttlMs;
1656
1826
  }
@@ -1665,7 +1835,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
1665
1835
  }
1666
1836
  function worktreeFileSignature(path, cwd) {
1667
1837
  try {
1668
- const stats = lstatSync2(join4(cwd, path));
1838
+ const stats = lstatSync2(join6(cwd, path));
1669
1839
  const inode = "ino" in stats ? stats.ino : 0;
1670
1840
  return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
1671
1841
  } catch {
@@ -1921,28 +2091,6 @@ function collectLineRangeFromIndexedText(text, index, start, end) {
1921
2091
  return { lines, total: index.total, complete: end >= index.total };
1922
2092
  }
1923
2093
 
1924
- // web-src/server/root.ts
1925
- import { existsSync as existsSync4 } from "node:fs";
1926
- import { dirname, join as join5, normalize } from "node:path";
1927
- import { fileURLToPath } from "node:url";
1928
- function findRoot(start) {
1929
- let current = start;
1930
- for (let i = 0;i < 5; i++) {
1931
- if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
1932
- return normalize(current);
1933
- }
1934
- const parent = dirname(current);
1935
- if (parent === current)
1936
- break;
1937
- current = parent;
1938
- }
1939
- return normalize(join5(start, "..", ".."));
1940
- }
1941
- var ROOT;
1942
- var init_root = __esm(() => {
1943
- ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
1944
- });
1945
-
1946
2094
  // web-src/server/search.ts
1947
2095
  function normalizeGrepMax(value) {
1948
2096
  const parsed = Number(value || "");
@@ -2069,7 +2217,7 @@ import {
2069
2217
  readdirSync as nodeReaddirSync,
2070
2218
  watch as nodeWatch
2071
2219
  } from "node:fs";
2072
- import { join as join6, relative } from "node:path";
2220
+ import { join as join7, relative } from "node:path";
2073
2221
  function normalizeRelativePath(path) {
2074
2222
  return path.replace(/\\/g, "/").replace(/^\/+/, "");
2075
2223
  }
@@ -2152,7 +2300,7 @@ function startWorktreeUpdateWatch(options) {
2152
2300
  for (const entry of entries) {
2153
2301
  if (!entry.isDirectory())
2154
2302
  continue;
2155
- children.push(join6(dir, entry.name));
2303
+ children.push(join7(dir, entry.name));
2156
2304
  }
2157
2305
  return children;
2158
2306
  };
@@ -2181,10 +2329,10 @@ function startWorktreeUpdateWatch(options) {
2181
2329
  scheduleUpdate();
2182
2330
  return;
2183
2331
  }
2184
- const changed = normalizeRelativePath(join6(rel, filename.toString()));
2332
+ const changed = normalizeRelativePath(join7(rel, filename.toString()));
2185
2333
  if (ignored(changed))
2186
2334
  return;
2187
- const fullChangedPath = join6(options.root, changed);
2335
+ const fullChangedPath = join7(options.root, changed);
2188
2336
  if (!isInsideRoot(options.root, fullChangedPath))
2189
2337
  return;
2190
2338
  const known = watchers.has(fullChangedPath);
@@ -2243,9 +2391,9 @@ var exports_preview = {};
2243
2391
  import {
2244
2392
  closeSync,
2245
2393
  constants,
2246
- existsSync as existsSync5,
2394
+ existsSync as existsSync6,
2247
2395
  lstatSync as lstatSync4,
2248
- mkdirSync as mkdirSync3,
2396
+ mkdirSync as mkdirSync4,
2249
2397
  openSync,
2250
2398
  readFileSync as readFileSync5,
2251
2399
  realpathSync as realpathSync2,
@@ -2255,8 +2403,8 @@ import {
2255
2403
  watch,
2256
2404
  writeFileSync as writeFileSync3
2257
2405
  } from "node:fs";
2258
- import { homedir as homedir2 } from "node:os";
2259
- import { basename as basename2, dirname as dirname2, extname, join as join7, relative as relative2 } from "node:path";
2406
+ import { homedir as homedir3 } from "node:os";
2407
+ import { basename as basename2, dirname as dirname2, extname, join as join8, relative as relative2 } from "node:path";
2260
2408
  function parseCli() {
2261
2409
  const rest = [];
2262
2410
  for (let i = 2;i < process.argv.length; i++) {
@@ -2395,8 +2543,8 @@ function staticFile(pathname) {
2395
2543
  const spec = map[pathname];
2396
2544
  if (!spec)
2397
2545
  return null;
2398
- const full = join7(WEB_ROOT, spec[0]);
2399
- if (!existsSync5(full))
2546
+ const full = join8(WEB_ROOT, spec[0]);
2547
+ if (!existsSync6(full))
2400
2548
  return text("not found", 404);
2401
2549
  return new Response(readFileSync5(full), {
2402
2550
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
@@ -2629,8 +2777,8 @@ function parseScopeExcludeNamesQuery(value) {
2629
2777
  return normalizeScopeExcludeNames(names);
2630
2778
  }
2631
2779
  function loadProjectConfig() {
2632
- const full = join7(cwd, ".code-viewer.json");
2633
- if (!existsSync5(full))
2780
+ const full = join8(cwd, ".code-viewer.json");
2781
+ if (!existsSync6(full))
2634
2782
  return null;
2635
2783
  let realCwd;
2636
2784
  let realConfig;
@@ -2694,8 +2842,8 @@ function safeWorktreePath(path) {
2694
2842
  return null;
2695
2843
  if (isGitInternalPath(path))
2696
2844
  return null;
2697
- const full = join7(cwd, path);
2698
- if (!existsSync5(full))
2845
+ const full = join8(cwd, path);
2846
+ if (!existsSync6(full))
2699
2847
  return null;
2700
2848
  let realCwd;
2701
2849
  let realFull;
@@ -2713,7 +2861,7 @@ function safeWorktreePath(path) {
2713
2861
  return realFull;
2714
2862
  }
2715
2863
  function worktreePath(path) {
2716
- return join7(cwd, path);
2864
+ return join8(cwd, path);
2717
2865
  }
2718
2866
  function safeOpenWorktreePath(path) {
2719
2867
  if (path === "") {
@@ -3476,10 +3624,10 @@ async function handleUploadFiles(req) {
3476
3624
  total += file.size;
3477
3625
  if (total > MAX_UPLOAD_TOTAL_BYTES)
3478
3626
  return text("upload too large", 413);
3479
- const target = join7(realDir, safeName);
3627
+ const target = join8(realDir, safeName);
3480
3628
  if (relative2(realDir, dirname2(target)) !== "")
3481
3629
  return text("invalid filename", 400);
3482
- if (existsSync5(target))
3630
+ if (existsSync6(target))
3483
3631
  return text("file exists", 409);
3484
3632
  uploads.push({ file, name: safeName, target });
3485
3633
  }
@@ -3597,11 +3745,11 @@ function triggerUpdate() {
3597
3745
  sendSse("update");
3598
3746
  }
3599
3747
  function moveMacPathIntoTrash(path) {
3600
- const trashDir = join7(homedir2(), ".Trash");
3748
+ const trashDir = join8(homedir3(), ".Trash");
3601
3749
  const base = basename2(path) || "code-viewer-trash-item";
3602
- const target = join7(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3750
+ const target = join8(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3603
3751
  try {
3604
- mkdirSync3(trashDir, { recursive: true });
3752
+ mkdirSync4(trashDir, { recursive: true });
3605
3753
  renameSync2(path, target);
3606
3754
  return { ok: true, trashPath: target };
3607
3755
  } catch (error) {
@@ -3633,19 +3781,19 @@ function restoreTrashPath(originalPath, trashPath) {
3633
3781
  if (!parentFullPath)
3634
3782
  return { ok: false, error: "invalid restore target" };
3635
3783
  const original = worktreePath(originalPath);
3636
- if (existsSync5(original))
3784
+ if (existsSync6(original))
3637
3785
  return { ok: false, error: "restore target exists" };
3638
3786
  if (trashPath) {
3639
3787
  if (process.platform !== "darwin")
3640
3788
  return { ok: false, error: "invalid trash handle" };
3641
- if (!existsSync5(trashPath))
3789
+ if (!existsSync6(trashPath))
3642
3790
  return { ok: false, error: "trash item not found" };
3643
3791
  try {
3644
- const trashRoot = join7(homedir2(), ".Trash");
3792
+ const trashRoot = join8(homedir3(), ".Trash");
3645
3793
  const trashRelative = relative2(trashRoot, trashPath);
3646
3794
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
3647
3795
  return { ok: false, error: "invalid trash handle" };
3648
- mkdirSync3(dirname2(original), { recursive: true });
3796
+ mkdirSync4(dirname2(original), { recursive: true });
3649
3797
  renameSync2(trashPath, original);
3650
3798
  return { ok: true };
3651
3799
  } catch (error) {
@@ -3791,11 +3939,11 @@ async function handleCreateDirectory(req) {
3791
3939
  const targetPath = dir ? `${dir}/${name}` : name;
3792
3940
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
3793
3941
  return text("invalid target", 400);
3794
- const target = join7(parent, name);
3795
- if (existsSync5(target))
3942
+ const target = join8(parent, name);
3943
+ if (existsSync6(target))
3796
3944
  return text("already exists", 409);
3797
3945
  try {
3798
- mkdirSync3(target, { recursive: false });
3946
+ mkdirSync4(target, { recursive: false });
3799
3947
  } catch (error) {
3800
3948
  if (error.code === "EEXIST")
3801
3949
  return text("already exists", 409);
@@ -3973,8 +4121,8 @@ var init_preview = __esm(async () => {
3973
4121
  init_search();
3974
4122
  init_server_registry();
3975
4123
  init_worktree_watcher();
3976
- WEB_ROOT = join7(ROOT, "web");
3977
- VERSION = JSON.parse(readFileSync5(join7(ROOT, "package.json"), "utf8")).version;
4124
+ WEB_ROOT = join8(ROOT, "web");
4125
+ VERSION = JSON.parse(readFileSync5(join8(ROOT, "package.json"), "utf8")).version;
3978
4126
  DEFAULT_ARGS = ["HEAD"];
3979
4127
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
3980
4128
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -4172,6 +4320,9 @@ data: ok
4172
4320
  if (process.argv[2] === "annotate") {
4173
4321
  const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
4174
4322
  await runAnnotateCli2(process.argv.slice(3));
4323
+ } else if (process.argv[2] === "skill") {
4324
+ const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
4325
+ runSkillCli2(process.argv.slice(3));
4175
4326
  } else {
4176
4327
  await init_preview().then(() => exports_preview);
4177
4328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@
23
23
  "dist",
24
24
  "web",
25
25
  "README.md",
26
- "LICENSE"
26
+ "LICENSE",
27
+ "skills"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "bun run build:web && bun run build:server",
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: code-viewer-annotate
3
+ description: Use when walking a human through code in their browser with code-viewer annotations - explaining changes you just made, guiding a code review in reading order, or onboarding walkthroughs. Triggers on "annotate", "ウォークスルー", "コードを説明して", "変更を解説して", "レビューを案内して", "walkthrough", "explain this change in the browser".
4
+ ---
5
+
6
+ # code-viewer annotate
7
+
8
+ Create browser walkthroughs for a human: each annotation jumps every open
9
+ code-viewer tab to a file location and renders your explanation directly
10
+ under the annotated lines. The human can also replay a session with
11
+ text-to-speech playback.
12
+
13
+ ## When to use
14
+
15
+ - Explaining a change you just made (per-file, per-hunk commentary)
16
+ - Guiding a code review: point at the risky lines, in reading order
17
+ - Onboarding walkthroughs: "how does feature X flow through the code"
18
+
19
+ ## Requirements
20
+
21
+ - A code-viewer server must already be running for the repository
22
+ (the human starts it with: `code-viewer`). The CLI never starts one.
23
+ - Run from inside the repository, or pass `--cwd <repo>`.
24
+ - If `code-viewer` is not on PATH, prefix every command with
25
+ `npx -y @youtyan/code-viewer`.
26
+
27
+ ## Workflow
28
+
29
+ 1. Start a session per walkthrough topic (the title is shown to the human):
30
+
31
+ ```sh
32
+ code-viewer annotate start --title "How the cache invalidation works"
33
+ ```
34
+
35
+ 2. Add annotations in READING ORDER (the order the human should follow):
36
+
37
+ ```sh
38
+ code-viewer annotate add --file src/cache.ts --line 42-58 --body "..."
39
+ ```
40
+
41
+ ## Full reference
42
+
43
+ Before composing annotations, read the complete agent guide (all commands,
44
+ options, body formatting rules):
45
+
46
+ ```sh
47
+ code-viewer annotate agent-help
48
+ ```
package/web/app.js CHANGED
@@ -246,6 +246,8 @@
246
246
  allowPaletteOpen: true
247
247
  },
248
248
  { action: "focus-file-filter", key: "/" },
249
+ { action: "annotation-next", key: "]" },
250
+ { action: "annotation-previous", key: "[" },
249
251
  { action: "focus-sidebar", key: "h", ctrl: true },
250
252
  { action: "focus-main", key: "l", ctrl: true },
251
253
  {
@@ -7339,6 +7341,13 @@ ${frontmatter.yaml}
7339
7341
  sessionEl.className = "annotation-session";
7340
7342
  sessionEl.dataset.sessionId = session.id;
7341
7343
  sessionEl.classList.toggle("active", session.id === activeSessionId);
7344
+ sessionEl.addEventListener("click", (event) => {
7345
+ const target = event.target;
7346
+ if (target.closest("button, a, input"))
7347
+ return;
7348
+ if (session.id !== activeSessionId)
7349
+ setActiveSession(session.id);
7350
+ });
7342
7351
  const head = document.createElement("div");
7343
7352
  head.className = "annotation-session-head";
7344
7353
  const title = document.createElement("button");
@@ -7572,14 +7581,18 @@ ${frontmatter.yaml}
7572
7581
  deps.scrollDiffElementIntoView(inlineRow, "center");
7573
7582
  }
7574
7583
  function stepAnnotation(direction) {
7575
- if (!activeAnnotationId)
7584
+ const found = activeAnnotationId ? findAnnotation(activeAnnotationId) : null;
7585
+ if (found && (!activeSessionId || found.session.id === activeSessionId)) {
7586
+ const next = found.session.entries[found.index + direction];
7587
+ if (next)
7588
+ openAnnotationEntry(next.id);
7576
7589
  return;
7577
- const found = findAnnotation(activeAnnotationId);
7578
- if (!found)
7579
- return;
7580
- const next = found.session.entries[found.index + direction];
7581
- if (next)
7582
- openAnnotationEntry(next.id);
7590
+ }
7591
+ const session = ANNOTATIONS.sessions.find((s2) => s2.id === activeSessionId) ?? ANNOTATIONS.sessions[0];
7592
+ const entries = session?.entries ?? [];
7593
+ const entry = direction === 1 ? entries[0] : entries[entries.length - 1];
7594
+ if (entry)
7595
+ openAnnotationEntry(entry.id);
7583
7596
  }
7584
7597
  function handleSse(raw) {
7585
7598
  let event = null;
@@ -7650,7 +7663,8 @@ ${frontmatter.yaml}
7650
7663
  },
7651
7664
  getActiveAnnotationId() {
7652
7665
  return activeAnnotationId;
7653
- }
7666
+ },
7667
+ stepAnnotation
7654
7668
  };
7655
7669
  }
7656
7670
 
@@ -8794,7 +8808,8 @@ ${frontmatter.yaml}
8794
8808
  ["Ctrl+K", "Open file palette"],
8795
8809
  ["Ctrl+G", "Open grep palette"],
8796
8810
  ["/", "Focus file filter"],
8797
- ["t", "Toggle theme"]
8811
+ ["t", "Toggle theme"],
8812
+ ["[ / ]", "Previous / next annotation"]
8798
8813
  ]
8799
8814
  },
8800
8815
  {
@@ -8842,7 +8857,8 @@ ${frontmatter.yaml}
8842
8857
  ["Ctrl+K", "ファイルパレットを開く"],
8843
8858
  ["Ctrl+G", "grep パレットを開く"],
8844
8859
  ["/", "ファイルフィルターへフォーカス"],
8845
- ["t", "テーマ切り替え"]
8860
+ ["t", "テーマ切り替え"],
8861
+ ["[ / ]", "前 / 次の注釈へ移動"]
8846
8862
  ]
8847
8863
  },
8848
8864
  {
@@ -15631,6 +15647,10 @@ ${frontmatter.yaml}
15631
15647
  if (action === "tab-preview" || action === "tab-code") {
15632
15648
  return switchSourceTab(action === "tab-preview" ? "preview" : "code");
15633
15649
  }
15650
+ if (action === "annotation-next" || action === "annotation-previous") {
15651
+ ANNOTATIONS_UI?.stepAnnotation(action === "annotation-next" ? 1 : -1);
15652
+ return true;
15653
+ }
15634
15654
  if (action === "start-g-sequence") {
15635
15655
  PENDING_G_SCOPE = scope;
15636
15656
  PENDING_G_UNTIL = performance.now() + 900;
package/web/style.css CHANGED
@@ -1132,7 +1132,8 @@ body.gdp-resizing * { user-select: none !important; }
1132
1132
  top: calc(var(--global-header-h) + 8px);
1133
1133
  width: min(460px, calc(100vw - 32px));
1134
1134
  max-width: calc(100vw - 32px);
1135
- z-index: 40;
1135
+ /* Transient popover: must sit above the annotation panel (45). */
1136
+ z-index: 70;
1136
1137
  padding: 12px;
1137
1138
  border: 1px solid var(--border);
1138
1139
  border-radius: 8px;