@computesdk/loops 0.1.5 → 0.1.6

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 (69) hide show
  1. package/API.md +15 -1
  2. package/README.md +9 -0
  3. package/client.ts +2 -8
  4. package/dist/assets/{abnfDiagram-VCTEODGH-B9RuyvBD.js → abnfDiagram-VCTEODGH-BmAHMdfH.js} +1 -1
  5. package/dist/assets/{arc-oSrRugMo.js → arc-D_2zELzk.js} +1 -1
  6. package/dist/assets/{architectureDiagram-5GKGNRK7-CEzHv5eh.js → architectureDiagram-5GKGNRK7-DbtMlGE1.js} +1 -1
  7. package/dist/assets/{blockDiagram-I7D4REHJ-B3sDGRLX.js → blockDiagram-I7D4REHJ-oeouAQDw.js} +1 -1
  8. package/dist/assets/{c4Diagram-7LVT6UL2-D00KlNWl.js → c4Diagram-7LVT6UL2-BRo4V2Z7.js} +1 -1
  9. package/dist/assets/channel-GK1ExE_g.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-BN48Ke2K.js → chunk-2Q5K7J3B-1yTKgMZ4.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-9sDw2Q8Q.js → chunk-5VM5RSS4-BsMua1fA.js} +1 -1
  12. package/dist/assets/{chunk-F27PBJKO-BhGYOTAs.js → chunk-F27PBJKO-BFx0TMpb.js} +1 -1
  13. package/dist/assets/{chunk-IMKFNOWR-B_ltGfuo.js → chunk-IMKFNOWR-D8UfXi3T.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-BTV5s7zl.js → chunk-JWPE2WC7-bBfJHxEJ.js} +1 -1
  15. package/dist/assets/{chunk-POPQ4Y6H-BBbmsZMU.js → chunk-POPQ4Y6H-CLnUbfSj.js} +1 -1
  16. package/dist/assets/{chunk-SVP7TREG-BBVuPqvz.js → chunk-SVP7TREG-uWuiRTo9.js} +1 -1
  17. package/dist/assets/{chunk-TICWLB2K-DGOhII0-.js → chunk-TICWLB2K-DV5DzCt1.js} +1 -1
  18. package/dist/assets/{chunk-XXDRQBXY-O9n8jFl3.js → chunk-XXDRQBXY-DflGukDx.js} +1 -1
  19. package/dist/assets/classDiagram-ZZMXUADV-veE8mBSk.js +1 -0
  20. package/dist/assets/classDiagram-v2-VYDZK3BY-veE8mBSk.js +1 -0
  21. package/dist/assets/{cose-bilkent-JH36ORCC-Bg-fKdMW.js → cose-bilkent-JH36ORCC-CXrAPLcM.js} +1 -1
  22. package/dist/assets/{cynefin-OW5HDTMX-C0JTyL1D.js → cynefin-OW5HDTMX-BZAor1Fd.js} +1 -1
  23. package/dist/assets/{cynefinDiagram-5FMLGOSQ-BKEcSvZC.js → cynefinDiagram-5FMLGOSQ-2mtwTNrl.js} +1 -1
  24. package/dist/assets/{dagre-GXQ25YYZ-CF35QAK7.js → dagre-GXQ25YYZ-BpwBh8sN.js} +1 -1
  25. package/dist/assets/{diagram-S7CK7UJ4-uYVv0n_8.js → diagram-S7CK7UJ4-BtWd6QOl.js} +1 -1
  26. package/dist/assets/{diagram-UQ7AKVKN-gJyJsotV.js → diagram-UQ7AKVKN-918gle9R.js} +1 -1
  27. package/dist/assets/{diagram-VSXAHHWV-CIbzn0k5.js → diagram-VSXAHHWV-BrjIR_Ik.js} +1 -1
  28. package/dist/assets/{diagram-VX7I27RA-B3NSrsYt.js → diagram-VX7I27RA-C28OpNTD.js} +1 -1
  29. package/dist/assets/{diagram-Z3DM3KII-Dtn85G8Z.js → diagram-Z3DM3KII-m__eZu-M.js} +1 -1
  30. package/dist/assets/{ebnfDiagram-PWID7BFC-Bk3kpRQ6.js → ebnfDiagram-PWID7BFC-DX1w-kGZ.js} +1 -1
  31. package/dist/assets/{erDiagram-RLTQ6QDP-CQdpI2Az.js → erDiagram-RLTQ6QDP-B7Ixmqav.js} +1 -1
  32. package/dist/assets/{flowDiagram-HODETNUW-DFEdxXsx.js → flowDiagram-HODETNUW-Bom5HiIz.js} +1 -1
  33. package/dist/assets/{ganttDiagram-EL5Y4UJY-3M3mh5KH.js → ganttDiagram-EL5Y4UJY-oGHhHPme.js} +1 -1
  34. package/dist/assets/{gitGraphDiagram-WWUBYQGX-D-VPIWMG.js → gitGraphDiagram-WWUBYQGX-BpdyfMmU.js} +1 -1
  35. package/dist/assets/{highlighted-body-JLMTFACH-CXpgazVU.js → highlighted-body-JLMTFACH-DqYsmdT7.js} +1 -1
  36. package/dist/assets/index-XP9FiM_q.js +902 -0
  37. package/dist/assets/{infoDiagram-27XIBGKW-mtm2MQD4.js → infoDiagram-27XIBGKW-CwHXRfO4.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-5VMMS53U-CCnoKccg.js → ishikawaDiagram-5VMMS53U-CDMEI2BQ.js} +1 -1
  39. package/dist/assets/{journeyDiagram-3NMN7TZE-DGGM7lRo.js → journeyDiagram-3NMN7TZE-Ba4NonhC.js} +1 -1
  40. package/dist/assets/{kanban-definition-UXKFOSKX-BGmjik6L.js → kanban-definition-UXKFOSKX-B7X_m14V.js} +1 -1
  41. package/dist/assets/{layout-DMtoi_oP.js → layout-CYu8n8qm.js} +1 -1
  42. package/dist/assets/{linear-B9xGDkRB.js → linear-BBLkaaSf.js} +1 -1
  43. package/dist/assets/{mindmap-definition-YA3MSWOX-VS9bTKR0.js → mindmap-definition-YA3MSWOX-BEACV2JJ.js} +1 -1
  44. package/dist/assets/{pegDiagram-XKGWAZYB-_umSwImn.js → pegDiagram-XKGWAZYB-BpkdZ-6G.js} +1 -1
  45. package/dist/assets/{pieDiagram-E7YTZNPT-DwYDfmi7.js → pieDiagram-E7YTZNPT-_DiRoL0F.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-AXDQQJYC-DKU_Wsvy.js → quadrantDiagram-AXDQQJYC-CG5zvdga.js} +1 -1
  47. package/dist/assets/{railroadDiagram-O6MQD6OU-DXB5lt92.js → railroadDiagram-O6MQD6OU-G4hMBXmt.js} +1 -1
  48. package/dist/assets/{requirementDiagram-BXWQKSXE-Dvnijc9_.js → requirementDiagram-BXWQKSXE-BkZ5guig.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-P5KCCOFB-B0GfeizJ.js → sankeyDiagram-P5KCCOFB-C5PzZw7u.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-WJ2MYXX4-UZPfweQ-.js → sequenceDiagram-WJ2MYXX4-UAlhcqTK.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CZMGAWQe.js → sizeCapture-INFHLROL-DNRbEYss.js} +1 -1
  52. package/dist/assets/{stateDiagram-D77RDMKH-B3JpYCVK.js → stateDiagram-D77RDMKH-DPCeQvnD.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-MP3YSRHH-CtZUrpem.js +1 -0
  54. package/dist/assets/{swimlanes-42K2YHIH-CZcyw1wj.js → swimlanes-42K2YHIH-Dr76qlly.js} +1 -1
  55. package/dist/assets/swimlanesDiagram-VR7AAH4N-CWvCwFtT.js +8 -0
  56. package/dist/assets/{timeline-definition-24CTP7MA-va5MgQAQ.js → timeline-definition-24CTP7MA-jelGqxUz.js} +1 -1
  57. package/dist/assets/{vennDiagram-4TSXK5OY-DolghlVs.js → vennDiagram-4TSXK5OY-CfIVRv2v.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VM6X3IG4-DjA12Q4V.js → wardleyDiagram-VM6X3IG4-BM5Zu9IN.js} +1 -1
  59. package/dist/assets/{xychartDiagram-S5SC5T6Z-C0gKPjZI.js → xychartDiagram-S5SC5T6Z-DYG0fGby.js} +1 -1
  60. package/dist/index.html +1 -1
  61. package/dist/server.mjs +162 -46
  62. package/package.json +3 -3
  63. package/src/shared/types.ts +13 -1
  64. package/dist/assets/channel-d6swnjWD.js +0 -1
  65. package/dist/assets/classDiagram-ZZMXUADV-BIQ2Cra4.js +0 -1
  66. package/dist/assets/classDiagram-v2-VYDZK3BY-BIQ2Cra4.js +0 -1
  67. package/dist/assets/index-rRV0z5I8.js +0 -902
  68. package/dist/assets/stateDiagram-v2-MP3YSRHH-DcAN0ZgF.js +0 -1
  69. package/dist/assets/swimlanesDiagram-VR7AAH4N-ColO3r6O.js +0 -8
package/dist/server.mjs CHANGED
@@ -2923,6 +2923,7 @@ function buildRun({ cwd, id, entries, live }) {
2923
2923
  model: pretty(a.model),
2924
2924
  style: a.style,
2925
2925
  status: a.status ?? "done",
2926
+ cut: a.cut,
2926
2927
  score,
2927
2928
  kept,
2928
2929
  humanPick: !!(kept && (r.human || chosen.round === r.n && !!chosen.label && chosen.label !== r.pick)),
@@ -2956,6 +2957,7 @@ function buildRun({ cwd, id, entries, live }) {
2956
2957
  judgeCost: r.judgeCost ?? void 0,
2957
2958
  startedAt: r.startedAt ?? void 0,
2958
2959
  endedAt: r.endedAt ?? void 0,
2960
+ status: r.status,
2959
2961
  inconclusive: !!r.inconclusive,
2960
2962
  unseen: r.unseen ?? [],
2961
2963
  ...r.escaped ? { escaped: r.escaped } : {}
@@ -2979,8 +2981,19 @@ function buildRun({ cwd, id, entries, live }) {
2979
2981
  }));
2980
2982
  const round = { n, from: { round: n - 1, label: "CURRENT" }, attempts, inProgress: true, judging: /judg/.test(live.phase ?? ""), why: void 0 };
2981
2983
  const at = rounds.findIndex((r) => r.n === n);
2982
- if (at >= 0) rounds[at] = { ...rounds[at], inProgress: true, judging: round.judging };
2983
- else rounds.push(round);
2984
+ if (at >= 0) {
2985
+ const saved = rounds[at].attempts;
2986
+ const incoming = new Map(attempts.map((a) => [a.name, a]));
2987
+ rounds[at] = {
2988
+ ...rounds[at],
2989
+ inProgress: true,
2990
+ judging: round.judging,
2991
+ attempts: [
2992
+ ...saved.map((a) => incoming.has(a.name) ? { ...a, ...incoming.get(a.name), hasSession: a.hasSession || incoming.get(a.name).hasSession } : a),
2993
+ ...attempts.filter((a) => !saved.some((old) => old.name === a.name))
2994
+ ]
2995
+ };
2996
+ } else rounds.push(round);
2984
2997
  }
2985
2998
  const bestRound = state?.best?.round ?? roundEntries.filter((r) => r.pick && r.pick !== "CURRENT").at(-1)?.n ?? 0;
2986
2999
  const bestEntry = roundEntries.filter((r) => r.n === bestRound).at(-1);
@@ -2992,12 +3005,27 @@ function buildRun({ cwd, id, entries, live }) {
2992
3005
  const firstT = Number(roundEntries[0]?.startedAt ?? Date.parse(head.at ?? "") ?? Date.now());
2993
3006
  log.push({ t: Number.isFinite(firstT) ? firstT : Date.now(), kind: "start", text: `Started ${head.goal ?? state?.goal ?? ""}`.trim() });
2994
3007
  for (const r of roundEntries) {
2995
- log.push({ t: Number(r.endedAt ?? r.at ?? Date.now()), kind: "judged", text: "", round: r.n, pick: r.pick ?? void 0, why: r.why ?? void 0, scores: r.scores ?? void 0 });
3008
+ const judged = (!r.status || r.status === "done") && !!r.pick && !r.inconclusive && !r.unseen?.length;
3009
+ if (judged) log.push({ t: Number(r.endedAt ?? r.at ?? Date.now()), kind: "judged", text: "", round: r.n, pick: r.pick, why: r.why ?? void 0, scores: r.scores ?? void 0 });
3010
+ else if (r.status !== "running") log.push({
3011
+ t: Number(r.endedAt ?? r.at ?? Date.now()),
3012
+ kind: "round",
3013
+ round: r.n,
3014
+ text: `Round ${r.n}: ${r.status === "aborted" ? "stopped before comparison completed" : r.status === "error" ? "failed before comparison completed" : "no completed comparison"}.`
3015
+ });
2996
3016
  }
2997
3017
  const wrote = state?.status ?? head.status ?? "done";
2998
3018
  const stalled = !alive && wrote === "running";
2999
3019
  const status = alive ? "running" : stalled ? "stalled" : wrote;
3000
- if (!alive && (wrote === "done" || wrote === "aborted") && roundEntries.length) log.push({ t: Number(roundEntries.at(-1)?.endedAt ?? Date.now()), kind: "done", text: `finished after ${roundEntries.length} round(s)` });
3020
+ if (!alive && ["done", "aborted", "error"].includes(wrote)) {
3021
+ const reason = state?.stopReason ?? state?.error;
3022
+ const compared = log.filter((e) => e.kind === "judged").length;
3023
+ log.push({
3024
+ t: Number(roundEntries.at(-1)?.endedAt ?? Date.now()),
3025
+ kind: "done",
3026
+ text: wrote === "aborted" ? `Stopped; worktrees kept.${reason ? ` ${reason}` : ""}` : wrote === "error" ? `Failed: ${reason || "run failed before a usable result"}.` : compared ? `finished after ${compared} compared round(s)` : "Ended with no completed comparison."
3027
+ });
3028
+ }
3001
3029
  const tokens = roundEntries.reduce((sum, r) => sum + (r.attempts ?? []).reduce((s, a) => s + (a.tokens ?? 0), 0) + (r.judgeTokens ?? 0), 0) + (live ? (live.attempts ?? []).reduce((s, a) => s + (a.tokens ?? 0), 0) : 0);
3002
3030
  return {
3003
3031
  id,
@@ -3009,7 +3037,7 @@ function buildRun({ cwd, id, entries, live }) {
3009
3037
  goal: state?.goal ?? head.goal ?? "",
3010
3038
  status: status === "aborted" ? "done" : status,
3011
3039
  alive,
3012
- stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : void 0,
3040
+ stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : state?.stopReason ?? state?.error,
3013
3041
  rules: rules.map((text) => ({ text, source: "config" })),
3014
3042
  rounds,
3015
3043
  best: {
@@ -3056,7 +3084,15 @@ function summarize(cwd, entries) {
3056
3084
  lastT = t;
3057
3085
  const bestRound = state?.best?.round ?? rounds.filter((r) => r.pick && r.pick !== "CURRENT").at(-1)?.n;
3058
3086
  const bestEntry = rounds.find((r) => r.n === bestRound);
3059
- latest = { name: goalName(state?.goal ?? head.goal ?? id), status: stalled ? "stalled" : status, kind: head.kind ?? state?.kind ?? "visual", score: state?.best?.score ?? (bestEntry?.pick ? bestEntry.scores?.[bestEntry.pick] : void 0), unit: state?.metric?.unit ?? rounds.at(-1)?.metric?.unit ?? "", rounds: rounds.length };
3087
+ const attempts = rounds.flatMap((r) => r.attempts ?? []);
3088
+ const failures = /* @__PURE__ */ new Map();
3089
+ for (const a of attempts) {
3090
+ const why = a.status === "budget" ? "token budget" : a.status === "timeout" ? "timed out" : String(a.status).startsWith("error") ? "error" : a.status === "aborted" ? "stopped" : null;
3091
+ if (why) failures.set(why, (failures.get(why) ?? 0) + 1);
3092
+ }
3093
+ const details = [...failures].map(([why, n]) => `${n} ${why}`).join(", ");
3094
+ const outcome = alive ? void 0 : status === "aborted" ? "Stopped \u2014 worktrees kept" : rounds.at(-1)?.inconclusive || rounds.at(-1)?.unseen?.length ? "Inconclusive \u2014 judging incomplete" : attempts.length && attempts.every((a) => !["done", "unchanged", "no-change"].includes(a.status)) ? `No candidates completed${details ? ` \xB7 ${details}` : ""}` : status === "error" ? `Failed: ${state?.stopReason ?? state?.error ?? "run failed"}` : !rounds.length ? "No usable result \u2014 no rounds completed" : void 0;
3095
+ latest = { name: goalName(state?.goal ?? head.goal ?? id), status: stalled ? "stalled" : status, kind: head.kind ?? state?.kind ?? "visual", score: state?.best?.score ?? (bestEntry?.pick ? bestEntry.scores?.[bestEntry.pick] : void 0), unit: state?.metric?.unit ?? rounds.at(-1)?.metric?.unit ?? "", rounds: rounds.length, ...outcome ? { outcome } : {} };
3060
3096
  }
3061
3097
  }
3062
3098
  return { loops: ids.length, running, open, question, lastT, latest };
@@ -3278,6 +3314,12 @@ var Chat = class {
3278
3314
  }
3279
3315
  onEvent(event) {
3280
3316
  const type = event.type;
3317
+ if (type === "extension_error") {
3318
+ this.notices.push({ t: Date.now(), level: "error", message: String(event.error ?? event.message ?? "Extension command failed") });
3319
+ this.notices = this.notices.slice(-20);
3320
+ this.broadcast(true);
3321
+ return;
3322
+ }
3281
3323
  if (type === "message_start" && event.message?.role === "assistant") {
3282
3324
  this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: "", text: "", tools: [] };
3283
3325
  this.broadcast();
@@ -3330,11 +3372,18 @@ var Chat = class {
3330
3372
  this.touch();
3331
3373
  }
3332
3374
  async prompt(text, behavior) {
3333
- if (!this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3375
+ const previous = this.live;
3376
+ if (!text.startsWith("/") && !this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3334
3377
  this.broadcast(true);
3335
- const r = await this.pi.prompt(text, behavior);
3336
- setTimeout(() => void this.refresh(), 400);
3337
- return r?.disposition ?? "started";
3378
+ try {
3379
+ const r = await this.pi.prompt(text, behavior);
3380
+ setTimeout(() => void this.refresh(), 400);
3381
+ return r?.disposition ?? "started";
3382
+ } catch (error) {
3383
+ if (!previous && this.live?.user === text) this.live = null;
3384
+ this.broadcast(true);
3385
+ throw error;
3386
+ }
3338
3387
  }
3339
3388
  /** The child's own view of "am I busy": a turn that settled with no event reaching us must not strand the UI. */
3340
3389
  async settleIfIdle() {
@@ -3378,11 +3427,16 @@ var Chat = class {
3378
3427
  this.answeringUi.delete(id);
3379
3428
  }
3380
3429
  }
3430
+ aborting = false;
3381
3431
  async abort() {
3382
- await this.pi.send({ type: "abort" }).catch(() => {
3383
- });
3384
- this.live = null;
3385
- this.broadcast(true);
3432
+ if (this.aborting) throw new Error("Stop agent is already in flight.");
3433
+ this.aborting = true;
3434
+ try {
3435
+ await this.pi.send({ type: "abort" }, 15e3);
3436
+ await this.settleIfIdle();
3437
+ } finally {
3438
+ this.aborting = false;
3439
+ }
3386
3440
  }
3387
3441
  /** The newest session entry of a kind (`loops-doctor`, `loops-run`, …), for read-only views. */
3388
3442
  custom(type) {
@@ -4740,7 +4794,7 @@ var withLive = (state) => {
4740
4794
  return state;
4741
4795
  };
4742
4796
  var streams = /* @__PURE__ */ new Set();
4743
- var snapshotOf = (chat, wsId) => withLive({ ...chat.snapshot(), chats: chatsOf(wsId), workspaces: workspaces.list(), ws: wsId, proposals });
4797
+ var snapshotOf = (chat, wsId) => withLive({ ...chat.snapshot(), chats: chatsOf(wsId), workspaces: workspaces.list(), ws: wsId, proposals, configErrors: [...configErrors.values()].filter((e) => e.ws === wsId) });
4744
4798
  manager.onChange((chat) => {
4745
4799
  const space = workspaces.ofChat(chat.id);
4746
4800
  const state = snapshotOf(chat, space.id);
@@ -4748,6 +4802,7 @@ manager.onChange((chat) => {
4748
4802
  });
4749
4803
  var watches = /* @__PURE__ */ new Map();
4750
4804
  var proposals = [];
4805
+ var configErrors = /* @__PURE__ */ new Map();
4751
4806
  var watchOf = (wsId) => {
4752
4807
  const ws = workspaces.get(wsId);
4753
4808
  let w = watches.get(ws.id);
@@ -4765,7 +4820,14 @@ var watchOf = (wsId) => {
4765
4820
  fs9.watch(path9.join(ws.path, ".pi"), (_e, name) => {
4766
4821
  if (name && name !== "loops.json") return;
4767
4822
  clearTimeout(watchTimers.get(ws.id));
4768
- watchTimers.set(ws.id, setTimeout(() => checkConfig(ws.id), 120));
4823
+ watchTimers.set(ws.id, setTimeout(() => {
4824
+ try {
4825
+ checkConfig(ws.id);
4826
+ } catch (e) {
4827
+ configErrors.set(ws.id, { ws: ws.id, file: path9.join(ws.path, ".pi/loops.json"), error: `Could not validate settings: ${e.message}. Fix the file or save settings again.` });
4828
+ notifyProposals();
4829
+ }
4830
+ }, 120));
4769
4831
  });
4770
4832
  } catch {
4771
4833
  }
@@ -4774,12 +4836,18 @@ var watchOf = (wsId) => {
4774
4836
  };
4775
4837
  var watchTimers = /* @__PURE__ */ new Map();
4776
4838
  function notifyProposals() {
4777
- const payload = JSON.stringify({ type: "proposals", proposals });
4778
4839
  for (const s of streams) {
4779
4840
  try {
4780
4841
  s.send("proposals", proposals);
4781
4842
  } catch {
4782
4843
  }
4844
+ const chat = manager.openChats().find((chat2) => chat2.id === s.chat);
4845
+ if (chat) {
4846
+ try {
4847
+ s.send("state", snapshotOf(chat, workspaces.ofChat(chat.id).id), chat.entryCursor);
4848
+ } catch {
4849
+ }
4850
+ }
4783
4851
  }
4784
4852
  }
4785
4853
  function checkConfig(wsId) {
@@ -4792,14 +4860,33 @@ function checkConfig(wsId) {
4792
4860
  return "";
4793
4861
  }
4794
4862
  })();
4863
+ let next;
4864
+ try {
4865
+ next = raw2.trim() ? JSON.parse(raw2) : {};
4866
+ if (!next || typeof next !== "object" || Array.isArray(next)) throw new Error("expected a JSON object");
4867
+ } catch (e) {
4868
+ let recovery = "";
4869
+ try {
4870
+ const backup = `${CONFIG}.invalid-${Date.now()}`;
4871
+ fs9.writeFileSync(backup, raw2);
4872
+ restoreConfig(w.cwd, w.accepted);
4873
+ recovery = ` Last accepted settings restored. Rejected text saved in ${backup}.`;
4874
+ } catch (writeError) {
4875
+ recovery = ` Could not restore settings: ${writeError.message}. Fix ${CONFIG} before running another loop.`;
4876
+ }
4877
+ configErrors.set(wsId, { ws: wsId, file: CONFIG, error: `Invalid config JSON: ${e.message}.${recovery}` });
4878
+ notifyProposals();
4879
+ return;
4880
+ }
4795
4881
  if (isSelfWriting() || isSelfWrite(raw2)) {
4796
- w.accepted = raw2.trim() ? JSON.parse(raw2) : {};
4882
+ w.accepted = next;
4797
4883
  return;
4798
4884
  }
4799
- const next = raw2.trim() ? JSON.parse(raw2) : {};
4885
+ const hadError = configErrors.delete(wsId);
4800
4886
  const changes = diffConfig(w.accepted, next);
4801
4887
  if (!changes.length) {
4802
4888
  w.accepted = next;
4889
+ if (hadError) notifyProposals();
4803
4890
  return;
4804
4891
  }
4805
4892
  restoreConfig(w.cwd, w.accepted);
@@ -4912,7 +4999,7 @@ api.get("/state", async (c) => {
4912
4999
  if (!id) return c.json({ error: "chat required" }, 400);
4913
5000
  const chat = await manager.open(id);
4914
5001
  const ws = workspaces.ofChat(id);
4915
- return c.json(withLive({ ...chat.snapshot(), chats: chatsOf(ws.id), workspaces: workspaces.list(), ws: ws.id, proposals }));
5002
+ return c.json(snapshotOf(chat, ws.id));
4916
5003
  });
4917
5004
  api.post("/chat", async (c) => {
4918
5005
  const body = await c.req.json().catch(() => ({}));
@@ -4988,10 +5075,15 @@ api.post("/rename", async (c) => {
4988
5075
  return c.json({ ok: true, title: chat.title });
4989
5076
  });
4990
5077
  api.post("/abort", async (c) => {
4991
- const { chat: id } = await c.req.json();
4992
- const chat = await manager.open(String(id));
4993
- await chat.abort();
4994
- return c.json({ ok: true });
5078
+ const { chat: id } = await c.req.json().catch(() => ({})) ?? {};
5079
+ if (typeof id !== "string" || !id.trim()) return c.json({ error: "chat required" }, 400);
5080
+ const chat = await manager.open(id);
5081
+ try {
5082
+ await chat.abort();
5083
+ return c.json({ ok: true, requested: true });
5084
+ } catch (e) {
5085
+ return c.json({ error: e.message }, 409);
5086
+ }
4995
5087
  });
4996
5088
  api.post("/chat/retry", async (c) => {
4997
5089
  const { chat: id } = await c.req.json();
@@ -5019,29 +5111,51 @@ api.post("/answer", async (c) => {
5019
5111
  await chat.prompt(`/loop answer ${String(question)} ${String(choice)}`);
5020
5112
  return c.json({ ok: true });
5021
5113
  });
5114
+ var steering = /* @__PURE__ */ new Set();
5022
5115
  api.post("/steer", async (c) => {
5023
- const { chat: id, run: run3, action, rule, attempt, text } = await c.req.json();
5024
- const chat = await manager.open(String(id));
5116
+ const { chat: id, run: run3, action, rule, attempt, text } = await c.req.json().catch(() => ({})) ?? {};
5117
+ const token = (v) => typeof v === "string" && /^[\w.-]+$/.test(v);
5118
+ if (!token(id) || !token(run3)) return c.json({ error: "chat and exact run ID required" }, 400);
5119
+ if (!["pick", "take", "apply", "stop", "clean", "rule", "talk", "more"].includes(action)) return c.json({ error: "valid action required" }, 400);
5120
+ if (["pick", "take", "talk"].includes(action) && !token(attempt)) return c.json({ error: "attempt required (durable name, e.g. R1-safe)" }, 400);
5121
+ if (action === "rule" && (typeof rule !== "string" || !rule.trim())) return c.json({ error: "rule required" }, 400);
5122
+ if (action === "talk" && (typeof text !== "string" || !text.trim())) return c.json({ error: "text required" }, 400);
5123
+ if (action === "more" && text !== void 0 && (!Number.isInteger(text) || text < 1 || text > 100)) return c.json({ error: "more rounds must be an integer from 1 to 100" }, 400);
5124
+ const chat = await manager.open(id);
5125
+ if (!chat.hasRun(run3)) return c.json({ error: "run does not belong to this chat" }, 404);
5126
+ if (action === "stop" && !chat.snapshot().runs.find((r) => r.id === run3)?.alive) return c.json({ error: "Selected run is not active. Read its latest session state." }, 409);
5127
+ if (["pick", "take", "talk"].includes(action) && !chat.snapshot().runs.find((r) => r.id === run3)?.rounds.some((r) => r.attempts.some((a) => a.name === attempt))) return c.json({ error: "unknown attempt for selected run" }, 400);
5128
+ const key = `${id}:${run3}:${action}`;
5129
+ if (steering.has(key)) return c.json({ error: "This action is already in flight." }, 409);
5130
+ steering.add(key);
5025
5131
  const slash = (cmd) => chat.prompt(cmd);
5026
- switch (String(action)) {
5027
- case "pick":
5028
- case "take":
5029
- return c.json({ ok: true, sent: await slash(`/loop take ${attempt} ${run3}`) });
5030
- case "apply":
5031
- return c.json({ ok: true, sent: await slash(`/loop apply`) });
5032
- case "stop":
5033
- return c.json({ ok: true, sent: await slash(`/loop stop`) });
5034
- case "clean":
5035
- return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5036
- case "rule":
5037
- return c.json({ ok: true, sent: await slash(`/loop rule ${run3} ${rule}`) });
5038
- case "talk":
5039
- return c.json({ ok: true, sent: await slash(`/loop talk ${attempt} ${text}`) });
5040
- // everything else is a decision only the agent can make (another round, a different direction)
5041
- case "more":
5042
- return c.json({ ok: true, sent: await slash(`Continue loop ${run3} with ${Number(text) || 2} more rounds. Check loop_status first, then call loop({ from: "${run3}", rounds: ${Number(text) || 2} }).`) });
5043
- default:
5044
- return c.json({ error: `unknown action ${action}` }, 400);
5132
+ try {
5133
+ switch (String(action)) {
5134
+ case "pick":
5135
+ case "take":
5136
+ return c.json({ ok: true, sent: await slash(`/loop take ${attempt} ${run3}`) });
5137
+ case "apply":
5138
+ return c.json({ ok: true, sent: await slash(`/loop apply ${run3}`) });
5139
+ // Pi executes extension commands before its streaming queue. Do not send this as a queued follow-up:
5140
+ // a background round must remain independently stoppable while the parent is in a long-running tool.
5141
+ case "stop":
5142
+ return c.json({ ok: true, requested: true, sent: await slash(`/loop stop ${run3}`) });
5143
+ case "clean":
5144
+ return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5145
+ case "rule":
5146
+ return c.json({ ok: true, sent: await slash(`/loop rule ${run3} ${rule}`) });
5147
+ case "talk":
5148
+ return c.json({ ok: true, sent: await slash(`/loop talk ${attempt} ${text}`) });
5149
+ // everything else is a decision only the agent can make (another round, a different direction)
5150
+ case "more":
5151
+ return c.json({ ok: true, sent: await slash(`Continue loop ${run3} with ${Number(text) || 2} more rounds. Check loop_status first, then call loop({ from: "${run3}", rounds: ${Number(text) || 2} }).`) });
5152
+ default:
5153
+ return c.json({ error: `unknown action ${action}` }, 400);
5154
+ }
5155
+ } catch (e) {
5156
+ return c.json({ error: e.message }, 409);
5157
+ } finally {
5158
+ steering.delete(key);
5045
5159
  }
5046
5160
  });
5047
5161
  api.get("/ui", async (c) => {
@@ -5144,18 +5258,20 @@ api.post("/proposals/:id/keep", (c) => {
5144
5258
  api.get("/config", (c) => {
5145
5259
  const ws = wsOf(c.req.query("chat"));
5146
5260
  watchOf(ws.id);
5147
- return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path) });
5261
+ return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path), error: configErrors.get(ws.id)?.error });
5148
5262
  });
5149
5263
  api.post("/config", async (c) => {
5150
5264
  const { key, value, chat } = await c.req.json();
5151
5265
  const ws = wsOf(chat);
5152
5266
  try {
5153
5267
  writeSetting(ws.path, String(key), value);
5268
+ configErrors.delete(ws.id);
5154
5269
  const w = watchOf(ws.id);
5155
5270
  try {
5156
5271
  w.accepted = JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
5157
5272
  } catch {
5158
5273
  }
5274
+ notifyProposals();
5159
5275
  return c.json({ ok: true });
5160
5276
  } catch (e) {
5161
5277
  return c.json({ ok: false, error: String(e.message ?? e) }, 400);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@computesdk/loops",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Loops for people who do not live in a terminal: one command starts a localhost UI that runs, watches and steers hill-climbing agents in your repos.",
6
6
  "keywords": [
@@ -37,8 +37,8 @@
37
37
  "start": "node --experimental-strip-types server/index.ts",
38
38
  "check": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json",
39
39
  "shots": "node shots.mjs",
40
- "test": "esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts",
41
- "test:browser": "node test/workspace-browser.mjs && node test/pi-dialog-browser.mjs"
40
+ "test": "esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/stop-api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts",
41
+ "test:browser": "node test/workspace-browser.mjs && node test/pi-dialog-browser.mjs && node test/stop-browser.mjs"
42
42
  },
43
43
  "dependencies": {
44
44
  "@computesdk/pi-loops": "^1.0.7",
@@ -2,6 +2,14 @@
2
2
  // Pi's session entries (`loops-run`, `loops-round`, `loops-question`, tool results), the run's state file and
3
3
  // the worktrees it owns. Nothing polls: a live round arrives as `tool_execution_update` from the RPC child.
4
4
 
5
+ /** Engine actions. Attempt is the durable name (R1-safe), never the display letter (A). */
6
+ export type SteerAction =
7
+ | { action: "pick" | "take"; attempt: string }
8
+ | { action: "apply" | "stop" | "clean" }
9
+ | { action: "rule"; rule: string }
10
+ | { action: "talk"; attempt: string; text: string }
11
+ | { action: "more"; text?: number };
12
+
5
13
  export type RunAttempt = {
6
14
  /** `run:rN-L` */
7
15
  id: string;
@@ -12,6 +20,7 @@ export type RunAttempt = {
12
20
  style: string;
13
21
  /** running | done | error | unchanged | aborted | timeout | budget */
14
22
  status: string;
23
+ cut?: string;
15
24
  score?: number;
16
25
  kept?: boolean;
17
26
  humanPick?: boolean;
@@ -58,6 +67,7 @@ export type RunRound = {
58
67
  shots?: Record<string, string[]>;
59
68
  /** the judge never saw the images: nothing in this round was compared */
60
69
  inconclusive?: boolean;
70
+ status?: string;
61
71
  /** an attempt committed into your checkout; `restored` says whether it was put back */
62
72
  escaped?: { from: string; to: string; restored?: boolean } | null;
63
73
  /** candidates whose stamped code the judge could not read back */
@@ -137,7 +147,7 @@ export type ChatSummary = {
137
147
  /** the newest open question, if any */
138
148
  question?: { kind: string; round?: number; title: string };
139
149
  lastT: number;
140
- latest?: { name: string; status: string; kind: "visual" | "bench"; score?: number; unit?: string; rounds: number };
150
+ latest?: { name: string; status: string; kind: "visual" | "bench"; score?: number; unit?: string; rounds: number; outcome?: string };
141
151
  /** a run whose process went away: it says running, nothing is working */
142
152
  };
143
153
 
@@ -176,10 +186,12 @@ export type ChatState = {
176
186
  items: ParentItem[];
177
187
  live: ParentLive | null;
178
188
  error?: string;
189
+ notices?: { t: number; level: string; message: string }[];
179
190
  runs: Run[];
180
191
  model?: string | null;
181
192
  /** open questions for chats that are not loaded, so the sidebar can count them */
182
193
  busyElsewhere?: boolean;
183
194
  /** config writes waiting for a yes/no; the file is unchanged until one */
184
195
  proposals?: ConfigProposal[];
196
+ configErrors?: { ws: string; file: string; error: string }[];
185
197
  };
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./index-rRV0z5I8.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K-DGOhII0-.js";import{_ as i}from"./index-rRV0z5I8.js";import"./chunk-5VM5RSS4-9sDw2Q8Q.js";import"./chunk-XXDRQBXY-O9n8jFl3.js";import"./chunk-POPQ4Y6H-BBbmsZMU.js";import"./chunk-F27PBJKO-BhGYOTAs.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K-DGOhII0-.js";import{_ as i}from"./index-rRV0z5I8.js";import"./chunk-5VM5RSS4-9sDw2Q8Q.js";import"./chunk-XXDRQBXY-O9n8jFl3.js";import"./chunk-POPQ4Y6H-BBbmsZMU.js";import"./chunk-F27PBJKO-BhGYOTAs.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};