@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 +15 -0
- package/dist/code-viewer.js +204 -53
- package/package.json +3 -2
- package/skills/code-viewer-annotate/SKILL.md +48 -0
- package/web/app.js +30 -10
- package/web/style.css +2 -1
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
|
package/dist/code-viewer.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
2332
|
+
const changed = normalizeRelativePath(join7(rel, filename.toString()));
|
|
2185
2333
|
if (ignored(changed))
|
|
2186
2334
|
return;
|
|
2187
|
-
const fullChangedPath =
|
|
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
|
|
2394
|
+
existsSync as existsSync6,
|
|
2247
2395
|
lstatSync as lstatSync4,
|
|
2248
|
-
mkdirSync as
|
|
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
|
|
2259
|
-
import { basename as basename2, dirname as dirname2, extname, join as
|
|
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 =
|
|
2399
|
-
if (!
|
|
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 =
|
|
2633
|
-
if (!
|
|
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 =
|
|
2698
|
-
if (!
|
|
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
|
|
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 =
|
|
3627
|
+
const target = join8(realDir, safeName);
|
|
3480
3628
|
if (relative2(realDir, dirname2(target)) !== "")
|
|
3481
3629
|
return text("invalid filename", 400);
|
|
3482
|
-
if (
|
|
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 =
|
|
3748
|
+
const trashDir = join8(homedir3(), ".Trash");
|
|
3601
3749
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
3602
|
-
const target =
|
|
3750
|
+
const target = join8(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
3603
3751
|
try {
|
|
3604
|
-
|
|
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 (
|
|
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 (!
|
|
3789
|
+
if (!existsSync6(trashPath))
|
|
3642
3790
|
return { ok: false, error: "trash item not found" };
|
|
3643
3791
|
try {
|
|
3644
|
-
const trashRoot =
|
|
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
|
-
|
|
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 =
|
|
3795
|
-
if (
|
|
3942
|
+
const target = join8(parent, name);
|
|
3943
|
+
if (existsSync6(target))
|
|
3796
3944
|
return text("already exists", 409);
|
|
3797
3945
|
try {
|
|
3798
|
-
|
|
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 =
|
|
3977
|
-
VERSION = JSON.parse(readFileSync5(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
const
|
|
7581
|
-
if (
|
|
7582
|
-
openAnnotationEntry(
|
|
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
|
-
|
|
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;
|