@youtyan/code-viewer 0.1.36 → 0.1.38
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 +14 -0
- package/dist/code-viewer.js +153 -53
- package/package.json +3 -2
- package/skills/code-viewer-annotate/SKILL.md +48 -0
- package/web/app.js +64 -7
package/README.md
CHANGED
|
@@ -149,6 +149,20 @@ 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://code.claude.com/docs/en/skills)
|
|
155
|
+
that teaches AI coding agents when and how to use `annotate`. Install it
|
|
156
|
+
into the current project (`.claude/skills/`):
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
npx -y @youtyan/code-viewer skill install
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or install it once for all projects with `--global`
|
|
163
|
+
(`~/.claude/skills/`). Running the same command again updates an existing
|
|
164
|
+
installation.
|
|
165
|
+
|
|
152
166
|
## Development
|
|
153
167
|
|
|
154
168
|
```sh
|
package/dist/code-viewer.js
CHANGED
|
@@ -1624,6 +1624,125 @@ 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
|
+
});
|
|
1657
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1658
|
+
import { homedir as homedir2 } from "node:os";
|
|
1659
|
+
import { join as join5, resolve } from "node:path";
|
|
1660
|
+
function parseSkillArgs(argv) {
|
|
1661
|
+
if (argv.length === 0 || argv.includes("--help") || argv[0] === "help") {
|
|
1662
|
+
return { ok: true, args: { kind: "help" } };
|
|
1663
|
+
}
|
|
1664
|
+
const [command, ...rest] = argv;
|
|
1665
|
+
if (command !== "install") {
|
|
1666
|
+
return { ok: false, error: `unknown skill command: ${command}` };
|
|
1667
|
+
}
|
|
1668
|
+
let global = false;
|
|
1669
|
+
let cwd;
|
|
1670
|
+
for (let i = 0;i < rest.length; i++) {
|
|
1671
|
+
const arg = rest[i];
|
|
1672
|
+
if (arg === "--global") {
|
|
1673
|
+
global = true;
|
|
1674
|
+
} else if (arg === "--cwd") {
|
|
1675
|
+
cwd = rest[++i];
|
|
1676
|
+
if (!cwd)
|
|
1677
|
+
return { ok: false, error: "--cwd requires a directory" };
|
|
1678
|
+
} else {
|
|
1679
|
+
return { ok: false, error: `unknown option: ${arg}` };
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return { ok: true, args: { kind: "install", global, cwd } };
|
|
1683
|
+
}
|
|
1684
|
+
function installSkill(args, deps) {
|
|
1685
|
+
if (!existsSync5(join5(deps.sourceDir, "SKILL.md"))) {
|
|
1686
|
+
return {
|
|
1687
|
+
ok: false,
|
|
1688
|
+
error: `bundled skill not found at ${deps.sourceDir}`
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const base = args.global ? deps.homeDir : resolve(args.cwd ?? deps.projectDir);
|
|
1692
|
+
const target = join5(base, ".claude", "skills", SKILL_NAME);
|
|
1693
|
+
const action = existsSync5(target) ? "updated" : "installed";
|
|
1694
|
+
try {
|
|
1695
|
+
mkdirSync3(target, { recursive: true });
|
|
1696
|
+
cpSync(deps.sourceDir, target, { recursive: true });
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
return { ok: false, error: String(error) };
|
|
1699
|
+
}
|
|
1700
|
+
return { ok: true, action, target };
|
|
1701
|
+
}
|
|
1702
|
+
function runSkillCli(argv) {
|
|
1703
|
+
const parsed = parseSkillArgs(argv);
|
|
1704
|
+
if (parsed.ok === false) {
|
|
1705
|
+
console.error(parsed.error);
|
|
1706
|
+
console.error('Run "code-viewer skill --help" for usage.');
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1709
|
+
if (parsed.args.kind === "help") {
|
|
1710
|
+
console.log(SKILL_HELP);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const result = installSkill(parsed.args, {
|
|
1714
|
+
sourceDir: join5(ROOT, "skills", SKILL_NAME),
|
|
1715
|
+
homeDir: homedir2(),
|
|
1716
|
+
projectDir: process.cwd()
|
|
1717
|
+
});
|
|
1718
|
+
if (result.ok === false) {
|
|
1719
|
+
console.error(result.error);
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
console.log(`${result.action}: ${result.target}`);
|
|
1723
|
+
if (result.action === "installed") {
|
|
1724
|
+
console.log("Re-run the same command anytime to update the skill.");
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
var SKILL_NAME = "code-viewer-annotate", SKILL_HELP;
|
|
1728
|
+
var init_skill_cli = __esm(() => {
|
|
1729
|
+
init_root();
|
|
1730
|
+
SKILL_HELP = `code-viewer skill — manage the bundled agent skill
|
|
1731
|
+
|
|
1732
|
+
Usage:
|
|
1733
|
+
code-viewer skill install [--global] [--cwd <dir>]
|
|
1734
|
+
|
|
1735
|
+
Installs the ${SKILL_NAME} skill (SKILL.md for AI coding agents) into
|
|
1736
|
+
.claude/skills/ of the current project, or into ~/.claude/skills/ with
|
|
1737
|
+
--global. Running install again overwrites the files, so the same command
|
|
1738
|
+
also updates an existing installation.
|
|
1739
|
+
|
|
1740
|
+
Options:
|
|
1741
|
+
--global install into ~/.claude/skills/ instead of the project
|
|
1742
|
+
--cwd <dir> project directory to install into (ignored with --global)
|
|
1743
|
+
`;
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1627
1746
|
// web-src/core/directory-name.ts
|
|
1628
1747
|
function normalizeNewDirectoryName(name) {
|
|
1629
1748
|
if (typeof name !== "string")
|
|
@@ -1650,7 +1769,7 @@ var init_routes = __esm(() => {
|
|
|
1650
1769
|
|
|
1651
1770
|
// web-src/server/cache.ts
|
|
1652
1771
|
import { lstatSync as lstatSync2 } from "node:fs";
|
|
1653
|
-
import { join as
|
|
1772
|
+
import { join as join6 } from "node:path";
|
|
1654
1773
|
function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
|
|
1655
1774
|
return !!cached && now - cached.storedAt <= ttlMs;
|
|
1656
1775
|
}
|
|
@@ -1665,7 +1784,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
|
|
|
1665
1784
|
}
|
|
1666
1785
|
function worktreeFileSignature(path, cwd) {
|
|
1667
1786
|
try {
|
|
1668
|
-
const stats = lstatSync2(
|
|
1787
|
+
const stats = lstatSync2(join6(cwd, path));
|
|
1669
1788
|
const inode = "ino" in stats ? stats.ino : 0;
|
|
1670
1789
|
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
1671
1790
|
} catch {
|
|
@@ -1921,28 +2040,6 @@ function collectLineRangeFromIndexedText(text, index, start, end) {
|
|
|
1921
2040
|
return { lines, total: index.total, complete: end >= index.total };
|
|
1922
2041
|
}
|
|
1923
2042
|
|
|
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
2043
|
// web-src/server/search.ts
|
|
1947
2044
|
function normalizeGrepMax(value) {
|
|
1948
2045
|
const parsed = Number(value || "");
|
|
@@ -2069,7 +2166,7 @@ import {
|
|
|
2069
2166
|
readdirSync as nodeReaddirSync,
|
|
2070
2167
|
watch as nodeWatch
|
|
2071
2168
|
} from "node:fs";
|
|
2072
|
-
import { join as
|
|
2169
|
+
import { join as join7, relative } from "node:path";
|
|
2073
2170
|
function normalizeRelativePath(path) {
|
|
2074
2171
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2075
2172
|
}
|
|
@@ -2152,7 +2249,7 @@ function startWorktreeUpdateWatch(options) {
|
|
|
2152
2249
|
for (const entry of entries) {
|
|
2153
2250
|
if (!entry.isDirectory())
|
|
2154
2251
|
continue;
|
|
2155
|
-
children.push(
|
|
2252
|
+
children.push(join7(dir, entry.name));
|
|
2156
2253
|
}
|
|
2157
2254
|
return children;
|
|
2158
2255
|
};
|
|
@@ -2181,10 +2278,10 @@ function startWorktreeUpdateWatch(options) {
|
|
|
2181
2278
|
scheduleUpdate();
|
|
2182
2279
|
return;
|
|
2183
2280
|
}
|
|
2184
|
-
const changed = normalizeRelativePath(
|
|
2281
|
+
const changed = normalizeRelativePath(join7(rel, filename.toString()));
|
|
2185
2282
|
if (ignored(changed))
|
|
2186
2283
|
return;
|
|
2187
|
-
const fullChangedPath =
|
|
2284
|
+
const fullChangedPath = join7(options.root, changed);
|
|
2188
2285
|
if (!isInsideRoot(options.root, fullChangedPath))
|
|
2189
2286
|
return;
|
|
2190
2287
|
const known = watchers.has(fullChangedPath);
|
|
@@ -2243,9 +2340,9 @@ var exports_preview = {};
|
|
|
2243
2340
|
import {
|
|
2244
2341
|
closeSync,
|
|
2245
2342
|
constants,
|
|
2246
|
-
existsSync as
|
|
2343
|
+
existsSync as existsSync6,
|
|
2247
2344
|
lstatSync as lstatSync4,
|
|
2248
|
-
mkdirSync as
|
|
2345
|
+
mkdirSync as mkdirSync4,
|
|
2249
2346
|
openSync,
|
|
2250
2347
|
readFileSync as readFileSync5,
|
|
2251
2348
|
realpathSync as realpathSync2,
|
|
@@ -2255,8 +2352,8 @@ import {
|
|
|
2255
2352
|
watch,
|
|
2256
2353
|
writeFileSync as writeFileSync3
|
|
2257
2354
|
} from "node:fs";
|
|
2258
|
-
import { homedir as
|
|
2259
|
-
import { basename as basename2, dirname as dirname2, extname, join as
|
|
2355
|
+
import { homedir as homedir3 } from "node:os";
|
|
2356
|
+
import { basename as basename2, dirname as dirname2, extname, join as join8, relative as relative2 } from "node:path";
|
|
2260
2357
|
function parseCli() {
|
|
2261
2358
|
const rest = [];
|
|
2262
2359
|
for (let i = 2;i < process.argv.length; i++) {
|
|
@@ -2395,8 +2492,8 @@ function staticFile(pathname) {
|
|
|
2395
2492
|
const spec = map[pathname];
|
|
2396
2493
|
if (!spec)
|
|
2397
2494
|
return null;
|
|
2398
|
-
const full =
|
|
2399
|
-
if (!
|
|
2495
|
+
const full = join8(WEB_ROOT, spec[0]);
|
|
2496
|
+
if (!existsSync6(full))
|
|
2400
2497
|
return text("not found", 404);
|
|
2401
2498
|
return new Response(readFileSync5(full), {
|
|
2402
2499
|
headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
|
|
@@ -2629,8 +2726,8 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
2629
2726
|
return normalizeScopeExcludeNames(names);
|
|
2630
2727
|
}
|
|
2631
2728
|
function loadProjectConfig() {
|
|
2632
|
-
const full =
|
|
2633
|
-
if (!
|
|
2729
|
+
const full = join8(cwd, ".code-viewer.json");
|
|
2730
|
+
if (!existsSync6(full))
|
|
2634
2731
|
return null;
|
|
2635
2732
|
let realCwd;
|
|
2636
2733
|
let realConfig;
|
|
@@ -2694,8 +2791,8 @@ function safeWorktreePath(path) {
|
|
|
2694
2791
|
return null;
|
|
2695
2792
|
if (isGitInternalPath(path))
|
|
2696
2793
|
return null;
|
|
2697
|
-
const full =
|
|
2698
|
-
if (!
|
|
2794
|
+
const full = join8(cwd, path);
|
|
2795
|
+
if (!existsSync6(full))
|
|
2699
2796
|
return null;
|
|
2700
2797
|
let realCwd;
|
|
2701
2798
|
let realFull;
|
|
@@ -2713,7 +2810,7 @@ function safeWorktreePath(path) {
|
|
|
2713
2810
|
return realFull;
|
|
2714
2811
|
}
|
|
2715
2812
|
function worktreePath(path) {
|
|
2716
|
-
return
|
|
2813
|
+
return join8(cwd, path);
|
|
2717
2814
|
}
|
|
2718
2815
|
function safeOpenWorktreePath(path) {
|
|
2719
2816
|
if (path === "") {
|
|
@@ -3476,10 +3573,10 @@ async function handleUploadFiles(req) {
|
|
|
3476
3573
|
total += file.size;
|
|
3477
3574
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
3478
3575
|
return text("upload too large", 413);
|
|
3479
|
-
const target =
|
|
3576
|
+
const target = join8(realDir, safeName);
|
|
3480
3577
|
if (relative2(realDir, dirname2(target)) !== "")
|
|
3481
3578
|
return text("invalid filename", 400);
|
|
3482
|
-
if (
|
|
3579
|
+
if (existsSync6(target))
|
|
3483
3580
|
return text("file exists", 409);
|
|
3484
3581
|
uploads.push({ file, name: safeName, target });
|
|
3485
3582
|
}
|
|
@@ -3597,11 +3694,11 @@ function triggerUpdate() {
|
|
|
3597
3694
|
sendSse("update");
|
|
3598
3695
|
}
|
|
3599
3696
|
function moveMacPathIntoTrash(path) {
|
|
3600
|
-
const trashDir =
|
|
3697
|
+
const trashDir = join8(homedir3(), ".Trash");
|
|
3601
3698
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
3602
|
-
const target =
|
|
3699
|
+
const target = join8(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
3603
3700
|
try {
|
|
3604
|
-
|
|
3701
|
+
mkdirSync4(trashDir, { recursive: true });
|
|
3605
3702
|
renameSync2(path, target);
|
|
3606
3703
|
return { ok: true, trashPath: target };
|
|
3607
3704
|
} catch (error) {
|
|
@@ -3633,19 +3730,19 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
3633
3730
|
if (!parentFullPath)
|
|
3634
3731
|
return { ok: false, error: "invalid restore target" };
|
|
3635
3732
|
const original = worktreePath(originalPath);
|
|
3636
|
-
if (
|
|
3733
|
+
if (existsSync6(original))
|
|
3637
3734
|
return { ok: false, error: "restore target exists" };
|
|
3638
3735
|
if (trashPath) {
|
|
3639
3736
|
if (process.platform !== "darwin")
|
|
3640
3737
|
return { ok: false, error: "invalid trash handle" };
|
|
3641
|
-
if (!
|
|
3738
|
+
if (!existsSync6(trashPath))
|
|
3642
3739
|
return { ok: false, error: "trash item not found" };
|
|
3643
3740
|
try {
|
|
3644
|
-
const trashRoot =
|
|
3741
|
+
const trashRoot = join8(homedir3(), ".Trash");
|
|
3645
3742
|
const trashRelative = relative2(trashRoot, trashPath);
|
|
3646
3743
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
3647
3744
|
return { ok: false, error: "invalid trash handle" };
|
|
3648
|
-
|
|
3745
|
+
mkdirSync4(dirname2(original), { recursive: true });
|
|
3649
3746
|
renameSync2(trashPath, original);
|
|
3650
3747
|
return { ok: true };
|
|
3651
3748
|
} catch (error) {
|
|
@@ -3791,11 +3888,11 @@ async function handleCreateDirectory(req) {
|
|
|
3791
3888
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
3792
3889
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
3793
3890
|
return text("invalid target", 400);
|
|
3794
|
-
const target =
|
|
3795
|
-
if (
|
|
3891
|
+
const target = join8(parent, name);
|
|
3892
|
+
if (existsSync6(target))
|
|
3796
3893
|
return text("already exists", 409);
|
|
3797
3894
|
try {
|
|
3798
|
-
|
|
3895
|
+
mkdirSync4(target, { recursive: false });
|
|
3799
3896
|
} catch (error) {
|
|
3800
3897
|
if (error.code === "EEXIST")
|
|
3801
3898
|
return text("already exists", 409);
|
|
@@ -3973,8 +4070,8 @@ var init_preview = __esm(async () => {
|
|
|
3973
4070
|
init_search();
|
|
3974
4071
|
init_server_registry();
|
|
3975
4072
|
init_worktree_watcher();
|
|
3976
|
-
WEB_ROOT =
|
|
3977
|
-
VERSION = JSON.parse(readFileSync5(
|
|
4073
|
+
WEB_ROOT = join8(ROOT, "web");
|
|
4074
|
+
VERSION = JSON.parse(readFileSync5(join8(ROOT, "package.json"), "utf8")).version;
|
|
3978
4075
|
DEFAULT_ARGS = ["HEAD"];
|
|
3979
4076
|
WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
|
|
3980
4077
|
LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
@@ -4172,6 +4269,9 @@ data: ok
|
|
|
4172
4269
|
if (process.argv[2] === "annotate") {
|
|
4173
4270
|
const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
|
|
4174
4271
|
await runAnnotateCli2(process.argv.slice(3));
|
|
4272
|
+
} else if (process.argv[2] === "skill") {
|
|
4273
|
+
const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
|
|
4274
|
+
runSkillCli2(process.argv.slice(3));
|
|
4175
4275
|
} else {
|
|
4176
4276
|
await init_preview().then(() => exports_preview);
|
|
4177
4277
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youtyan/code-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
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
|
@@ -532,16 +532,16 @@
|
|
|
532
532
|
};
|
|
533
533
|
deps.jump(item.entryId).then(proceed, proceed);
|
|
534
534
|
}
|
|
535
|
-
function play() {
|
|
535
|
+
function play(fromIndex) {
|
|
536
536
|
if (status === "playing")
|
|
537
537
|
return;
|
|
538
538
|
if (status === "paused") {
|
|
539
|
-
startEntry(index);
|
|
539
|
+
startEntry(fromIndex ?? index);
|
|
540
540
|
return;
|
|
541
541
|
}
|
|
542
542
|
if (deps.items().length === 0)
|
|
543
543
|
return;
|
|
544
|
-
startEntry(0);
|
|
544
|
+
startEntry(fromIndex ?? 0);
|
|
545
545
|
}
|
|
546
546
|
function pause() {
|
|
547
547
|
if (status !== "playing")
|
|
@@ -574,6 +574,11 @@
|
|
|
574
574
|
deps.jump(item.entryId).catch(() => {});
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
|
+
function jumpTo(at) {
|
|
578
|
+
if (status === "idle")
|
|
579
|
+
return;
|
|
580
|
+
moveTo(at);
|
|
581
|
+
}
|
|
577
582
|
function next() {
|
|
578
583
|
if (status === "idle")
|
|
579
584
|
return;
|
|
@@ -603,7 +608,17 @@
|
|
|
603
608
|
rate = value;
|
|
604
609
|
restartCurrentIfPlaying();
|
|
605
610
|
}
|
|
606
|
-
return {
|
|
611
|
+
return {
|
|
612
|
+
play,
|
|
613
|
+
pause,
|
|
614
|
+
stop,
|
|
615
|
+
next,
|
|
616
|
+
prev,
|
|
617
|
+
jumpTo,
|
|
618
|
+
setMuted,
|
|
619
|
+
setRate,
|
|
620
|
+
getState
|
|
621
|
+
};
|
|
607
622
|
}
|
|
608
623
|
|
|
609
624
|
// web-src/core/annotation-speech.ts
|
|
@@ -616,6 +631,7 @@
|
|
|
616
631
|
text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
617
632
|
text = text.replace(/`([^`]*)`/g, "$1");
|
|
618
633
|
text = text.replace(/https?:\/\/\S+/g, "URL");
|
|
634
|
+
text = text.replace(/<([^<>]*)>/g, " $1 ");
|
|
619
635
|
text = text.replace(/^[ \t]*(?:#{1,6}|>|[-*+]|\d+\.)[ \t]+/gm, "");
|
|
620
636
|
text = text.replace(/(\*\*|__|\*|_)/g, "");
|
|
621
637
|
return text.replace(/\s+/g, " ").trim();
|
|
@@ -689,9 +705,17 @@
|
|
|
689
705
|
prevBtn.disabled = state.status === "idle";
|
|
690
706
|
nextBtn.disabled = state.status === "idle";
|
|
691
707
|
}
|
|
708
|
+
let selfJumping = false;
|
|
692
709
|
const core = createAnnotationPlayerCore({
|
|
693
710
|
items,
|
|
694
|
-
jump: (entryId) =>
|
|
711
|
+
jump: async (entryId) => {
|
|
712
|
+
selfJumping = true;
|
|
713
|
+
try {
|
|
714
|
+
await deps.openAnnotationEntry(entryId);
|
|
715
|
+
} finally {
|
|
716
|
+
selfJumping = false;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
695
719
|
speak,
|
|
696
720
|
schedule: (ms, cb) => {
|
|
697
721
|
const id = window.setTimeout(cb, ms);
|
|
@@ -713,13 +737,23 @@
|
|
|
713
737
|
rateSel.disabled = true;
|
|
714
738
|
core.setMuted(true);
|
|
715
739
|
}
|
|
740
|
+
function entryIndexOf(entryId) {
|
|
741
|
+
if (!entryId)
|
|
742
|
+
return -1;
|
|
743
|
+
return deps.getActiveSessionEntries().findIndex((entry) => entry.id === entryId);
|
|
744
|
+
}
|
|
716
745
|
toggleBtn.addEventListener("click", () => {
|
|
717
746
|
const state = core.getState();
|
|
718
747
|
if (state.status === "playing") {
|
|
719
748
|
core.pause();
|
|
720
749
|
} else {
|
|
721
750
|
deps.setAnnotationPanelOpen(true);
|
|
722
|
-
|
|
751
|
+
if (state.status === "idle") {
|
|
752
|
+
const activeIndex = entryIndexOf(deps.getActiveAnnotationId());
|
|
753
|
+
core.play(activeIndex >= 0 ? activeIndex : 0);
|
|
754
|
+
} else {
|
|
755
|
+
core.play();
|
|
756
|
+
}
|
|
723
757
|
}
|
|
724
758
|
});
|
|
725
759
|
prevBtn.addEventListener("click", () => core.prev());
|
|
@@ -743,6 +777,15 @@
|
|
|
743
777
|
core.stop();
|
|
744
778
|
syncVisibility();
|
|
745
779
|
});
|
|
780
|
+
deps.onAnnotationOpened((entryId) => {
|
|
781
|
+
if (selfJumping)
|
|
782
|
+
return;
|
|
783
|
+
if (core.getState().status === "idle")
|
|
784
|
+
return;
|
|
785
|
+
const index = entryIndexOf(entryId);
|
|
786
|
+
if (index >= 0)
|
|
787
|
+
core.jumpTo(index);
|
|
788
|
+
});
|
|
746
789
|
syncVisibility();
|
|
747
790
|
render(core.getState());
|
|
748
791
|
return { stop: () => core.stop(), syncVisibility };
|
|
@@ -6970,6 +7013,11 @@ ${frontmatter.yaml}
|
|
|
6970
7013
|
for (const cb of annotationsChangedCallbacks)
|
|
6971
7014
|
cb();
|
|
6972
7015
|
}
|
|
7016
|
+
const annotationOpenedCallbacks = [];
|
|
7017
|
+
function notifyAnnotationOpened(entryId) {
|
|
7018
|
+
for (const cb of annotationOpenedCallbacks)
|
|
7019
|
+
cb(entryId);
|
|
7020
|
+
}
|
|
6973
7021
|
let mdHighlighter = null;
|
|
6974
7022
|
let mdHighlighterRequested = false;
|
|
6975
7023
|
function ensureMarkdownHighlighter() {
|
|
@@ -7475,6 +7523,7 @@ ${frontmatter.yaml}
|
|
|
7475
7523
|
activeSessionId = session.id;
|
|
7476
7524
|
if (sessionChanged)
|
|
7477
7525
|
notifyAnnotationsChanged();
|
|
7526
|
+
notifyAnnotationOpened(entry.id);
|
|
7478
7527
|
showAnnotationDetail(session, entry, index);
|
|
7479
7528
|
const from = entry.range.from || "HEAD";
|
|
7480
7529
|
const to = entry.range.to || "worktree";
|
|
@@ -7595,6 +7644,12 @@ ${frontmatter.yaml}
|
|
|
7595
7644
|
},
|
|
7596
7645
|
onAnnotationsChanged(cb) {
|
|
7597
7646
|
annotationsChangedCallbacks.push(cb);
|
|
7647
|
+
},
|
|
7648
|
+
onAnnotationOpened(cb) {
|
|
7649
|
+
annotationOpenedCallbacks.push(cb);
|
|
7650
|
+
},
|
|
7651
|
+
getActiveAnnotationId() {
|
|
7652
|
+
return activeAnnotationId;
|
|
7598
7653
|
}
|
|
7599
7654
|
};
|
|
7600
7655
|
}
|
|
@@ -15874,7 +15929,9 @@ ${frontmatter.yaml}
|
|
|
15874
15929
|
getActiveSessionEntries: () => ANNOTATIONS_UI?.getActiveSessionEntries() ?? [],
|
|
15875
15930
|
openAnnotationEntry: (id) => ANNOTATIONS_UI ? ANNOTATIONS_UI.openAnnotationEntry(id) : Promise.resolve(),
|
|
15876
15931
|
setAnnotationPanelOpen: (open) => ANNOTATIONS_UI?.setAnnotationPanelOpen(open),
|
|
15877
|
-
onAnnotationsChanged: (cb) => ANNOTATIONS_UI?.onAnnotationsChanged(cb)
|
|
15932
|
+
onAnnotationsChanged: (cb) => ANNOTATIONS_UI?.onAnnotationsChanged(cb),
|
|
15933
|
+
onAnnotationOpened: (cb) => ANNOTATIONS_UI?.onAnnotationOpened(cb),
|
|
15934
|
+
getActiveAnnotationId: () => ANNOTATIONS_UI ? ANNOTATIONS_UI.getActiveAnnotationId() : null
|
|
15878
15935
|
});
|
|
15879
15936
|
let sseTimer = null;
|
|
15880
15937
|
function scheduleSseLoad() {
|