@computesdk/loops 0.1.5 → 0.1.7

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-BKE3wXte.js} +1 -1
  5. package/dist/assets/{arc-oSrRugMo.js → arc-C6ZC2rP7.js} +1 -1
  6. package/dist/assets/{architectureDiagram-5GKGNRK7-CEzHv5eh.js → architectureDiagram-5GKGNRK7-BP4EuLVY.js} +1 -1
  7. package/dist/assets/{blockDiagram-I7D4REHJ-B3sDGRLX.js → blockDiagram-I7D4REHJ-BLw7XXPz.js} +1 -1
  8. package/dist/assets/{c4Diagram-7LVT6UL2-D00KlNWl.js → c4Diagram-7LVT6UL2-Bx80SfZM.js} +1 -1
  9. package/dist/assets/channel-D148DRm1.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-BN48Ke2K.js → chunk-2Q5K7J3B-BPBebY_E.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-9sDw2Q8Q.js → chunk-5VM5RSS4-QL5btn2z.js} +1 -1
  12. package/dist/assets/{chunk-F27PBJKO-BhGYOTAs.js → chunk-F27PBJKO-DwZ1yutT.js} +1 -1
  13. package/dist/assets/{chunk-IMKFNOWR-B_ltGfuo.js → chunk-IMKFNOWR-D_oIgde6.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-BTV5s7zl.js → chunk-JWPE2WC7-B7803oA6.js} +1 -1
  15. package/dist/assets/{chunk-POPQ4Y6H-BBbmsZMU.js → chunk-POPQ4Y6H-BVGDPtWK.js} +1 -1
  16. package/dist/assets/{chunk-SVP7TREG-BBVuPqvz.js → chunk-SVP7TREG-LBP3GH9l.js} +1 -1
  17. package/dist/assets/{chunk-TICWLB2K-DGOhII0-.js → chunk-TICWLB2K-BI5m3CJb.js} +1 -1
  18. package/dist/assets/{chunk-XXDRQBXY-O9n8jFl3.js → chunk-XXDRQBXY-kxt8MH_7.js} +1 -1
  19. package/dist/assets/classDiagram-ZZMXUADV-CncavLbb.js +1 -0
  20. package/dist/assets/classDiagram-v2-VYDZK3BY-CncavLbb.js +1 -0
  21. package/dist/assets/{cose-bilkent-JH36ORCC-Bg-fKdMW.js → cose-bilkent-JH36ORCC-9ujaMun3.js} +1 -1
  22. package/dist/assets/{cynefin-OW5HDTMX-C0JTyL1D.js → cynefin-OW5HDTMX-D-nWdDZr.js} +1 -1
  23. package/dist/assets/{cynefinDiagram-5FMLGOSQ-BKEcSvZC.js → cynefinDiagram-5FMLGOSQ-CI6Scxp6.js} +1 -1
  24. package/dist/assets/{dagre-GXQ25YYZ-CF35QAK7.js → dagre-GXQ25YYZ-B8xNwtVY.js} +1 -1
  25. package/dist/assets/{diagram-S7CK7UJ4-uYVv0n_8.js → diagram-S7CK7UJ4-CbG9exCu.js} +1 -1
  26. package/dist/assets/{diagram-UQ7AKVKN-gJyJsotV.js → diagram-UQ7AKVKN-DJEVVOdt.js} +1 -1
  27. package/dist/assets/{diagram-VSXAHHWV-CIbzn0k5.js → diagram-VSXAHHWV-DXqMiZ59.js} +1 -1
  28. package/dist/assets/{diagram-VX7I27RA-B3NSrsYt.js → diagram-VX7I27RA-BgUsFlGi.js} +1 -1
  29. package/dist/assets/{diagram-Z3DM3KII-Dtn85G8Z.js → diagram-Z3DM3KII-15W3Mbq6.js} +1 -1
  30. package/dist/assets/{ebnfDiagram-PWID7BFC-Bk3kpRQ6.js → ebnfDiagram-PWID7BFC-QnWuy49w.js} +1 -1
  31. package/dist/assets/{erDiagram-RLTQ6QDP-CQdpI2Az.js → erDiagram-RLTQ6QDP-CtFAKfHK.js} +1 -1
  32. package/dist/assets/{flowDiagram-HODETNUW-DFEdxXsx.js → flowDiagram-HODETNUW-BXiSgIdj.js} +1 -1
  33. package/dist/assets/{ganttDiagram-EL5Y4UJY-3M3mh5KH.js → ganttDiagram-EL5Y4UJY-0X4MJoKa.js} +1 -1
  34. package/dist/assets/{gitGraphDiagram-WWUBYQGX-D-VPIWMG.js → gitGraphDiagram-WWUBYQGX-D1RSU4p4.js} +1 -1
  35. package/dist/assets/{highlighted-body-JLMTFACH-CXpgazVU.js → highlighted-body-JLMTFACH-DJpqqoLg.js} +1 -1
  36. package/dist/assets/index-B3Ugq2o6.js +902 -0
  37. package/dist/assets/{infoDiagram-27XIBGKW-mtm2MQD4.js → infoDiagram-27XIBGKW-DHPDHcTh.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-5VMMS53U-CCnoKccg.js → ishikawaDiagram-5VMMS53U-BaYQe1ZZ.js} +1 -1
  39. package/dist/assets/{journeyDiagram-3NMN7TZE-DGGM7lRo.js → journeyDiagram-3NMN7TZE-gQ5s_wv1.js} +1 -1
  40. package/dist/assets/{kanban-definition-UXKFOSKX-BGmjik6L.js → kanban-definition-UXKFOSKX-BxwSa57H.js} +1 -1
  41. package/dist/assets/{layout-DMtoi_oP.js → layout-thi9yMYl.js} +1 -1
  42. package/dist/assets/{linear-B9xGDkRB.js → linear-C3tNDt0i.js} +1 -1
  43. package/dist/assets/{mindmap-definition-YA3MSWOX-VS9bTKR0.js → mindmap-definition-YA3MSWOX-D4R4OXev.js} +1 -1
  44. package/dist/assets/{pegDiagram-XKGWAZYB-_umSwImn.js → pegDiagram-XKGWAZYB-BnVd3_9Z.js} +1 -1
  45. package/dist/assets/{pieDiagram-E7YTZNPT-DwYDfmi7.js → pieDiagram-E7YTZNPT-CwX8mvdJ.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-AXDQQJYC-DKU_Wsvy.js → quadrantDiagram-AXDQQJYC-CoiKagpY.js} +1 -1
  47. package/dist/assets/{railroadDiagram-O6MQD6OU-DXB5lt92.js → railroadDiagram-O6MQD6OU-DZCee713.js} +1 -1
  48. package/dist/assets/{requirementDiagram-BXWQKSXE-Dvnijc9_.js → requirementDiagram-BXWQKSXE-BCksgH3P.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-P5KCCOFB-B0GfeizJ.js → sankeyDiagram-P5KCCOFB-B0xOVx2_.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-WJ2MYXX4-UZPfweQ-.js → sequenceDiagram-WJ2MYXX4-8myX-w3k.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CZMGAWQe.js → sizeCapture-INFHLROL-DtAA6XEA.js} +1 -1
  52. package/dist/assets/{stateDiagram-D77RDMKH-B3JpYCVK.js → stateDiagram-D77RDMKH-eNo1EA8t.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-MP3YSRHH-BZn7zD-F.js +1 -0
  54. package/dist/assets/{swimlanes-42K2YHIH-CZcyw1wj.js → swimlanes-42K2YHIH-CuZGWGh2.js} +1 -1
  55. package/dist/assets/swimlanesDiagram-VR7AAH4N-BG9s9tCY.js +8 -0
  56. package/dist/assets/{timeline-definition-24CTP7MA-va5MgQAQ.js → timeline-definition-24CTP7MA-lyUBY_Xm.js} +1 -1
  57. package/dist/assets/{vennDiagram-4TSXK5OY-DolghlVs.js → vennDiagram-4TSXK5OY-BaFKzCtR.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VM6X3IG4-DjA12Q4V.js → wardleyDiagram-VM6X3IG4-DMcUcLi9.js} +1 -1
  59. package/dist/assets/{xychartDiagram-S5SC5T6Z-C0gKPjZI.js → xychartDiagram-S5SC5T6Z-BVotbEcG.js} +1 -1
  60. package/dist/index.html +1 -1
  61. package/dist/server.mjs +165 -47
  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,25 +2981,53 @@ 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);
2987
3000
  const bestLabel = state?.best?.label ?? bestEntry?.best?.label ?? head.best ?? null;
2988
3001
  const bestScore = state?.best?.score ?? (bestLabel ? bestEntry?.scores?.[bestLabel] : void 0) ?? void 0;
2989
3002
  const scores = roundEntries.map((r) => (r.pick && r.pick !== "CURRENT" ? r.scores?.[r.pick] : r.scores?.CURRENT) ?? (r.scores?.CURRENT ?? null)).map((v) => typeof v === "number" ? v : null).filter((v) => v != null);
2990
- const baseline = roundEntries[0]?.scores?.CURRENT ?? void 0;
3003
+ const originalBase = roundEntries[0]?.from ?? roundEntries[0]?.baseCommit ?? entries.find((e) => e.customType === STATE_ENTRY && (e.data?.run === id || e.data?.state?.id === id) && e.data?.state?.baseCommit)?.data?.state?.baseCommit ?? head.baseCommit;
3004
+ const measured = (v) => typeof v === "number" && Number.isFinite(v);
3005
+ const baseline = measured(roundEntries[0]?.scores?.CURRENT) ? roundEntries[0].scores.CURRENT : typeof originalBase === "string" && originalBase ? roundEntries.find((r) => r.from === originalBase && measured(r.scores?.CURRENT))?.scores.CURRENT : void 0;
2991
3006
  const log = [];
2992
3007
  const firstT = Number(roundEntries[0]?.startedAt ?? Date.parse(head.at ?? "") ?? Date.now());
2993
3008
  log.push({ t: Number.isFinite(firstT) ? firstT : Date.now(), kind: "start", text: `Started ${head.goal ?? state?.goal ?? ""}`.trim() });
2994
3009
  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 });
3010
+ const judged = (!r.status || r.status === "done") && !!r.pick && !r.inconclusive && !r.unseen?.length;
3011
+ 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 });
3012
+ else if (r.status !== "running") log.push({
3013
+ t: Number(r.endedAt ?? r.at ?? Date.now()),
3014
+ kind: "round",
3015
+ round: r.n,
3016
+ text: `Round ${r.n}: ${r.status === "aborted" ? "stopped before comparison completed" : r.status === "error" ? "failed before comparison completed" : "no completed comparison"}.`
3017
+ });
2996
3018
  }
2997
3019
  const wrote = state?.status ?? head.status ?? "done";
2998
3020
  const stalled = !alive && wrote === "running";
2999
3021
  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)` });
3022
+ if (!alive && ["done", "aborted", "error"].includes(wrote)) {
3023
+ const reason = state?.stopReason ?? state?.error;
3024
+ const compared = log.filter((e) => e.kind === "judged").length;
3025
+ log.push({
3026
+ t: Number(roundEntries.at(-1)?.endedAt ?? Date.now()),
3027
+ kind: "done",
3028
+ 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."
3029
+ });
3030
+ }
3001
3031
  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
3032
  return {
3003
3033
  id,
@@ -3009,7 +3039,7 @@ function buildRun({ cwd, id, entries, live }) {
3009
3039
  goal: state?.goal ?? head.goal ?? "",
3010
3040
  status: status === "aborted" ? "done" : status,
3011
3041
  alive,
3012
- stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : void 0,
3042
+ stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : state?.stopReason ?? state?.error,
3013
3043
  rules: rules.map((text) => ({ text, source: "config" })),
3014
3044
  rounds,
3015
3045
  best: {
@@ -3056,7 +3086,15 @@ function summarize(cwd, entries) {
3056
3086
  lastT = t;
3057
3087
  const bestRound = state?.best?.round ?? rounds.filter((r) => r.pick && r.pick !== "CURRENT").at(-1)?.n;
3058
3088
  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 };
3089
+ const attempts = rounds.flatMap((r) => r.attempts ?? []);
3090
+ const failures = /* @__PURE__ */ new Map();
3091
+ for (const a of attempts) {
3092
+ const why = a.status === "budget" ? "token budget" : a.status === "timeout" ? "timed out" : String(a.status).startsWith("error") ? "error" : a.status === "aborted" ? "stopped" : null;
3093
+ if (why) failures.set(why, (failures.get(why) ?? 0) + 1);
3094
+ }
3095
+ const details = [...failures].map(([why, n]) => `${n} ${why}`).join(", ");
3096
+ 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;
3097
+ 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
3098
  }
3061
3099
  }
3062
3100
  return { loops: ids.length, running, open, question, lastT, latest };
@@ -3278,6 +3316,12 @@ var Chat = class {
3278
3316
  }
3279
3317
  onEvent(event) {
3280
3318
  const type = event.type;
3319
+ if (type === "extension_error") {
3320
+ this.notices.push({ t: Date.now(), level: "error", message: String(event.error ?? event.message ?? "Extension command failed") });
3321
+ this.notices = this.notices.slice(-20);
3322
+ this.broadcast(true);
3323
+ return;
3324
+ }
3281
3325
  if (type === "message_start" && event.message?.role === "assistant") {
3282
3326
  this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: "", text: "", tools: [] };
3283
3327
  this.broadcast();
@@ -3330,11 +3374,18 @@ var Chat = class {
3330
3374
  this.touch();
3331
3375
  }
3332
3376
  async prompt(text, behavior) {
3333
- if (!this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3377
+ const previous = this.live;
3378
+ if (!text.startsWith("/") && !this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3334
3379
  this.broadcast(true);
3335
- const r = await this.pi.prompt(text, behavior);
3336
- setTimeout(() => void this.refresh(), 400);
3337
- return r?.disposition ?? "started";
3380
+ try {
3381
+ const r = await this.pi.prompt(text, behavior);
3382
+ setTimeout(() => void this.refresh(), 400);
3383
+ return r?.disposition ?? "started";
3384
+ } catch (error) {
3385
+ if (!previous && this.live?.user === text) this.live = null;
3386
+ this.broadcast(true);
3387
+ throw error;
3388
+ }
3338
3389
  }
3339
3390
  /** The child's own view of "am I busy": a turn that settled with no event reaching us must not strand the UI. */
3340
3391
  async settleIfIdle() {
@@ -3378,11 +3429,16 @@ var Chat = class {
3378
3429
  this.answeringUi.delete(id);
3379
3430
  }
3380
3431
  }
3432
+ aborting = false;
3381
3433
  async abort() {
3382
- await this.pi.send({ type: "abort" }).catch(() => {
3383
- });
3384
- this.live = null;
3385
- this.broadcast(true);
3434
+ if (this.aborting) throw new Error("Stop agent is already in flight.");
3435
+ this.aborting = true;
3436
+ try {
3437
+ await this.pi.send({ type: "abort" }, 15e3);
3438
+ await this.settleIfIdle();
3439
+ } finally {
3440
+ this.aborting = false;
3441
+ }
3386
3442
  }
3387
3443
  /** The newest session entry of a kind (`loops-doctor`, `loops-run`, …), for read-only views. */
3388
3444
  custom(type) {
@@ -4740,7 +4796,7 @@ var withLive = (state) => {
4740
4796
  return state;
4741
4797
  };
4742
4798
  var streams = /* @__PURE__ */ new Set();
4743
- var snapshotOf = (chat, wsId) => withLive({ ...chat.snapshot(), chats: chatsOf(wsId), workspaces: workspaces.list(), ws: wsId, proposals });
4799
+ 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
4800
  manager.onChange((chat) => {
4745
4801
  const space = workspaces.ofChat(chat.id);
4746
4802
  const state = snapshotOf(chat, space.id);
@@ -4748,6 +4804,7 @@ manager.onChange((chat) => {
4748
4804
  });
4749
4805
  var watches = /* @__PURE__ */ new Map();
4750
4806
  var proposals = [];
4807
+ var configErrors = /* @__PURE__ */ new Map();
4751
4808
  var watchOf = (wsId) => {
4752
4809
  const ws = workspaces.get(wsId);
4753
4810
  let w = watches.get(ws.id);
@@ -4765,7 +4822,14 @@ var watchOf = (wsId) => {
4765
4822
  fs9.watch(path9.join(ws.path, ".pi"), (_e, name) => {
4766
4823
  if (name && name !== "loops.json") return;
4767
4824
  clearTimeout(watchTimers.get(ws.id));
4768
- watchTimers.set(ws.id, setTimeout(() => checkConfig(ws.id), 120));
4825
+ watchTimers.set(ws.id, setTimeout(() => {
4826
+ try {
4827
+ checkConfig(ws.id);
4828
+ } catch (e) {
4829
+ 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.` });
4830
+ notifyProposals();
4831
+ }
4832
+ }, 120));
4769
4833
  });
4770
4834
  } catch {
4771
4835
  }
@@ -4774,12 +4838,18 @@ var watchOf = (wsId) => {
4774
4838
  };
4775
4839
  var watchTimers = /* @__PURE__ */ new Map();
4776
4840
  function notifyProposals() {
4777
- const payload = JSON.stringify({ type: "proposals", proposals });
4778
4841
  for (const s of streams) {
4779
4842
  try {
4780
4843
  s.send("proposals", proposals);
4781
4844
  } catch {
4782
4845
  }
4846
+ const chat = manager.openChats().find((chat2) => chat2.id === s.chat);
4847
+ if (chat) {
4848
+ try {
4849
+ s.send("state", snapshotOf(chat, workspaces.ofChat(chat.id).id), chat.entryCursor);
4850
+ } catch {
4851
+ }
4852
+ }
4783
4853
  }
4784
4854
  }
4785
4855
  function checkConfig(wsId) {
@@ -4792,14 +4862,33 @@ function checkConfig(wsId) {
4792
4862
  return "";
4793
4863
  }
4794
4864
  })();
4865
+ let next;
4866
+ try {
4867
+ next = raw2.trim() ? JSON.parse(raw2) : {};
4868
+ if (!next || typeof next !== "object" || Array.isArray(next)) throw new Error("expected a JSON object");
4869
+ } catch (e) {
4870
+ let recovery = "";
4871
+ try {
4872
+ const backup = `${CONFIG}.invalid-${Date.now()}`;
4873
+ fs9.writeFileSync(backup, raw2);
4874
+ restoreConfig(w.cwd, w.accepted);
4875
+ recovery = ` Last accepted settings restored. Rejected text saved in ${backup}.`;
4876
+ } catch (writeError) {
4877
+ recovery = ` Could not restore settings: ${writeError.message}. Fix ${CONFIG} before running another loop.`;
4878
+ }
4879
+ configErrors.set(wsId, { ws: wsId, file: CONFIG, error: `Invalid config JSON: ${e.message}.${recovery}` });
4880
+ notifyProposals();
4881
+ return;
4882
+ }
4795
4883
  if (isSelfWriting() || isSelfWrite(raw2)) {
4796
- w.accepted = raw2.trim() ? JSON.parse(raw2) : {};
4884
+ w.accepted = next;
4797
4885
  return;
4798
4886
  }
4799
- const next = raw2.trim() ? JSON.parse(raw2) : {};
4887
+ const hadError = configErrors.delete(wsId);
4800
4888
  const changes = diffConfig(w.accepted, next);
4801
4889
  if (!changes.length) {
4802
4890
  w.accepted = next;
4891
+ if (hadError) notifyProposals();
4803
4892
  return;
4804
4893
  }
4805
4894
  restoreConfig(w.cwd, w.accepted);
@@ -4912,7 +5001,7 @@ api.get("/state", async (c) => {
4912
5001
  if (!id) return c.json({ error: "chat required" }, 400);
4913
5002
  const chat = await manager.open(id);
4914
5003
  const ws = workspaces.ofChat(id);
4915
- return c.json(withLive({ ...chat.snapshot(), chats: chatsOf(ws.id), workspaces: workspaces.list(), ws: ws.id, proposals }));
5004
+ return c.json(snapshotOf(chat, ws.id));
4916
5005
  });
4917
5006
  api.post("/chat", async (c) => {
4918
5007
  const body = await c.req.json().catch(() => ({}));
@@ -4988,10 +5077,15 @@ api.post("/rename", async (c) => {
4988
5077
  return c.json({ ok: true, title: chat.title });
4989
5078
  });
4990
5079
  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 });
5080
+ const { chat: id } = await c.req.json().catch(() => ({})) ?? {};
5081
+ if (typeof id !== "string" || !id.trim()) return c.json({ error: "chat required" }, 400);
5082
+ const chat = await manager.open(id);
5083
+ try {
5084
+ await chat.abort();
5085
+ return c.json({ ok: true, requested: true });
5086
+ } catch (e) {
5087
+ return c.json({ error: e.message }, 409);
5088
+ }
4995
5089
  });
4996
5090
  api.post("/chat/retry", async (c) => {
4997
5091
  const { chat: id } = await c.req.json();
@@ -5019,29 +5113,51 @@ api.post("/answer", async (c) => {
5019
5113
  await chat.prompt(`/loop answer ${String(question)} ${String(choice)}`);
5020
5114
  return c.json({ ok: true });
5021
5115
  });
5116
+ var steering = /* @__PURE__ */ new Set();
5022
5117
  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));
5118
+ const { chat: id, run: run3, action, rule, attempt, text } = await c.req.json().catch(() => ({})) ?? {};
5119
+ const token = (v) => typeof v === "string" && /^[\w.-]+$/.test(v);
5120
+ if (!token(id) || !token(run3)) return c.json({ error: "chat and exact run ID required" }, 400);
5121
+ if (!["pick", "take", "apply", "stop", "clean", "rule", "talk", "more"].includes(action)) return c.json({ error: "valid action required" }, 400);
5122
+ if (["pick", "take", "talk"].includes(action) && !token(attempt)) return c.json({ error: "attempt required (durable name, e.g. R1-safe)" }, 400);
5123
+ if (action === "rule" && (typeof rule !== "string" || !rule.trim())) return c.json({ error: "rule required" }, 400);
5124
+ if (action === "talk" && (typeof text !== "string" || !text.trim())) return c.json({ error: "text required" }, 400);
5125
+ 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);
5126
+ const chat = await manager.open(id);
5127
+ if (!chat.hasRun(run3)) return c.json({ error: "run does not belong to this chat" }, 404);
5128
+ 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);
5129
+ 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);
5130
+ const key = `${id}:${run3}:${action}`;
5131
+ if (steering.has(key)) return c.json({ error: "This action is already in flight." }, 409);
5132
+ steering.add(key);
5025
5133
  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);
5134
+ try {
5135
+ switch (String(action)) {
5136
+ case "pick":
5137
+ case "take":
5138
+ return c.json({ ok: true, sent: await slash(`/loop take ${attempt} ${run3}`) });
5139
+ case "apply":
5140
+ return c.json({ ok: true, sent: await slash(`/loop apply ${run3}`) });
5141
+ // Pi executes extension commands before its streaming queue. Do not send this as a queued follow-up:
5142
+ // a background round must remain independently stoppable while the parent is in a long-running tool.
5143
+ case "stop":
5144
+ return c.json({ ok: true, requested: true, sent: await slash(`/loop stop ${run3}`) });
5145
+ case "clean":
5146
+ return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5147
+ case "rule":
5148
+ return c.json({ ok: true, sent: await slash(`/loop rule ${run3} ${rule}`) });
5149
+ case "talk":
5150
+ return c.json({ ok: true, sent: await slash(`/loop talk ${attempt} ${text}`) });
5151
+ // everything else is a decision only the agent can make (another round, a different direction)
5152
+ case "more":
5153
+ 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} }).`) });
5154
+ default:
5155
+ return c.json({ error: `unknown action ${action}` }, 400);
5156
+ }
5157
+ } catch (e) {
5158
+ return c.json({ error: e.message }, 409);
5159
+ } finally {
5160
+ steering.delete(key);
5045
5161
  }
5046
5162
  });
5047
5163
  api.get("/ui", async (c) => {
@@ -5144,18 +5260,20 @@ api.post("/proposals/:id/keep", (c) => {
5144
5260
  api.get("/config", (c) => {
5145
5261
  const ws = wsOf(c.req.query("chat"));
5146
5262
  watchOf(ws.id);
5147
- return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path) });
5263
+ return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path), error: configErrors.get(ws.id)?.error });
5148
5264
  });
5149
5265
  api.post("/config", async (c) => {
5150
5266
  const { key, value, chat } = await c.req.json();
5151
5267
  const ws = wsOf(chat);
5152
5268
  try {
5153
5269
  writeSetting(ws.path, String(key), value);
5270
+ configErrors.delete(ws.id);
5154
5271
  const w = watchOf(ws.id);
5155
5272
  try {
5156
5273
  w.accepted = JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
5157
5274
  } catch {
5158
5275
  }
5276
+ notifyProposals();
5159
5277
  return c.json({ ok: true });
5160
5278
  } catch (e) {
5161
5279
  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.7",
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};