diffing 0.1.2 → 0.2.1

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.
Files changed (95) hide show
  1. package/README.md +103 -7
  2. package/dist/cli-agent-BLPX3oOP.mjs +443 -0
  3. package/dist/cli-agent-BNwY5lZE.mjs +446 -0
  4. package/dist/cli-agent-CbzXDSSF.mjs +446 -0
  5. package/dist/cli-agent-DsqbS3n7.mjs +443 -0
  6. package/dist/cli-agent-Vbs-ePHv.mjs +443 -0
  7. package/dist/cli-agent-ZAkixf4c.mjs +446 -0
  8. package/dist/cli.mjs +954 -36
  9. package/dist/client/assets/{angular-html-BABKYppC.js → angular-html-DsssUIMZ.js} +1 -1
  10. package/dist/client/assets/{angular-ts-CtXLuOQb.js → angular-ts-dTg-sJ77.js} +1 -1
  11. package/dist/client/assets/{apl-DOi69evI.js → apl-C1yUKbqz.js} +1 -1
  12. package/dist/client/assets/{astro-J8Em2tfW.js → astro-BI07ha2y.js} +1 -1
  13. package/dist/client/assets/{blade-B_tOnqRD.js → blade-DkzUGffS.js} +1 -1
  14. package/dist/client/assets/{c-C0tRZWSi.js → c-KkuqqOvW.js} +1 -1
  15. package/dist/client/assets/{cobol-Dj_rkgIY.js → cobol-DRh7-jLi.js} +1 -1
  16. package/dist/client/assets/{coffee-C6Vq4BFF.js → coffee-wR_ZAh9a.js} +1 -1
  17. package/dist/client/assets/{cpp-Cno48fgF.js → cpp-OD_qyywn.js} +1 -1
  18. package/dist/client/assets/{crystal-C7tmDnU1.js → crystal-E4ahAddE.js} +1 -1
  19. package/dist/client/assets/{css-0v4o-iNQ.js → css-2NgE_uxz.js} +1 -1
  20. package/dist/client/assets/{edge-CvULxRem.js → edge-8mbscxxD.js} +1 -1
  21. package/dist/client/assets/{elixir-vRTauEw-.js → elixir-CJv1b3Q6.js} +1 -1
  22. package/dist/client/assets/{elm-fLFTu1xn.js → elm-DUPzqZ9H.js} +1 -1
  23. package/dist/client/assets/{erb-CtItcikB.js → erb-ssvEF26z.js} +1 -1
  24. package/dist/client/assets/{git-rebase-5MeRBXZD.js → git-rebase-DiWIK1Oa.js} +1 -1
  25. package/dist/client/assets/{glimmer-js-B7yA-u8_.js → glimmer-js-DrHg6AuA.js} +1 -1
  26. package/dist/client/assets/{glimmer-ts-B2g1OaHc.js → glimmer-ts-wQSGOa8A.js} +1 -1
  27. package/dist/client/assets/{glsl-CqNPWp5Y.js → glsl-OY27QGY0.js} +1 -1
  28. package/dist/client/assets/{graphql-CF78KbfF.js → graphql-CSWSqe6f.js} +1 -1
  29. package/dist/client/assets/{hack-Dz8fQ8O9.js → hack-Bi1v624Q.js} +1 -1
  30. package/dist/client/assets/{haml-3Gbl257r.js → haml-CX94zV9m.js} +1 -1
  31. package/dist/client/assets/{handlebars-5UZnh5L7.js → handlebars-C1j6wnjG.js} +1 -1
  32. package/dist/client/assets/{html-JOaQiIkL.js → html-BcAjMlB7.js} +1 -1
  33. package/dist/client/assets/{html-derivative-GyX6oYy3.js → html-derivative-DcEpOzdx.js} +1 -1
  34. package/dist/client/assets/{http-t6uVAxgS.js → http-BBxxSc6S.js} +1 -1
  35. package/dist/client/assets/{hurl-OLQZpg0E.js → hurl-CpKRNTz4.js} +1 -1
  36. package/dist/client/assets/{index-CghVgrAF.js → index-Bwv7BWUp.js} +181 -78
  37. package/dist/client/assets/index-CoZyJYkw.css +1 -0
  38. package/dist/client/assets/{java-BqL_yaya.js → java-CFUlLvAA.js} +1 -1
  39. package/dist/client/assets/{javascript-CxBNAxR5.js → javascript-C-HnT3sE.js} +1 -1
  40. package/dist/client/assets/{jinja-CuaK033Z.js → jinja-CPU8RwNP.js} +1 -1
  41. package/dist/client/assets/{jison-aL_w2vvo.js → jison-CW0TBQOs.js} +1 -1
  42. package/dist/client/assets/{json-Byruj-EC.js → json-qUjlNuEJ.js} +1 -1
  43. package/dist/client/assets/{jsx-CosY06uG.js → jsx-KWeo60kf.js} +1 -1
  44. package/dist/client/assets/{julia-u1y7fV63.js → julia-CnJB8y2e.js} +1 -1
  45. package/dist/client/assets/{just-DS4T9P_K.js → just-Do22d9PP.js} +1 -1
  46. package/dist/client/assets/{latex-C9c2IWW4.js → latex-CCPgzvKn.js} +1 -1
  47. package/dist/client/assets/{liquid-DcXQgkEV.js → liquid-B9btdZoI.js} +1 -1
  48. package/dist/client/assets/{lua-DWe0hUn1.js → lua-IFRifurW.js} +1 -1
  49. package/dist/client/assets/{marko-CIW7o2up.js → marko-CCaVBFq1.js} +1 -1
  50. package/dist/client/assets/{mdc-DKjGzb4k.js → mdc-BtbnOLUc.js} +1 -1
  51. package/dist/client/assets/{nginx-CBXzDlkk.js → nginx-BkbFZHVt.js} +1 -1
  52. package/dist/client/assets/{nim-CBf_vdee.js → nim-2uCd5j65.js} +1 -1
  53. package/dist/client/assets/{perl-xC9qc39I.js → perl-C4ixqSNT.js} +1 -1
  54. package/dist/client/assets/{php-n4vnn5oz.js → php-DJ8WOtcI.js} +1 -1
  55. package/dist/client/assets/{pug-Db536lA_.js → pug-BOaafhT5.js} +1 -1
  56. package/dist/client/assets/{qml-CrATLnNN.js → qml-CURE-1y4.js} +1 -1
  57. package/dist/client/assets/{r-DBQ-dydl.js → r-CPWmgzo3.js} +1 -1
  58. package/dist/client/assets/{razor-C_wvK1iP.js → razor-DH83VP_d.js} +1 -1
  59. package/dist/client/assets/{regexp-DowFj0Xd.js → regexp-BXVWi-t_.js} +1 -1
  60. package/dist/client/assets/{rst-rY4UPiwe.js → rst-Bbxxs6E2.js} +1 -1
  61. package/dist/client/assets/{ruby-C4cISY4u.js → ruby-C-TuWES4.js} +1 -1
  62. package/dist/client/assets/{sas-ui1PNNkD.js → sas-njzNYNOq.js} +1 -1
  63. package/dist/client/assets/{scss-BZrs95TC.js → scss-D2i6rHB8.js} +1 -1
  64. package/dist/client/assets/{shellscript-D9YVFS67.js → shellscript-D6Bxi40C.js} +1 -1
  65. package/dist/client/assets/{shellsession-DK7LMzYg.js → shellsession-Defxq-I6.js} +1 -1
  66. package/dist/client/assets/{soy-CNkc-Oji.js → soy-D0GKXrtT.js} +1 -1
  67. package/dist/client/assets/{sql-BvRdqBMa.js → sql-BRW_ADkU.js} +1 -1
  68. package/dist/client/assets/{stata-Bawjgtiz.js → stata-w7w59F48.js} +1 -1
  69. package/dist/client/assets/{surrealql-7v7lT5jp.js → surrealql-O-lLuqz7.js} +1 -1
  70. package/dist/client/assets/{svelte-p7w_kluv.js → svelte-DN3DR5-t.js} +1 -1
  71. package/dist/client/assets/{templ-0Gi_yZZr.js → templ-CJ2JFoia.js} +1 -1
  72. package/dist/client/assets/{tex-nZ_J3lks.js → tex-CpWrf5g9.js} +1 -1
  73. package/dist/client/assets/{ts-tags-CuG-Aj5j.js → ts-tags-CZ6Af6Pe.js} +1 -1
  74. package/dist/client/assets/{tsx-Cc7GNb_V.js → tsx-gIUSpRih.js} +1 -1
  75. package/dist/client/assets/{twig-BBlmihVM.js → twig-DwZINhmw.js} +1 -1
  76. package/dist/client/assets/{typescript-aiXwsZhm.js → typescript-Dj2aMECR.js} +1 -1
  77. package/dist/client/assets/{vue-Cs5tk2kn.js → vue-B5K0aWrP.js} +1 -1
  78. package/dist/client/assets/{vue-html-BEI_B8Ml.js → vue-html-DKGgrypd.js} +1 -1
  79. package/dist/client/assets/{vue-vine-CpWdOkP4.js → vue-vine-B5iDYTCh.js} +1 -1
  80. package/dist/client/assets/{xml-madLQSkS.js → xml-CbTzxfc-.js} +1 -1
  81. package/dist/client/assets/{xsl-CqZR37V9.js → xsl-Dj-Be6nx.js} +1 -1
  82. package/dist/client/assets/{yaml-DHr9ndbK.js → yaml-rTK8uEjh.js} +1 -1
  83. package/dist/client/index.html +2 -5
  84. package/dist/mcp-CE77qfjx.mjs +254 -0
  85. package/dist/mcp-CFeE13n-.mjs +254 -0
  86. package/dist/mcp-DKlThflu.mjs +253 -0
  87. package/dist/mcp-SGCrfYGo.mjs +254 -0
  88. package/dist/server-lock-B4crz9-_.mjs +708 -0
  89. package/dist/server-lock-CAdqFSfl.mjs +691 -0
  90. package/dist/server-lock-DpCrvL-Z.mjs +714 -0
  91. package/dist/update-check-DJuFE9w6.mjs +128 -0
  92. package/dist/update-check-DSC02MWQ.mjs +129 -0
  93. package/dist/update-check-KL-2sGxE.mjs +129 -0
  94. package/package.json +12 -14
  95. package/dist/client/assets/index-CmfB2R-C.css +0 -1
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as gitAddFile, a as formatComments, b as revertHunk, c as getFileContent, d as getMergeStatus, f as getProjectStorageDir, g as getUntrackedFilePathsAsync, h as getTabSizeForFiles, i as writeServerLock, l as getGitDiffAsync, m as getRepoRoot, o as getBranchName, p as getRepoName, r as removeServerLock, s as getCustomGitDiffAsync, u as getHunkHistory, v as isGitRepo, x as isSafePath, y as listRepoFiles } from "./server-lock-DbxpfF3j.mjs";
2
+ import { C as revertHunk, S as listRepoFiles, T as toSafeRelativePath, _ as getRepoRoot, a as extractPlanLines, b as gitAddFile, c as formatComments, d as getFileContent, f as getGitDiffAsync, g as getRepoName, h as getProjectStorageDir, i as writeServerLock, l as getBranchName, m as getMergeStatus, o as formatPlanReview, p as getHunkHistory, r as removeServerLock, s as sectionTitleForLine, u as getCustomGitDiffAsync, v as getTabSizeForFiles, w as isSafePath, x as isGitRepo, y as getUntrackedFilePathsAsync } from "./server-lock-DpCrvL-Z.mjs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, extname, join, resolve } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
@@ -11,6 +11,7 @@ import { homedir } from "node:os";
11
11
  import { Hono } from "hono";
12
12
  import { streamSSE } from "hono/streaming";
13
13
  import { serve } from "@hono/node-server";
14
+ import { stdout } from "node:process";
14
15
  //#region src/lib/diff-options.ts
15
16
  const DEFAULTS$1 = {
16
17
  revisions: [],
@@ -1112,7 +1113,10 @@ const DEFAULTS = {
1112
1113
  expandContextByDefault: false,
1113
1114
  collapsedContextThreshold: 10,
1114
1115
  expansionLineCount: 20,
1115
- haptics: true
1116
+ haptics: true,
1117
+ sounds: true,
1118
+ uiFont: null,
1119
+ monoFont: null
1116
1120
  };
1117
1121
  function loadSettings() {
1118
1122
  try {
@@ -1139,9 +1143,15 @@ function saveSettings(settings) {
1139
1143
  var FileCommentStore = class {
1140
1144
  dirPath;
1141
1145
  filePath;
1142
- constructor(customRepoRoot) {
1143
- if (customRepoRoot) this.dirPath = join(customRepoRoot, ".diffing");
1144
- else this.dirPath = getProjectStorageDir();
1146
+ /**
1147
+ * @param storageDir Absolute directory to persist `comments.json` in.
1148
+ * Defaults to the per-repo storage dir under `~/.diffing` — comments are
1149
+ * NEVER written inside the reviewed (consumer) repo, so a consumer project
1150
+ * stays free of any diffing-specific artifacts. The override exists only so
1151
+ * tests can point at a throwaway temp dir.
1152
+ */
1153
+ constructor(storageDir) {
1154
+ this.dirPath = storageDir ?? getProjectStorageDir();
1145
1155
  this.filePath = join(this.dirPath, "comments.json");
1146
1156
  }
1147
1157
  async getAll() {
@@ -1222,6 +1232,212 @@ var FileCommentStore = class {
1222
1232
  }
1223
1233
  };
1224
1234
  //#endregion
1235
+ //#region src/lib/plans.ts
1236
+ function newId() {
1237
+ return crypto.randomUUID();
1238
+ }
1239
+ /** Shared mutation helpers so both stores behave identically. */
1240
+ function applyUpsert(plans, input, now) {
1241
+ if (input.id) {
1242
+ const existing = plans.find((p) => p.id === input.id);
1243
+ if (existing) {
1244
+ existing.title = input.title;
1245
+ existing.body = input.body;
1246
+ if (input.source !== void 0) existing.source = input.source;
1247
+ if (input.model !== void 0) existing.model = input.model;
1248
+ existing.version += 1;
1249
+ existing.updatedAt = now;
1250
+ existing.decision = "pending";
1251
+ existing.decisionComment = void 0;
1252
+ existing.decidedAt = void 0;
1253
+ return existing;
1254
+ }
1255
+ }
1256
+ const plan = {
1257
+ id: input.id || newId(),
1258
+ title: input.title,
1259
+ body: input.body,
1260
+ source: input.source,
1261
+ model: input.model,
1262
+ createdAt: now,
1263
+ updatedAt: now,
1264
+ version: 1,
1265
+ decision: "pending",
1266
+ comments: []
1267
+ };
1268
+ plans.push(plan);
1269
+ return plan;
1270
+ }
1271
+ var FilePlanStore = class {
1272
+ dirPath;
1273
+ filePath;
1274
+ /**
1275
+ * @param storageDir Absolute directory to persist `plans.json` in. Defaults
1276
+ * to the per-repo storage dir under `~/.diffing`, the same place comments
1277
+ * and media live — plans are NEVER written inside the reviewed (consumer)
1278
+ * repo, so a consumer project stays free of any diffing-specific artifacts.
1279
+ * The override exists only so tests can point at a throwaway temp dir.
1280
+ */
1281
+ constructor(storageDir) {
1282
+ this.dirPath = storageDir ?? getProjectStorageDir();
1283
+ this.filePath = join(this.dirPath, "plans.json");
1284
+ }
1285
+ async getAll() {
1286
+ try {
1287
+ const data = await readFile(this.filePath, "utf-8");
1288
+ return JSON.parse(data);
1289
+ } catch {
1290
+ return [];
1291
+ }
1292
+ }
1293
+ async get(id) {
1294
+ return (await this.getAll()).find((p) => p.id === id) ?? null;
1295
+ }
1296
+ async save(plans) {
1297
+ try {
1298
+ await mkdir(this.dirPath, { recursive: true });
1299
+ try {
1300
+ const repoRoot = getRepoRoot();
1301
+ await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1302
+ } catch {}
1303
+ await writeFile(this.filePath, JSON.stringify(plans, null, 2), "utf-8");
1304
+ } catch (err) {
1305
+ console.error("Failed to save plans to file:", err);
1306
+ }
1307
+ }
1308
+ async upsert(input) {
1309
+ const plans = await this.getAll();
1310
+ const plan = applyUpsert(plans, input, Date.now());
1311
+ await this.save(plans);
1312
+ return plan;
1313
+ }
1314
+ async update(id, fields) {
1315
+ const plans = await this.getAll();
1316
+ const plan = plans.find((p) => p.id === id);
1317
+ if (!plan) return null;
1318
+ if (fields.title !== void 0) plan.title = fields.title;
1319
+ if (fields.body !== void 0) plan.body = fields.body;
1320
+ if (fields.source !== void 0) plan.source = fields.source;
1321
+ if (fields.model !== void 0) plan.model = fields.model;
1322
+ plan.updatedAt = Date.now();
1323
+ await this.save(plans);
1324
+ return plan;
1325
+ }
1326
+ async remove(id) {
1327
+ const plans = await this.getAll();
1328
+ const idx = plans.findIndex((p) => p.id === id);
1329
+ if (idx === -1) return false;
1330
+ plans.splice(idx, 1);
1331
+ await this.save(plans);
1332
+ return true;
1333
+ }
1334
+ async setDecision(id, decision, decisionComment) {
1335
+ const plans = await this.getAll();
1336
+ const plan = plans.find((p) => p.id === id);
1337
+ if (!plan) return null;
1338
+ plan.decision = decision;
1339
+ plan.decisionComment = decisionComment?.trim() || void 0;
1340
+ plan.decidedAt = Date.now();
1341
+ plan.updatedAt = plan.decidedAt;
1342
+ await this.save(plans);
1343
+ return plan;
1344
+ }
1345
+ async mutate(planId, fn) {
1346
+ const plans = await this.getAll();
1347
+ const plan = plans.find((p) => p.id === planId);
1348
+ if (!plan) return null;
1349
+ if (!plan.comments) plan.comments = [];
1350
+ if (!fn(plan)) return null;
1351
+ await this.save(plans);
1352
+ return plan;
1353
+ }
1354
+ async addComment(planId, comment) {
1355
+ return this.mutate(planId, (plan) => {
1356
+ plan.comments.push(comment);
1357
+ return true;
1358
+ });
1359
+ }
1360
+ async updateComment(planId, commentId, fields) {
1361
+ return this.mutate(planId, (plan) => {
1362
+ const c = plan.comments.find((x) => x.id === commentId);
1363
+ if (!c) return false;
1364
+ if (fields.body !== void 0) c.body = fields.body;
1365
+ if (fields.status !== void 0) c.status = fields.status;
1366
+ return true;
1367
+ });
1368
+ }
1369
+ async removeComment(planId, commentId) {
1370
+ return this.mutate(planId, (plan) => {
1371
+ const idx = plan.comments.findIndex((x) => x.id === commentId);
1372
+ if (idx === -1) return false;
1373
+ plan.comments.splice(idx, 1);
1374
+ return true;
1375
+ });
1376
+ }
1377
+ async addReply(planId, commentId, reply) {
1378
+ return this.mutate(planId, (plan) => {
1379
+ const c = plan.comments.find((x) => x.id === commentId);
1380
+ if (!c) return false;
1381
+ if (!c.replies) c.replies = [];
1382
+ c.replies.push(reply);
1383
+ return true;
1384
+ });
1385
+ }
1386
+ async removeReply(planId, commentId, replyId) {
1387
+ return this.mutate(planId, (plan) => {
1388
+ const c = plan.comments.find((x) => x.id === commentId);
1389
+ if (!c) return false;
1390
+ const idx = c.replies.findIndex((r) => r.id === replyId);
1391
+ if (idx === -1) return false;
1392
+ c.replies.splice(idx, 1);
1393
+ return true;
1394
+ });
1395
+ }
1396
+ async updateReply(planId, commentId, replyId, body) {
1397
+ return this.mutate(planId, (plan) => {
1398
+ const c = plan.comments.find((x) => x.id === commentId);
1399
+ if (!c) return false;
1400
+ const reply = c.replies.find((r) => r.id === replyId);
1401
+ if (!reply) return false;
1402
+ reply.body = body;
1403
+ return true;
1404
+ });
1405
+ }
1406
+ };
1407
+ //#endregion
1408
+ //#region src/lib/state.ts
1409
+ var FileUiStateStore = class {
1410
+ dirPath;
1411
+ filePath;
1412
+ constructor(storageDir) {
1413
+ this.dirPath = storageDir ?? getProjectStorageDir();
1414
+ this.filePath = join(this.dirPath, "ui-state.json");
1415
+ }
1416
+ async getAll() {
1417
+ try {
1418
+ const data = await readFile(this.filePath, "utf-8");
1419
+ return JSON.parse(data);
1420
+ } catch {
1421
+ return {};
1422
+ }
1423
+ }
1424
+ async setAll(state) {
1425
+ await this.save(state);
1426
+ }
1427
+ async save(state) {
1428
+ try {
1429
+ await mkdir(this.dirPath, { recursive: true });
1430
+ try {
1431
+ const repoRoot = getRepoRoot();
1432
+ await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1433
+ } catch {}
1434
+ await writeFile(this.filePath, JSON.stringify(state, null, 2), "utf-8");
1435
+ } catch (err) {
1436
+ console.error("Failed to save UI state to file:", err);
1437
+ }
1438
+ }
1439
+ };
1440
+ //#endregion
1225
1441
  //#region src/lib/review-session.ts
1226
1442
  var ReviewSession = class {
1227
1443
  round = 0;
@@ -1285,7 +1501,8 @@ var ReviewSession = class {
1285
1501
  sentAt: input.sentAt,
1286
1502
  commentXml: input.commentXml,
1287
1503
  openCount: input.openCount,
1288
- comments: input.comments
1504
+ comments: input.comments,
1505
+ decision: input.decision
1289
1506
  };
1290
1507
  this.lastPayload = payload;
1291
1508
  const waiters = [...this.waiters];
@@ -1319,6 +1536,100 @@ var ReviewSession = class {
1319
1536
  }
1320
1537
  };
1321
1538
  //#endregion
1539
+ //#region src/lib/plan-review-session.ts
1540
+ var PlanReviewSession = class {
1541
+ round = 0;
1542
+ lastDecidedAt = null;
1543
+ lastPayload = null;
1544
+ waiters = /* @__PURE__ */ new Set();
1545
+ onStatusChange;
1546
+ constructor(onStatusChange) {
1547
+ this.onStatusChange = onStatusChange;
1548
+ }
1549
+ /** Block until the next `decide()` or until `timeoutMs` elapses. */
1550
+ await(opts) {
1551
+ const { sinceRound, timeoutMs, signal } = opts;
1552
+ if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
1553
+ status: "released",
1554
+ payload: this.lastPayload
1555
+ });
1556
+ if (signal?.aborted) return Promise.resolve({
1557
+ status: "keep-waiting",
1558
+ round: this.round
1559
+ });
1560
+ return new Promise((resolve) => {
1561
+ const waiter = {
1562
+ resolve,
1563
+ timer: setTimeout(() => {
1564
+ this.removeWaiter(waiter);
1565
+ resolve({
1566
+ status: "keep-waiting",
1567
+ round: this.round
1568
+ });
1569
+ }, timeoutMs),
1570
+ cleanup: () => {}
1571
+ };
1572
+ if (signal) {
1573
+ const onAbort = () => {
1574
+ this.removeWaiter(waiter);
1575
+ resolve({
1576
+ status: "keep-waiting",
1577
+ round: this.round
1578
+ });
1579
+ };
1580
+ signal.addEventListener("abort", onAbort, { once: true });
1581
+ waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
1582
+ }
1583
+ this.waiters.add(waiter);
1584
+ this.emitStatus();
1585
+ });
1586
+ }
1587
+ /** Capture a decision and release every blocked waiter. */
1588
+ decide(input) {
1589
+ this.round += 1;
1590
+ this.lastDecidedAt = input.sentAt;
1591
+ const payload = {
1592
+ round: this.round,
1593
+ sentAt: input.sentAt,
1594
+ planId: input.planId,
1595
+ decision: input.decision,
1596
+ decisionComment: input.decisionComment,
1597
+ reviewXml: input.reviewXml,
1598
+ openCommentCount: input.openCommentCount,
1599
+ plan: input.plan
1600
+ };
1601
+ this.lastPayload = payload;
1602
+ const waiters = [...this.waiters];
1603
+ this.waiters.clear();
1604
+ for (const waiter of waiters) {
1605
+ clearTimeout(waiter.timer);
1606
+ waiter.cleanup();
1607
+ waiter.resolve({
1608
+ status: "released",
1609
+ payload
1610
+ });
1611
+ }
1612
+ this.emitStatus();
1613
+ return payload;
1614
+ }
1615
+ snapshot() {
1616
+ return {
1617
+ round: this.round,
1618
+ waiters: this.waiters.size,
1619
+ lastDecidedAt: this.lastDecidedAt
1620
+ };
1621
+ }
1622
+ removeWaiter(waiter) {
1623
+ if (!this.waiters.delete(waiter)) return;
1624
+ clearTimeout(waiter.timer);
1625
+ waiter.cleanup();
1626
+ this.emitStatus();
1627
+ }
1628
+ emitStatus() {
1629
+ this.onStatusChange?.(this.snapshot());
1630
+ }
1631
+ };
1632
+ //#endregion
1322
1633
  //#region src/server.ts
1323
1634
  const MIME_TYPES = {
1324
1635
  ".html": "text/html",
@@ -1338,10 +1649,12 @@ const MIME_TYPES = {
1338
1649
  function isCustomMode(opts) {
1339
1650
  return opts.revisions.length > 0 || opts.pathspecs.length > 0;
1340
1651
  }
1341
- function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1652
+ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1342
1653
  const app = new Hono();
1343
1654
  const customMode = isCustomMode(diffOpts);
1344
1655
  const store = commentStore ?? new FileCommentStore();
1656
+ const plans = planStore ?? new FilePlanStore();
1657
+ const uiStateStore = new FileUiStateStore();
1345
1658
  const viewedFiles = /* @__PURE__ */ new Set();
1346
1659
  const activeClients = /* @__PURE__ */ new Set();
1347
1660
  const broadcast = (event, data) => {
@@ -1350,23 +1663,30 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1350
1663
  } catch {}
1351
1664
  };
1352
1665
  const reviewSession = new ReviewSession((snapshot) => broadcast("agent-status", JSON.stringify(snapshot)));
1666
+ const planReviewSession = new PlanReviewSession((snapshot) => broadcast("plan-review-status", JSON.stringify(snapshot)));
1353
1667
  let repoRoot;
1354
1668
  try {
1355
1669
  repoRoot = getRepoRoot();
1356
1670
  } catch {
1357
1671
  repoRoot = process.cwd();
1358
1672
  }
1359
- if (!commentStore) try {
1673
+ if (!commentStore && !planStore) try {
1360
1674
  const storageDir = getProjectStorageDir();
1361
1675
  mkdirSync(storageDir, { recursive: true });
1362
1676
  let commentsDebounce = null;
1677
+ let plansDebounce = null;
1363
1678
  watch(storageDir, (_eventType, filename) => {
1364
- if (filename && !filename.startsWith("comments.json")) return;
1365
- if (commentsDebounce) clearTimeout(commentsDebounce);
1366
- commentsDebounce = setTimeout(() => broadcast("comments", Date.now().toString()), 120);
1679
+ if (!filename) return;
1680
+ if (filename.startsWith("comments.json")) {
1681
+ if (commentsDebounce) clearTimeout(commentsDebounce);
1682
+ commentsDebounce = setTimeout(() => broadcast("comments", Date.now().toString()), 120);
1683
+ } else if (filename.startsWith("plans.json")) {
1684
+ if (plansDebounce) clearTimeout(plansDebounce);
1685
+ plansDebounce = setTimeout(() => broadcast("plans", Date.now().toString()), 120);
1686
+ }
1367
1687
  }).unref();
1368
1688
  } catch (err) {
1369
- console.warn("Failed to initialize comment store watcher:", err);
1689
+ console.warn("Failed to initialize storage watcher:", err);
1370
1690
  }
1371
1691
  let debounceTimeout = null;
1372
1692
  try {
@@ -1468,8 +1788,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1468
1788
  if (!filePath) return c.json({ error: "filePath is required" }, 400);
1469
1789
  try {
1470
1790
  const root = getRepoRoot();
1471
- if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1472
- const absolutePath = resolve(root, filePath);
1791
+ const relPath = toSafeRelativePath(filePath, root);
1792
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1793
+ const absolutePath = resolve(root, relPath);
1473
1794
  const { exec } = await import("node:child_process");
1474
1795
  if (editor === "vscode") exec(`code "${absolutePath}"`);
1475
1796
  else if (editor === "zed") exec(`zed "${absolutePath}"`);
@@ -1543,8 +1864,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1543
1864
  const { filePath, hunkIndex } = await c.req.json();
1544
1865
  if (!filePath || typeof hunkIndex !== "number") return c.json({ error: "Missing filePath or hunkIndex" }, 400);
1545
1866
  try {
1546
- if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1547
- revertHunk(filePath, hunkIndex);
1867
+ const relPath = toSafeRelativePath(filePath, getRepoRoot());
1868
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1869
+ revertHunk(relPath, hunkIndex);
1548
1870
  return c.json({ ok: true });
1549
1871
  } catch (err) {
1550
1872
  const stderr = typeof err?.stderr === "string" ? err.stderr : err?.stderr instanceof Buffer ? err.stderr.toString("utf-8") : "";
@@ -1557,8 +1879,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1557
1879
  const deletionCount = Number(c.req.query("deletionCount"));
1558
1880
  if (!filePath || Number.isNaN(deletionStart) || Number.isNaN(deletionCount)) return c.json({ error: "Missing or invalid parameters" }, 400);
1559
1881
  try {
1560
- if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1561
- const history = getHunkHistory(filePath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
1882
+ const relPath = toSafeRelativePath(filePath, getRepoRoot());
1883
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1884
+ const history = getHunkHistory(relPath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
1562
1885
  return c.json(history);
1563
1886
  } catch (err) {
1564
1887
  return c.json({ error: err.message || "Failed to fetch hunk history" }, 500);
@@ -1569,10 +1892,11 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1569
1892
  if (!filePath || typeof content !== "string") return c.json({ error: "Missing filePath or content" }, 400);
1570
1893
  try {
1571
1894
  const root = getRepoRoot();
1572
- if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1573
- await writeFile(resolve(root, filePath), content, "utf-8");
1895
+ const relPath = toSafeRelativePath(filePath, root);
1896
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1897
+ await writeFile(resolve(root, relPath), content, "utf-8");
1574
1898
  if (gitAdd) try {
1575
- gitAddFile(filePath);
1899
+ gitAddFile(relPath);
1576
1900
  } catch (err) {
1577
1901
  return c.json({
1578
1902
  ok: true,
@@ -1591,6 +1915,19 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1591
1915
  const settings = saveSettings(await c.req.json());
1592
1916
  return c.json(settings);
1593
1917
  });
1918
+ app.get("/api/ui-state", async (c) => {
1919
+ return c.json(await uiStateStore.getAll());
1920
+ });
1921
+ app.put("/api/ui-state", async (c) => {
1922
+ const body = await c.req.json();
1923
+ const merged = {
1924
+ ...await uiStateStore.getAll(),
1925
+ ...body
1926
+ };
1927
+ for (const key of Object.keys(body)) if (body[key] === null || body[key] === void 0) delete merged[key];
1928
+ await uiStateStore.setAll(merged);
1929
+ return c.json(merged);
1930
+ });
1594
1931
  app.get("/api/viewed", (c) => {
1595
1932
  return c.json([...viewedFiles]);
1596
1933
  });
@@ -1671,8 +2008,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1671
2008
  const suggestion = match[1];
1672
2009
  try {
1673
2010
  const root = getRepoRoot();
1674
- if (!isSafePath(comment.filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1675
- const absolutePath = resolve(root, comment.filePath);
2011
+ const relPath = toSafeRelativePath(comment.filePath, root);
2012
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2013
+ const absolutePath = resolve(root, relPath);
1676
2014
  const lines = (await readFile(absolutePath, "utf-8")).split(/\r?\n/);
1677
2015
  const lineIdx = comment.lineNumber - 1;
1678
2016
  if (lineIdx < 0 || lineIdx >= lines.length) return c.json({ error: "Comment line number out of range" }, 400);
@@ -1692,18 +2030,21 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1692
2030
  app.post("/api/review/send", async (c) => {
1693
2031
  const body = await c.req.json().catch(() => ({}));
1694
2032
  const generalComment = typeof body?.generalComment === "string" ? body.generalComment : void 0;
2033
+ const decision = body?.decision === "approved" || body?.decision === "changes-requested" || body?.decision === "rejected" ? body.decision : void 0;
1695
2034
  const all = await store.getAll();
1696
2035
  const openCount = all.filter((x) => x.status === "open").length;
1697
2036
  const payload = reviewSession.send({
1698
2037
  sentAt: Date.now(),
1699
- commentXml: formatComments(all, generalComment),
2038
+ commentXml: formatComments(all, generalComment, decision),
1700
2039
  openCount,
1701
- comments: all
2040
+ comments: all,
2041
+ decision
1702
2042
  });
1703
2043
  return c.json({
1704
2044
  ok: true,
1705
2045
  round: payload.round,
1706
2046
  openCount: payload.openCount,
2047
+ decision: payload.decision,
1707
2048
  waiters: reviewSession.snapshot().waiters
1708
2049
  });
1709
2050
  });
@@ -1722,6 +2063,148 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1722
2063
  app.get("/api/review/status", (c) => {
1723
2064
  return c.json(reviewSession.snapshot());
1724
2065
  });
2066
+ app.get("/api/plans", async (c) => {
2067
+ return c.json(await plans.getAll());
2068
+ });
2069
+ app.get("/api/plans/:id", async (c) => {
2070
+ const plan = await plans.get(c.req.param("id"));
2071
+ if (!plan) return c.json({ error: "Plan not found" }, 404);
2072
+ return c.json(plan);
2073
+ });
2074
+ app.post("/api/plans", async (c) => {
2075
+ const body = await c.req.json().catch(() => ({}));
2076
+ if (typeof body.body !== "string" || !body.body.trim()) return c.json({ error: "A plan body (markdown) is required" }, 400);
2077
+ const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "Untitled plan";
2078
+ const plan = await plans.upsert({
2079
+ id: typeof body.id === "string" && body.id ? body.id : void 0,
2080
+ title,
2081
+ body: body.body,
2082
+ source: typeof body.source === "string" ? body.source : void 0,
2083
+ model: typeof body.model === "string" ? body.model : void 0
2084
+ });
2085
+ return c.json(plan, 201);
2086
+ });
2087
+ app.put("/api/plans/:id", async (c) => {
2088
+ const id = c.req.param("id");
2089
+ const { title, body, source, model } = await c.req.json().catch(() => ({}));
2090
+ const updated = await plans.update(id, {
2091
+ title: typeof title === "string" ? title : void 0,
2092
+ body: typeof body === "string" ? body : void 0,
2093
+ source: typeof source === "string" ? source : void 0,
2094
+ model: typeof model === "string" ? model : void 0
2095
+ });
2096
+ if (!updated) return c.json({ error: "Plan not found" }, 404);
2097
+ return c.json(updated);
2098
+ });
2099
+ app.delete("/api/plans/:id", async (c) => {
2100
+ if (!await plans.remove(c.req.param("id"))) return c.json({ error: "Plan not found" }, 404);
2101
+ return c.json({ ok: true });
2102
+ });
2103
+ app.post("/api/plans/:id/comments", async (c) => {
2104
+ const planId = c.req.param("id");
2105
+ const plan = await plans.get(planId);
2106
+ if (!plan) return c.json({ error: "Plan not found" }, 404);
2107
+ const body = await c.req.json();
2108
+ const lineNumber = Number.isFinite(body.lineNumber) ? Number(body.lineNumber) : 0;
2109
+ const startLineNumber = Number.isFinite(body.startLineNumber) ? Number(body.startLineNumber) : void 0;
2110
+ const anchorStart = startLineNumber ?? lineNumber;
2111
+ const lineContent = typeof body.lineContent === "string" && body.lineContent ? body.lineContent : lineNumber > 0 ? extractPlanLines(plan.body, anchorStart, lineNumber) : "";
2112
+ const sectionTitle = typeof body.sectionTitle === "string" && body.sectionTitle ? body.sectionTitle : lineNumber > 0 ? sectionTitleForLine(plan.body, anchorStart) : void 0;
2113
+ const comment = {
2114
+ id: crypto.randomUUID(),
2115
+ lineNumber,
2116
+ startLineNumber,
2117
+ lineContent,
2118
+ sectionTitle,
2119
+ body: body.body,
2120
+ status: "open",
2121
+ createdAt: Date.now(),
2122
+ replies: []
2123
+ };
2124
+ const updated = await plans.addComment(planId, comment);
2125
+ if (!updated) return c.json({ error: "Plan not found" }, 404);
2126
+ return c.json(updated, 201);
2127
+ });
2128
+ app.put("/api/plans/:id/comments/:commentId", async (c) => {
2129
+ const { body, status } = await c.req.json();
2130
+ const updated = await plans.updateComment(c.req.param("id"), c.req.param("commentId"), {
2131
+ body,
2132
+ status
2133
+ });
2134
+ if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
2135
+ return c.json(updated);
2136
+ });
2137
+ app.delete("/api/plans/:id/comments/:commentId", async (c) => {
2138
+ const updated = await plans.removeComment(c.req.param("id"), c.req.param("commentId"));
2139
+ if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
2140
+ return c.json(updated);
2141
+ });
2142
+ app.post("/api/plans/:id/comments/:commentId/replies", async (c) => {
2143
+ const { body, role, model } = await c.req.json();
2144
+ const reply = {
2145
+ id: crypto.randomUUID(),
2146
+ body,
2147
+ createdAt: Date.now(),
2148
+ role: role || (model ? "agent" : "user"),
2149
+ model: model || void 0
2150
+ };
2151
+ const updated = await plans.addReply(c.req.param("id"), c.req.param("commentId"), reply);
2152
+ if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
2153
+ return c.json(updated);
2154
+ });
2155
+ app.put("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
2156
+ const { body } = await c.req.json();
2157
+ if (!body) return c.json({ error: "Body is required" }, 400);
2158
+ const updated = await plans.updateReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"), body);
2159
+ if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
2160
+ return c.json(updated);
2161
+ });
2162
+ app.delete("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
2163
+ const updated = await plans.removeReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"));
2164
+ if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
2165
+ return c.json(updated);
2166
+ });
2167
+ app.post("/api/plans/:id/decision", async (c) => {
2168
+ const planId = c.req.param("id");
2169
+ const body = await c.req.json().catch(() => ({}));
2170
+ const decision = body.decision;
2171
+ if (decision !== "approved" && decision !== "rejected" && decision !== "changes-requested") return c.json({ error: "decision must be one of: approved, rejected, changes-requested" }, 400);
2172
+ const decisionComment = typeof body.decisionComment === "string" ? body.decisionComment : void 0;
2173
+ const plan = await plans.setDecision(planId, decision, decisionComment);
2174
+ if (!plan) return c.json({ error: "Plan not found" }, 404);
2175
+ const openCommentCount = (plan.comments ?? []).filter((x) => x.status === "open").length;
2176
+ const payload = planReviewSession.decide({
2177
+ sentAt: Date.now(),
2178
+ planId,
2179
+ decision,
2180
+ decisionComment: plan.decisionComment,
2181
+ reviewXml: formatPlanReview(plan),
2182
+ openCommentCount,
2183
+ plan
2184
+ });
2185
+ return c.json({
2186
+ ok: true,
2187
+ round: payload.round,
2188
+ decision,
2189
+ openCommentCount,
2190
+ waiters: planReviewSession.snapshot().waiters
2191
+ });
2192
+ });
2193
+ app.get("/api/plan-review/await", async (c) => {
2194
+ const sinceRaw = c.req.query("sinceRound");
2195
+ const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
2196
+ const requested = Number(c.req.query("timeoutMs")) || 25e3;
2197
+ const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
2198
+ const result = await planReviewSession.await({
2199
+ sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
2200
+ timeoutMs,
2201
+ signal: c.req.raw.signal
2202
+ });
2203
+ return c.json(result);
2204
+ });
2205
+ app.get("/api/plan-review/status", (c) => {
2206
+ return c.json(planReviewSession.snapshot());
2207
+ });
1725
2208
  app.post("/api/attachments", async (c) => {
1726
2209
  const file = (await c.req.parseBody())["file"];
1727
2210
  if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
@@ -1771,6 +2254,40 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1771
2254
  });
1772
2255
  return app;
1773
2256
  }
2257
+ /**
2258
+ * Newest mtime (epoch ms) across everything that counts as project activity:
2259
+ * comments.json, plans.json, and any attachment (media) file. Plans and media
2260
+ * extend a project's life exactly like comments do, so retention is uniform
2261
+ * across all three. Returns null when none of them exist yet.
2262
+ */
2263
+ async function newestActivityMs(projectDir) {
2264
+ const candidates = [];
2265
+ const safeStat = async (p) => {
2266
+ try {
2267
+ return (await stat(p)).mtimeMs;
2268
+ } catch {
2269
+ return null;
2270
+ }
2271
+ };
2272
+ for (const name of ["comments.json", "plans.json"]) {
2273
+ const file = join(projectDir, name);
2274
+ if (!existsSync(file)) continue;
2275
+ const m = await safeStat(file);
2276
+ if (m !== null) candidates.push(m);
2277
+ }
2278
+ const attachmentsDir = join(projectDir, "attachments");
2279
+ if (existsSync(attachmentsDir)) {
2280
+ const dirM = await safeStat(attachmentsDir);
2281
+ if (dirM !== null) candidates.push(dirM);
2282
+ try {
2283
+ for (const file of await readdir(attachmentsDir)) {
2284
+ const m = await safeStat(join(attachmentsDir, file));
2285
+ if (m !== null) candidates.push(m);
2286
+ }
2287
+ } catch {}
2288
+ }
2289
+ return candidates.length ? Math.max(...candidates) : null;
2290
+ }
1774
2291
  async function cleanupStaleProjects() {
1775
2292
  const baseDir = join(homedir(), ".diffing");
1776
2293
  if (!existsSync(baseDir)) return;
@@ -1781,15 +2298,15 @@ async function cleanupStaleProjects() {
1781
2298
  for (const entry of entries) if (entry.isDirectory()) {
1782
2299
  const projectDir = join(baseDir, entry.name);
1783
2300
  const repoPathFile = join(projectDir, "repo_path.txt");
1784
- const commentsFile = join(projectDir, "comments.json");
1785
2301
  let shouldDelete = false;
1786
2302
  if (existsSync(repoPathFile)) try {
1787
2303
  const repoPath = (await readFile(repoPathFile, "utf-8")).trim();
1788
2304
  if (!repoPath || !existsSync(repoPath)) shouldDelete = true;
1789
2305
  } catch {}
1790
- if (!shouldDelete && existsSync(commentsFile)) try {
1791
- if (now - (await stat(commentsFile)).mtimeMs > STALE_TIME) shouldDelete = true;
1792
- } catch {}
2306
+ if (!shouldDelete) {
2307
+ const newest = await newestActivityMs(projectDir);
2308
+ if (newest !== null && now - newest > STALE_TIME) shouldDelete = true;
2309
+ }
1793
2310
  if (shouldDelete) await rm(projectDir, {
1794
2311
  recursive: true,
1795
2312
  force: true
@@ -1815,6 +2332,389 @@ function startServer(options) {
1815
2332
  });
1816
2333
  }
1817
2334
  //#endregion
2335
+ //#region src/lib/startup-display.ts
2336
+ const R = "\x1B[0m";
2337
+ const B = "\x1B[1m";
2338
+ const HIDE = "\x1B[?25l";
2339
+ const SHOW = "\x1B[?25h";
2340
+ const CLR = "\x1B[2K\r";
2341
+ const GLITCH_CHARS = "░▒▓█▄▀■□▸▹◆◇";
2342
+ const c = (n) => `\x1b[38;5;${n}m`;
2343
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2344
+ const up = (n) => `\x1b[${n}A`;
2345
+ const PALETTES = [
2346
+ {
2347
+ base: c(51),
2348
+ text: c(159),
2349
+ faint: c(30),
2350
+ glow: B + c(87),
2351
+ rain: c(37)
2352
+ },
2353
+ {
2354
+ base: c(82),
2355
+ text: c(155),
2356
+ faint: c(22),
2357
+ glow: B + c(118),
2358
+ rain: c(34)
2359
+ },
2360
+ {
2361
+ base: c(207),
2362
+ text: c(219),
2363
+ faint: c(96),
2364
+ glow: B + c(213),
2365
+ rain: c(128)
2366
+ },
2367
+ {
2368
+ base: c(220),
2369
+ text: c(229),
2370
+ faint: c(136),
2371
+ glow: B + c(226),
2372
+ rain: c(178)
2373
+ },
2374
+ {
2375
+ base: c(75),
2376
+ text: c(153),
2377
+ faint: c(25),
2378
+ glow: B + c(81),
2379
+ rain: c(39)
2380
+ },
2381
+ {
2382
+ base: c(214),
2383
+ text: c(223),
2384
+ faint: c(130),
2385
+ glow: B + c(208),
2386
+ rain: c(172)
2387
+ }
2388
+ ];
2389
+ const QUOTES = [
2390
+ {
2391
+ text: "The best code is no code at all.",
2392
+ author: "Jeff Atwood"
2393
+ },
2394
+ {
2395
+ text: "Programs must be written for people to read, and only incidentally for machines to execute.",
2396
+ author: "Abelson & Sussman"
2397
+ },
2398
+ {
2399
+ text: "Make it work, make it right, make it fast.",
2400
+ author: "Kent Beck"
2401
+ },
2402
+ {
2403
+ text: "Simplicity is the soul of efficiency.",
2404
+ author: "Austin Freeman"
2405
+ },
2406
+ {
2407
+ text: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
2408
+ author: "Martin Fowler"
2409
+ },
2410
+ {
2411
+ text: "First, solve the problem. Then, write the code.",
2412
+ author: "John Johnson"
2413
+ },
2414
+ {
2415
+ text: "Code is like humor. When you have to explain it, it's bad.",
2416
+ author: "Cory House"
2417
+ },
2418
+ {
2419
+ text: "Before software can be reusable it first has to be usable.",
2420
+ author: "Ralph Johnson"
2421
+ },
2422
+ {
2423
+ text: "Clean code always looks like it was written by someone who cares.",
2424
+ author: "Robert C. Martin"
2425
+ },
2426
+ {
2427
+ text: "The most dangerous phrase is \"we've always done it this way\".",
2428
+ author: "Grace Hopper"
2429
+ },
2430
+ {
2431
+ text: "Architecture is the decisions you wish you could get right early in a project.",
2432
+ author: "Martin Fowler"
2433
+ },
2434
+ {
2435
+ text: "The best error message is the one that never shows up.",
2436
+ author: "Thomas Fuchs"
2437
+ },
2438
+ {
2439
+ text: "It works on my machine.",
2440
+ author: "Every developer, always"
2441
+ },
2442
+ {
2443
+ text: "git blame yourself.",
2444
+ author: "The terminal"
2445
+ },
2446
+ {
2447
+ text: "There are only two hard things in CS: cache invalidation, naming things, and off-by-one errors.",
2448
+ author: "Unknown"
2449
+ },
2450
+ {
2451
+ text: "Debugging: being the detective in a crime movie where you're also the murderer.",
2452
+ author: "Unknown"
2453
+ },
2454
+ {
2455
+ text: "The code you wrote six months ago was written by an idiot.",
2456
+ author: "Unknown"
2457
+ },
2458
+ {
2459
+ text: "sudo make me a sandwich.",
2460
+ author: "xkcd"
2461
+ },
2462
+ {
2463
+ text: "I don't always test my code, but when I do, I do it in production.",
2464
+ author: "Unknown"
2465
+ },
2466
+ {
2467
+ text: "A QA engineer walks into a bar. Orders 0 beers. Orders 999999 beers. Orders NULL beers. Walks in through the window.",
2468
+ author: "Unknown"
2469
+ },
2470
+ {
2471
+ text: "To understand recursion, you must first understand recursion.",
2472
+ author: "Unknown"
2473
+ },
2474
+ {
2475
+ text: "The code review is the product.",
2476
+ author: "diffing"
2477
+ },
2478
+ {
2479
+ text: "Your abstractions are just someone else's bugs.",
2480
+ author: "Unknown"
2481
+ },
2482
+ {
2483
+ text: "Every rewrite is a confession that you didn't understand the problem the first time.",
2484
+ author: "Unknown"
2485
+ },
2486
+ {
2487
+ text: "The best PR is no PR — ship it in the design.",
2488
+ author: "Unknown"
2489
+ },
2490
+ {
2491
+ text: "You don't own your code. Your code owns you.",
2492
+ author: "Unknown"
2493
+ },
2494
+ {
2495
+ text: "A diff is a conversation. What story does yours tell?",
2496
+ author: "Unknown"
2497
+ },
2498
+ {
2499
+ text: "The senior engineer's superpower: knowing which shortcuts will haunt you.",
2500
+ author: "Unknown"
2501
+ },
2502
+ {
2503
+ text: "Production is the only real test environment.",
2504
+ author: "Site Reliability truth"
2505
+ },
2506
+ {
2507
+ text: "Tech debt is just deferred thinking.",
2508
+ author: "Unknown"
2509
+ },
2510
+ {
2511
+ text: "Comments lie. Code never does.",
2512
+ author: "Ron Jeffries"
2513
+ }
2514
+ ];
2515
+ function wrapText(text, width) {
2516
+ const words = text.split(" ");
2517
+ const lines = [];
2518
+ let cur = "";
2519
+ for (const word of words) {
2520
+ if (!cur) {
2521
+ cur = word;
2522
+ continue;
2523
+ }
2524
+ if (cur.length + 1 + word.length <= width) cur += " " + word;
2525
+ else {
2526
+ lines.push(cur);
2527
+ cur = word;
2528
+ }
2529
+ }
2530
+ if (cur) lines.push(cur);
2531
+ return lines;
2532
+ }
2533
+ function makeBox(quote, pal) {
2534
+ const termWidth = stdout.columns ?? 80;
2535
+ const maxContent = Math.min(64, termWidth - 8);
2536
+ const fullText = `"${quote.text}"`;
2537
+ const authorStr = `— ${quote.author}`;
2538
+ const textLines = wrapText(fullText, maxContent);
2539
+ return {
2540
+ textLines,
2541
+ authorStr,
2542
+ innerWidth: Math.max(...textLines.map((l) => l.length), authorStr.length, 28) + 4,
2543
+ pal
2544
+ };
2545
+ }
2546
+ function topBorder(b, borderCol = b.pal.base) {
2547
+ return `${borderCol}╭${"─".repeat(b.innerWidth)}╮${R}`;
2548
+ }
2549
+ function bottomBorder(b, borderCol = b.pal.base) {
2550
+ return `${borderCol}╰${"─".repeat(b.innerWidth)}╯${R}`;
2551
+ }
2552
+ function contentRow(b, text, borderCol = b.pal.base, textCol = b.pal.text) {
2553
+ return `${borderCol}│${R} ${textCol}${text}${R}${" ".repeat(b.innerWidth - 4 - text.length)} ${borderCol}│${R}`;
2554
+ }
2555
+ function authorRow(b, borderCol = b.pal.base) {
2556
+ const leading = " ".repeat(b.innerWidth - 4 - b.authorStr.length);
2557
+ return `${borderCol}│${R} ${b.pal.faint}${leading}${b.authorStr}${R} ${borderCol}│${R}`;
2558
+ }
2559
+ function allLines(b, borderCol = b.pal.base) {
2560
+ return [
2561
+ topBorder(b, borderCol),
2562
+ ...b.textLines.map((l) => contentRow(b, l, borderCol)),
2563
+ authorRow(b, borderCol),
2564
+ bottomBorder(b, borderCol)
2565
+ ];
2566
+ }
2567
+ function printBox(b) {
2568
+ for (const line of allLines(b)) stdout.write(line + "\n");
2569
+ }
2570
+ const animTypewriter = async (b) => {
2571
+ stdout.write(topBorder(b) + "\n");
2572
+ for (const line of b.textLines) {
2573
+ stdout.write(`${b.pal.base}│${R} `);
2574
+ for (let i = 0; i < line.length; i++) {
2575
+ const ch = line[i];
2576
+ stdout.write(`${b.pal.text}${ch}${b.pal.glow}█${R}`);
2577
+ await sleep(25);
2578
+ stdout.write("\b \b");
2579
+ }
2580
+ for (let blink = 0; blink < 2; blink++) {
2581
+ stdout.write(`${b.pal.glow}█${R}`);
2582
+ await sleep(100);
2583
+ stdout.write("\b \b");
2584
+ await sleep(100);
2585
+ }
2586
+ stdout.write(" ".repeat(b.innerWidth - 4 - line.length) + ` ${b.pal.base}│${R}\n`);
2587
+ }
2588
+ stdout.write(authorRow(b) + "\n");
2589
+ stdout.write(bottomBorder(b) + "\n");
2590
+ };
2591
+ const animWaveReveal = async (b) => {
2592
+ stdout.write(topBorder(b) + "\n");
2593
+ for (const line of b.textLines) {
2594
+ const half = Math.ceil(line.length / 2);
2595
+ stdout.write(contentRow(b, line.slice(0, half).padEnd(line.length), b.pal.base, b.pal.faint) + "\n");
2596
+ await sleep(130);
2597
+ stdout.write(up(1) + CLR + contentRow(b, line) + "\n");
2598
+ await sleep(60);
2599
+ }
2600
+ stdout.write(authorRow(b) + "\n");
2601
+ stdout.write(bottomBorder(b) + "\n");
2602
+ };
2603
+ const animSlide = async (b) => {
2604
+ const lines = allLines(b);
2605
+ for (let i = 0; i < lines.length; i++) stdout.write("\n");
2606
+ stdout.write(up(lines.length));
2607
+ for (let step = 0; step < 5; step++) {
2608
+ const pad = " ".repeat(Math.floor(12 * (1 - step / 4)));
2609
+ for (const line of lines) stdout.write(CLR + pad + line + "\n");
2610
+ if (step < 4) {
2611
+ stdout.write(up(lines.length));
2612
+ await sleep(80);
2613
+ }
2614
+ }
2615
+ };
2616
+ const animPulseBorder = async (b) => {
2617
+ const stages = [
2618
+ allLines(b, b.pal.faint),
2619
+ allLines(b, b.pal.base),
2620
+ allLines(b, b.pal.glow),
2621
+ allLines(b, b.pal.base)
2622
+ ];
2623
+ for (let s = 0; s < stages.length; s++) {
2624
+ if (s > 0) stdout.write(up(stages[0].length));
2625
+ for (const line of stages[s]) stdout.write((s > 0 ? CLR : "") + line + "\n");
2626
+ if (s < stages.length - 1) await sleep(180);
2627
+ }
2628
+ };
2629
+ const animGlitch = async (b) => {
2630
+ const noiseRates = [
2631
+ .25,
2632
+ .12,
2633
+ .04,
2634
+ 0
2635
+ ];
2636
+ const totalLines = b.textLines.length + 3;
2637
+ for (let f = 0; f < noiseRates.length; f++) {
2638
+ if (f > 0) stdout.write(up(totalLines));
2639
+ const rate = noiseRates[f];
2640
+ const clr = f > 0 ? CLR : "";
2641
+ const jitter = rate > 0 ? Math.floor(Math.random() * 3) : 0;
2642
+ const pad = " ".repeat(jitter);
2643
+ stdout.write(clr + pad + topBorder(b) + "\n");
2644
+ for (const line of b.textLines) if (rate === 0) stdout.write(clr + contentRow(b, line) + "\n");
2645
+ else {
2646
+ let inner = "";
2647
+ for (const ch of line) if (Math.random() < rate) {
2648
+ const charColor = Math.random() < .3 ? b.pal.glow : b.pal.base;
2649
+ inner += `${charColor}${GLITCH_CHARS[Math.floor(Math.random() * 12)]}${b.pal.text}`;
2650
+ } else inner += ch;
2651
+ const fill = " ".repeat(b.innerWidth - 4 - line.length);
2652
+ stdout.write(clr + pad + `${b.pal.base}│${R} ${b.pal.text}${inner}${R}${fill} ${b.pal.base}│${R}\n`);
2653
+ }
2654
+ stdout.write(clr + pad + authorRow(b) + "\n");
2655
+ stdout.write(clr + pad + bottomBorder(b) + "\n");
2656
+ if (f < noiseRates.length - 1) await sleep(120);
2657
+ }
2658
+ };
2659
+ const animMatrixRain = async (b) => {
2660
+ const width = b.innerWidth + 2;
2661
+ const RAIN_HEIGHT = 6;
2662
+ const streams = Array.from({ length: RAIN_HEIGHT }, () => Array(width).fill(" "));
2663
+ const chars = "日ハミヒーウシナモニサワツオリアホマケメエカキムユラセネスタヌヘ0123456789";
2664
+ for (let frame = 0; frame < 9; frame++) {
2665
+ for (let y = RAIN_HEIGHT - 1; y > 0; y--) streams[y] = [...streams[y - 1]];
2666
+ streams[0] = Array(width).fill(" ").map(() => {
2667
+ if (Math.random() < .25) return { char: chars[Math.floor(Math.random() * 42)] };
2668
+ return " ";
2669
+ });
2670
+ if (frame > 0) stdout.write(up(RAIN_HEIGHT));
2671
+ for (let y = 0; y < RAIN_HEIGHT; y++) {
2672
+ let line = "";
2673
+ for (let x = 0; x < width; x++) {
2674
+ const cell = streams[y][x];
2675
+ if (cell === " ") line += " ";
2676
+ else {
2677
+ const typedCell = cell;
2678
+ const color = y === 0 ? b.pal.glow : y <= 2 ? b.pal.text : y <= 4 ? b.pal.base : b.pal.faint;
2679
+ line += `${color}${typedCell.char}${R}`;
2680
+ }
2681
+ }
2682
+ stdout.write((frame > 0 ? CLR : "") + line + "\n");
2683
+ }
2684
+ await sleep(90);
2685
+ }
2686
+ stdout.write(up(RAIN_HEIGHT));
2687
+ for (let y = 0; y < RAIN_HEIGHT; y++) stdout.write(CLR + "\n");
2688
+ stdout.write(up(RAIN_HEIGHT));
2689
+ printBox(b);
2690
+ };
2691
+ const ANIMS = [
2692
+ animTypewriter,
2693
+ animWaveReveal,
2694
+ animSlide,
2695
+ animPulseBorder,
2696
+ animGlitch,
2697
+ animMatrixRain
2698
+ ];
2699
+ function playStartupDisplay() {
2700
+ if (!stdout.isTTY) return Promise.resolve();
2701
+ return (async () => {
2702
+ try {
2703
+ const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
2704
+ const pal = PALETTES[Math.floor(Math.random() * PALETTES.length)];
2705
+ const b = makeBox(quote, pal);
2706
+ const anim = ANIMS[Math.floor(Math.random() * ANIMS.length)];
2707
+ stdout.write("\n" + HIDE);
2708
+ await anim(b);
2709
+ stdout.write(SHOW);
2710
+ } catch {
2711
+ try {
2712
+ stdout.write(SHOW);
2713
+ } catch {}
2714
+ }
2715
+ })();
2716
+ }
2717
+ //#endregion
1818
2718
  //#region src/cli.ts
1819
2719
  const args = process.argv.slice(2);
1820
2720
  if (new Set([
@@ -1823,14 +2723,16 @@ if (new Set([
1823
2723
  "resolve",
1824
2724
  "comments",
1825
2725
  "url",
1826
- "mcp"
2726
+ "mcp",
2727
+ "plan",
2728
+ "update"
1827
2729
  ]).has(args[0])) {
1828
2730
  if (args[0] === "mcp") {
1829
- const { startMcpServer } = await import("./mcp-BQmH_By-.mjs");
2731
+ const { startMcpServer } = await import("./mcp-CFeE13n-.mjs");
1830
2732
  await startMcpServer();
1831
2733
  await new Promise(() => {});
1832
2734
  }
1833
- const { runSubcommand } = await import("./cli-agent-DZBvZoXZ.mjs");
2735
+ const { runSubcommand } = await import("./cli-agent-BNwY5lZE.mjs");
1834
2736
  process.exit(await runSubcommand(args[0], args.slice(1)));
1835
2737
  }
1836
2738
  const opts = parseDiffOptions(args);
@@ -1853,9 +2755,19 @@ if (opts.outputMode === "terminal") {
1853
2755
  const exitCode = runTerminalDiff(opts);
1854
2756
  process.exit(exitCode);
1855
2757
  }
2758
+ const __pkgDir = dirname(fileURLToPath(import.meta.url));
2759
+ const currentVersion = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
2760
+ const updateCheckPromise = (async () => {
2761
+ try {
2762
+ const { checkForUpdates } = await import("./update-check-KL-2sGxE.mjs");
2763
+ return await checkForUpdates(currentVersion);
2764
+ } catch {
2765
+ return null;
2766
+ }
2767
+ })();
1856
2768
  const port = await getPort(opts.port ? { port: opts.port } : void 0);
1857
2769
  const host = opts.host;
1858
- const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "client");
2770
+ const clientDir = resolve(__pkgDir, "client");
1859
2771
  const { existsSync: existsSync$1 } = await import("node:fs");
1860
2772
  const { port: actualPort } = await startServer({
1861
2773
  port,
@@ -1871,18 +2783,24 @@ try {
1871
2783
  } catch {
1872
2784
  repoRoot = process.cwd();
1873
2785
  }
1874
- const __pkgDir = dirname(fileURLToPath(import.meta.url));
1875
- const version = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
1876
2786
  writeServerLock({
1877
2787
  port: actualPort,
1878
2788
  host,
1879
2789
  pid: process.pid,
1880
2790
  repoRoot,
1881
2791
  startedAt: Date.now(),
1882
- version
2792
+ version: currentVersion
1883
2793
  });
1884
2794
  } catch {}
1885
2795
  console.log(`diffing server running at ${localUrl}`);
2796
+ await playStartupDisplay();
2797
+ try {
2798
+ const updateInfo = await updateCheckPromise;
2799
+ if (updateInfo?.hasUpdate) {
2800
+ const { printUpdateDisclaimer } = await import("./update-check-KL-2sGxE.mjs");
2801
+ printUpdateDisclaimer(currentVersion, updateInfo.latestVersion);
2802
+ }
2803
+ } catch {}
1886
2804
  if (!opts.noOpen) {
1887
2805
  const settings = loadSettings();
1888
2806
  const openUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;