@youtyan/code-viewer 0.1.34 → 0.1.36
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 +11 -6
- package/dist/code-viewer.js +235 -4
- package/package.json +2 -3
- package/web/app.js +14359 -13242
- package/web/index.html +20 -5
- package/web/style.css +203 -14
package/README.md
CHANGED
|
@@ -122,7 +122,9 @@ code-viewer annotate add --file web-src/app.ts --line 9650 \
|
|
|
122
122
|
|
|
123
123
|
The body is Markdown. Long bodies can be passed with `--body-file <path>` or
|
|
124
124
|
piped through stdin. `code-viewer annotate --help` shows all commands,
|
|
125
|
-
including `list`, `delete <id>`, and `clear`.
|
|
125
|
+
including `list`, `delete <id>`, and `clear`. For AI agents,
|
|
126
|
+
`code-viewer annotate agent-help` prints a skill-style guide covering the
|
|
127
|
+
workflow, conventions, and pitfalls for writing good walkthroughs.
|
|
126
128
|
|
|
127
129
|
Annotations are grouped into sessions and persisted in
|
|
128
130
|
`.code-viewer/annotations.json` at the repository root, so the walkthrough
|
|
@@ -138,11 +140,14 @@ location, and entries and sessions can be deleted there too. The "follow"
|
|
|
138
140
|
checkbox controls whether the tab jumps automatically when an agent adds a
|
|
139
141
|
new annotation.
|
|
140
142
|
|
|
141
|
-
The `annotate` subcommand
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
The `annotate` subcommand talks to the running server for the repository
|
|
144
|
+
(discovered via `~/.cache/code-viewer/servers/`); start one with
|
|
145
|
+
`code-viewer` first. Pass `--cwd <repo>` when annotating a repository other
|
|
146
|
+
than the current directory, or `--server <url>` to target a specific server.
|
|
147
|
+
|
|
148
|
+
`add` appends to the most recent session (creating one when none exists);
|
|
149
|
+
run `annotate start` again to begin a new session, or pass `--session <id>`
|
|
150
|
+
to target a specific one.
|
|
146
151
|
|
|
147
152
|
## Development
|
|
148
153
|
|
package/dist/code-viewer.js
CHANGED
|
@@ -197,6 +197,47 @@ function addAnnotationEntry(state, input, now, makeId = makeAnnotationId) {
|
|
|
197
197
|
created_session: createdSession
|
|
198
198
|
};
|
|
199
199
|
}
|
|
200
|
+
function renameAnnotationSession(state, id, title) {
|
|
201
|
+
const session = state.sessions.find((s) => s.id === id);
|
|
202
|
+
if (!session)
|
|
203
|
+
return { state, renamed: false };
|
|
204
|
+
const next = title.trim().slice(0, ANNOTATION_TITLE_MAX_CHARS) || "Untitled session";
|
|
205
|
+
return {
|
|
206
|
+
state: {
|
|
207
|
+
version: 1,
|
|
208
|
+
sessions: state.sessions.map((s) => s.id === id ? { ...s, title: next } : s)
|
|
209
|
+
},
|
|
210
|
+
renamed: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function updateAnnotationEntry(state, id, patch) {
|
|
214
|
+
const session = state.sessions.find((s) => s.entries.some((e) => e.id === id));
|
|
215
|
+
if (!session)
|
|
216
|
+
return { ok: false, error: "annotation not found" };
|
|
217
|
+
if (patch.body !== undefined) {
|
|
218
|
+
if (!patch.body.trim())
|
|
219
|
+
return { ok: false, error: "body is required" };
|
|
220
|
+
if (Buffer.byteLength(patch.body, "utf8") > ANNOTATION_BODY_MAX_BYTES)
|
|
221
|
+
return { ok: false, error: "body is too large" };
|
|
222
|
+
}
|
|
223
|
+
let updated = null;
|
|
224
|
+
const sessions = state.sessions.map((s) => s.id === session.id ? {
|
|
225
|
+
...s,
|
|
226
|
+
entries: s.entries.map((e) => {
|
|
227
|
+
if (e.id !== id)
|
|
228
|
+
return e;
|
|
229
|
+
updated = {
|
|
230
|
+
...e,
|
|
231
|
+
...patch.title !== undefined ? { title: patch.title.trim() || undefined } : {},
|
|
232
|
+
...patch.body !== undefined ? { body: patch.body } : {}
|
|
233
|
+
};
|
|
234
|
+
return updated;
|
|
235
|
+
})
|
|
236
|
+
} : s);
|
|
237
|
+
if (!updated)
|
|
238
|
+
return { ok: false, error: "annotation not found" };
|
|
239
|
+
return { ok: true, state: { version: 1, sessions }, entry: updated };
|
|
240
|
+
}
|
|
200
241
|
function deleteAnnotationById(state, id) {
|
|
201
242
|
for (const session of state.sessions) {
|
|
202
243
|
if (session.id === id) {
|
|
@@ -1106,7 +1147,8 @@ var exports_annotate_cli = {};
|
|
|
1106
1147
|
__export(exports_annotate_cli, {
|
|
1107
1148
|
runAnnotateCli: () => runAnnotateCli,
|
|
1108
1149
|
parseAnnotateArgs: () => parseAnnotateArgs,
|
|
1109
|
-
ANNOTATE_HELP: () => ANNOTATE_HELP
|
|
1150
|
+
ANNOTATE_HELP: () => ANNOTATE_HELP,
|
|
1151
|
+
ANNOTATE_AGENT_HELP: () => ANNOTATE_AGENT_HELP
|
|
1110
1152
|
});
|
|
1111
1153
|
import { readFileSync as readFileSync4, realpathSync } from "node:fs";
|
|
1112
1154
|
function takeValue(argv, index, flag) {
|
|
@@ -1162,6 +1204,9 @@ function parseAnnotateArgs(argv) {
|
|
|
1162
1204
|
const subcommand = rest[0];
|
|
1163
1205
|
if (!subcommand)
|
|
1164
1206
|
return { ok: true, args: { command: { kind: "help" } } };
|
|
1207
|
+
if (subcommand === "agent-help") {
|
|
1208
|
+
return { ok: true, args: { command: { kind: "agent-help" } } };
|
|
1209
|
+
}
|
|
1165
1210
|
if (subcommand === "start") {
|
|
1166
1211
|
return {
|
|
1167
1212
|
ok: true,
|
|
@@ -1207,6 +1252,41 @@ function parseAnnotateArgs(argv) {
|
|
|
1207
1252
|
}
|
|
1208
1253
|
};
|
|
1209
1254
|
}
|
|
1255
|
+
if (subcommand === "rename") {
|
|
1256
|
+
const id = rest[1];
|
|
1257
|
+
if (!id)
|
|
1258
|
+
return { ok: false, error: "rename requires a session id" };
|
|
1259
|
+
const title = options.get("--title");
|
|
1260
|
+
if (!title)
|
|
1261
|
+
return { ok: false, error: "rename requires --title <text>" };
|
|
1262
|
+
return {
|
|
1263
|
+
ok: true,
|
|
1264
|
+
args: { command: { kind: "rename", id, title }, cwd, server }
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
if (subcommand === "edit") {
|
|
1268
|
+
const id = rest[1];
|
|
1269
|
+
if (!id)
|
|
1270
|
+
return { ok: false, error: "edit requires an annotation id" };
|
|
1271
|
+
const body = options.get("--body");
|
|
1272
|
+
const bodyFile = options.get("--body-file");
|
|
1273
|
+
if (body !== undefined && bodyFile !== undefined)
|
|
1274
|
+
return { ok: false, error: "use either --body or --body-file" };
|
|
1275
|
+
return {
|
|
1276
|
+
ok: true,
|
|
1277
|
+
args: {
|
|
1278
|
+
command: {
|
|
1279
|
+
kind: "edit",
|
|
1280
|
+
id,
|
|
1281
|
+
title: options.get("--title"),
|
|
1282
|
+
body,
|
|
1283
|
+
bodyFile
|
|
1284
|
+
},
|
|
1285
|
+
cwd,
|
|
1286
|
+
server
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1210
1290
|
if (subcommand === "list") {
|
|
1211
1291
|
return {
|
|
1212
1292
|
ok: true,
|
|
@@ -1331,6 +1411,10 @@ async function runAnnotateCli(argv) {
|
|
|
1331
1411
|
console.log(ANNOTATE_HELP);
|
|
1332
1412
|
return;
|
|
1333
1413
|
}
|
|
1414
|
+
if (command.kind === "agent-help") {
|
|
1415
|
+
console.log(ANNOTATE_AGENT_HELP);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1334
1418
|
const root = resolveRepoRoot(cwd);
|
|
1335
1419
|
const serverUrl = await ensureServerUrl(root, server);
|
|
1336
1420
|
if (command.kind === "start") {
|
|
@@ -1383,6 +1467,37 @@ async function runAnnotateCli(argv) {
|
|
|
1383
1467
|
printList(state);
|
|
1384
1468
|
return;
|
|
1385
1469
|
}
|
|
1470
|
+
if (command.kind === "rename") {
|
|
1471
|
+
await request(serverUrl, "POST", {
|
|
1472
|
+
action: "rename",
|
|
1473
|
+
id: command.id,
|
|
1474
|
+
title: command.title
|
|
1475
|
+
});
|
|
1476
|
+
console.log(`renamed session ${command.id} to "${command.title}"`);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (command.kind === "edit") {
|
|
1480
|
+
let bodyText = command.body;
|
|
1481
|
+
if (command.bodyFile !== undefined)
|
|
1482
|
+
bodyText = readFileSync4(command.bodyFile, "utf8");
|
|
1483
|
+
if (bodyText === undefined) {
|
|
1484
|
+
const stdin = await readStdin();
|
|
1485
|
+
if (stdin.trim())
|
|
1486
|
+
bodyText = stdin;
|
|
1487
|
+
}
|
|
1488
|
+
if (bodyText === undefined && command.title === undefined) {
|
|
1489
|
+
console.error("edit requires --title, --body, --body-file, or stdin");
|
|
1490
|
+
process.exit(1);
|
|
1491
|
+
}
|
|
1492
|
+
const result = await request(serverUrl, "POST", {
|
|
1493
|
+
action: "update",
|
|
1494
|
+
id: command.id,
|
|
1495
|
+
title: command.title,
|
|
1496
|
+
body: bodyText
|
|
1497
|
+
});
|
|
1498
|
+
console.log(`updated annotation ${result.entry.id} (${result.entry.path}${formatLine(result.entry.line)})`);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1386
1501
|
if (command.kind === "delete") {
|
|
1387
1502
|
const result = await request(serverUrl, "POST", {
|
|
1388
1503
|
action: "delete",
|
|
@@ -1405,11 +1520,17 @@ in <repo>/.code-viewer/annotations.json. A running code-viewer server for
|
|
|
1405
1520
|
the repository is required: start one with "code-viewer" before using
|
|
1406
1521
|
annotate (or point at one explicitly with --server).
|
|
1407
1522
|
|
|
1523
|
+
Run "code-viewer annotate agent-help" for an AI-agent oriented guide
|
|
1524
|
+
(workflow, conventions, and pitfalls for writing good walkthroughs).
|
|
1525
|
+
|
|
1408
1526
|
Usage:
|
|
1409
1527
|
code-viewer annotate start [--title <text>]
|
|
1410
1528
|
code-viewer annotate add --file <path> [--line <n>|<n>-<m>]
|
|
1411
1529
|
[--from <ref>] [--to <ref>] [--title <text>] [--session <id>]
|
|
1412
1530
|
[--body <markdown> | --body-file <path>] (or pipe body via stdin)
|
|
1531
|
+
code-viewer annotate rename <session-id> --title <text>
|
|
1532
|
+
code-viewer annotate edit <id> [--title <text>]
|
|
1533
|
+
[--body <markdown> | --body-file <path>] (or pipe body via stdin)
|
|
1413
1534
|
code-viewer annotate list [--json]
|
|
1414
1535
|
code-viewer annotate delete <id>
|
|
1415
1536
|
code-viewer annotate clear
|
|
@@ -1424,6 +1545,78 @@ Examples:
|
|
|
1424
1545
|
--body "This endpoint keeps one SSE stream per browser tab."
|
|
1425
1546
|
git diff HEAD~1 | code-viewer annotate add --file src/app.ts --line 10 \\
|
|
1426
1547
|
--from HEAD~1 --to worktree --body "The fix moves the guard up here."
|
|
1548
|
+
`, ANNOTATE_AGENT_HELP = `code-viewer annotate — agent guide
|
|
1549
|
+
|
|
1550
|
+
You are an AI coding agent. Use this tool to walk a human through code in
|
|
1551
|
+
their browser: each annotation jumps every open code-viewer tab to a file
|
|
1552
|
+
location and renders your explanation directly under the annotated lines.
|
|
1553
|
+
|
|
1554
|
+
## When to use
|
|
1555
|
+
|
|
1556
|
+
- Explaining a change you just made (per-file, per-hunk commentary)
|
|
1557
|
+
- Guiding a code review: point at the risky lines, in reading order
|
|
1558
|
+
- Onboarding walkthroughs: "how does feature X flow through the code"
|
|
1559
|
+
|
|
1560
|
+
## Requirements
|
|
1561
|
+
|
|
1562
|
+
- A code-viewer server must already be running for the repository
|
|
1563
|
+
(the human starts it with: code-viewer). This command never starts one.
|
|
1564
|
+
- Run from inside the repository, or pass --cwd <repo>.
|
|
1565
|
+
- If "code-viewer" is not on PATH (e.g. the human runs it via npx), invoke
|
|
1566
|
+
every command below as: npx -y @youtyan/code-viewer annotate ...
|
|
1567
|
+
|
|
1568
|
+
## Workflow
|
|
1569
|
+
|
|
1570
|
+
1. Start a session per walkthrough topic. The title is shown to the human:
|
|
1571
|
+
code-viewer annotate start --title "How the cache invalidation works"
|
|
1572
|
+
2. Add annotations in READING ORDER (the order you want the human to
|
|
1573
|
+
follow). Each add without --session appends to the most recent session:
|
|
1574
|
+
code-viewer annotate add --file src/cache.ts --line 120-145 \\
|
|
1575
|
+
--title "Entry point" --body "Writes land here first. ..."
|
|
1576
|
+
3. Verify what you posted:
|
|
1577
|
+
code-viewer annotate list
|
|
1578
|
+
|
|
1579
|
+
## Conventions for good walkthroughs
|
|
1580
|
+
|
|
1581
|
+
- One idea per annotation. Prefer 5-10 focused annotations over 2 huge ones.
|
|
1582
|
+
- Always pass --line. Use the smallest range that covers the idea; the
|
|
1583
|
+
body is rendered inline directly under the LAST line of the range.
|
|
1584
|
+
- Line numbers must match the "to" side of the range (default: the current
|
|
1585
|
+
worktree state of the file). When annotating a diff against another ref,
|
|
1586
|
+
pass --from/--to and use NEW-side line numbers.
|
|
1587
|
+
- The body is Markdown. Code spans, fenced blocks, and links work. Long
|
|
1588
|
+
bodies: use --body-file <path> or pipe via stdin instead of --body.
|
|
1589
|
+
- Give every annotation a short --title; it becomes the inline heading.
|
|
1590
|
+
- Annotating unchanged code is fine: the viewer auto-expands diff context
|
|
1591
|
+
or falls back to the full source view.
|
|
1592
|
+
|
|
1593
|
+
## Sessions
|
|
1594
|
+
|
|
1595
|
+
- add (no --session) → appends to the most recent session.
|
|
1596
|
+
- annotate start → begins a NEW session; later adds go there.
|
|
1597
|
+
- add --session <id> → targets a specific session (ids: annotate list).
|
|
1598
|
+
- The human can share a walkthrough as a URL; one session = one shareable
|
|
1599
|
+
walkthrough. Do not mix unrelated topics in one session.
|
|
1600
|
+
|
|
1601
|
+
## Fixing mistakes and follow-ups
|
|
1602
|
+
|
|
1603
|
+
- The human may paste a reference block copied from the viewer that starts
|
|
1604
|
+
with "code-viewer のコード注釈について依頼があります" and lists the
|
|
1605
|
+
annotation id, location, and session, followed by their question.
|
|
1606
|
+
Read the current body first: code-viewer annotate list --json
|
|
1607
|
+
- Revise a wrong annotation IN PLACE (do not delete + re-add; the id and
|
|
1608
|
+
its position in the walkthrough are preserved):
|
|
1609
|
+
code-viewer annotate edit <id> --body "<corrected markdown>"
|
|
1610
|
+
(long bodies: --body-file <path> or pipe via stdin; --title also works)
|
|
1611
|
+
- Post a follow-up answer next to the original instead of replacing it:
|
|
1612
|
+
code-viewer annotate add --session <session-id> --file <path> --line <n> --title "回答: ..." --body "<markdown>"
|
|
1613
|
+
- Rename a session: code-viewer annotate rename <session-id> --title <text>
|
|
1614
|
+
|
|
1615
|
+
## Cleanup
|
|
1616
|
+
|
|
1617
|
+
- delete <id> removes one annotation or a whole session by its id.
|
|
1618
|
+
- clear removes everything. Ask the human before clearing state you did
|
|
1619
|
+
not create.
|
|
1427
1620
|
`;
|
|
1428
1621
|
var init_annotate_cli = __esm(() => {
|
|
1429
1622
|
init_annotations();
|
|
@@ -1431,7 +1624,7 @@ var init_annotate_cli = __esm(() => {
|
|
|
1431
1624
|
init_server_registry();
|
|
1432
1625
|
});
|
|
1433
1626
|
|
|
1434
|
-
// web-src/directory-name.ts
|
|
1627
|
+
// web-src/core/directory-name.ts
|
|
1435
1628
|
function normalizeNewDirectoryName(name) {
|
|
1436
1629
|
if (typeof name !== "string")
|
|
1437
1630
|
return null;
|
|
@@ -1448,7 +1641,7 @@ function normalizeNewDirectoryName(name) {
|
|
|
1448
1641
|
return trimmed;
|
|
1449
1642
|
}
|
|
1450
1643
|
|
|
1451
|
-
// web-src/routes.ts
|
|
1644
|
+
// web-src/core/routes.ts
|
|
1452
1645
|
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
1453
1646
|
var init_routes = __esm(() => {
|
|
1454
1647
|
SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
|
|
@@ -3715,6 +3908,32 @@ async function handleAnnotations(req) {
|
|
|
3715
3908
|
}
|
|
3716
3909
|
return json({ ok: true, removed: result.removed });
|
|
3717
3910
|
}
|
|
3911
|
+
if (action === "rename") {
|
|
3912
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
3913
|
+
const title = typeof body.title === "string" ? body.title : "";
|
|
3914
|
+
if (!id)
|
|
3915
|
+
return text("invalid id", 400);
|
|
3916
|
+
const result = renameAnnotationSession(loadAnnotationsState(cwd), id, title);
|
|
3917
|
+
if (!result.renamed)
|
|
3918
|
+
return text("session not found", 404);
|
|
3919
|
+
saveAnnotationsState(cwd, result.state);
|
|
3920
|
+
annotationSse("update", id);
|
|
3921
|
+
return json({ ok: true });
|
|
3922
|
+
}
|
|
3923
|
+
if (action === "update") {
|
|
3924
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
3925
|
+
if (!id)
|
|
3926
|
+
return text("invalid id", 400);
|
|
3927
|
+
const result = updateAnnotationEntry(loadAnnotationsState(cwd), id, {
|
|
3928
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
3929
|
+
body: typeof body.body === "string" ? body.body : undefined
|
|
3930
|
+
});
|
|
3931
|
+
if (result.ok === false)
|
|
3932
|
+
return text(result.error, 400);
|
|
3933
|
+
saveAnnotationsState(cwd, result.state);
|
|
3934
|
+
annotationSse("update", undefined, id);
|
|
3935
|
+
return json({ ok: true, entry: result.entry });
|
|
3936
|
+
}
|
|
3718
3937
|
if (action === "clear") {
|
|
3719
3938
|
saveAnnotationsState(cwd, emptyAnnotationsState());
|
|
3720
3939
|
annotationSse("clear");
|
|
@@ -3908,12 +4127,24 @@ data: ok
|
|
|
3908
4127
|
started_at: new Date().toISOString()
|
|
3909
4128
|
});
|
|
3910
4129
|
process.on("exit", () => removeServerRegistry(cwd, process.pid));
|
|
3911
|
-
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
4130
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
3912
4131
|
process.on(signal, () => {
|
|
3913
4132
|
removeServerRegistry(cwd, process.pid);
|
|
3914
4133
|
process.exit(0);
|
|
3915
4134
|
});
|
|
3916
4135
|
}
|
|
4136
|
+
if (process.env.CODE_VIEWER_DEV === "1") {
|
|
4137
|
+
const parentPid = process.ppid;
|
|
4138
|
+
setInterval(() => {
|
|
4139
|
+
try {
|
|
4140
|
+
process.kill(parentPid, 0);
|
|
4141
|
+
} catch {
|
|
4142
|
+
console.log("dev wrapper exited; shutting down preview server");
|
|
4143
|
+
removeServerRegistry(cwd, process.pid);
|
|
4144
|
+
process.exit(0);
|
|
4145
|
+
}
|
|
4146
|
+
}, 1000).unref();
|
|
4147
|
+
}
|
|
3917
4148
|
startDevAssetReload({
|
|
3918
4149
|
enabled: process.env.CODE_VIEWER_DEV === "1",
|
|
3919
4150
|
webRoot: WEB_ROOT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youtyan/code-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Local browser-based code and git diff viewer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,6 +65,5 @@
|
|
|
65
65
|
},
|
|
66
66
|
"engines": {
|
|
67
67
|
"node": ">=20.0.0"
|
|
68
|
-
}
|
|
69
|
-
"dependencies": {}
|
|
68
|
+
}
|
|
70
69
|
}
|