@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 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 reuses the running server for the repository when
142
- one exists (discovered via `~/.cache/code-viewer/servers/`) and starts one
143
- automatically otherwise, printing its URL to stderr. Pass `--cwd <repo>` when
144
- annotating a repository other than the current directory, or `--server <url>`
145
- to target a specific server.
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
 
@@ -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.34",
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
  }