loopable-cli 0.2.0 → 0.3.0

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 (138) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
  3. package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
  4. package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
  5. package/.output/public/assets/agents-CBozi7oi.js +1 -0
  6. package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
  7. package/.output/public/assets/button-K3ypIhAZ.js +1 -0
  8. package/.output/public/assets/card-DiVnaTIE.js +1 -0
  9. package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
  10. package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
  11. package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
  12. package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
  13. package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
  14. package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
  15. package/.output/public/assets/index-C9SQ_Baq.js +16 -0
  16. package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
  17. package/.output/public/assets/loops-DcEfZa2_.js +1 -0
  18. package/.output/public/assets/manifests-yJpevyye.js +1 -0
  19. package/.output/public/assets/new-CwsnNuRW.js +1 -0
  20. package/.output/public/assets/page-bvnfRMum.js +1 -0
  21. package/.output/public/assets/play-yHLs4DjK.js +1 -0
  22. package/.output/public/assets/plus-Ber25lrU.js +1 -0
  23. package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
  24. package/.output/public/assets/refresh-cw-BzSY7M8I.js +1 -0
  25. package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
  26. package/.output/public/assets/runners-DzX-RU3i.js +2 -0
  27. package/.output/public/assets/select-C9grIFwG.js +1 -0
  28. package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
  29. package/.output/public/assets/styles-B9B98zLk.css +2 -0
  30. package/.output/public/assets/switch-DwABVI6d.js +1 -0
  31. package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
  32. package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
  33. package/.output/public/assets/time-goBWd-5J.js +1 -0
  34. package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
  35. package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
  36. package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
  37. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  38. package/.output/server/_connectorId-COuQN02a.mjs +512 -0
  39. package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
  40. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  41. package/.output/server/_libs/lucide-react.mjs +93 -84
  42. package/.output/server/_libs/next-themes.mjs +1 -1
  43. package/.output/server/_libs/sonner.mjs +1 -1
  44. package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
  45. package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
  46. package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-B_qo1gpL.mjs} +196 -7
  47. package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-Bz10gz_m.mjs} +8 -8
  48. package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
  49. package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
  50. package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
  51. package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
  52. package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
  53. package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Cqvb6Gw2.mjs} +1109 -263
  54. package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
  55. package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
  56. package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-DJI-rzPU.mjs} +21 -10
  57. package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
  58. package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-CcvHiLcj.mjs} +18 -10
  59. package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
  60. package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
  61. package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-6WhUe_lc.mjs} +228 -25
  62. package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
  63. package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
  64. package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
  65. package/.output/server/_ssr/{router-BbrBz36P.mjs → router-DdwREiOf.mjs} +167 -489
  66. package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
  67. package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
  68. package/.output/server/_ssr/runners-W-pgo5yo.mjs +55 -0
  69. package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
  70. package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
  71. package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-LNgywAW7.mjs} +25 -20
  72. package/.output/server/_ssr/ssr.mjs +49 -40
  73. package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-C_j-e4vF.mjs} +10 -6
  74. package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
  75. package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
  76. package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-D-kXLgVP.mjs} +13 -13
  77. package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-D7aUvA33.mjs} +55 -17
  78. package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
  79. package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
  80. package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
  81. package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
  82. package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
  83. package/.output/server/index.mjs +204 -162
  84. package/README.md +54 -19
  85. package/dist/cli.mjs +185 -67
  86. package/dist/dispatcher.mjs +1443 -277
  87. package/dist/runner.mjs +333 -46
  88. package/drizzle/0025_prompt_tasks_done.sql +2 -0
  89. package/drizzle/0026_guidance_into_prompt.sql +3 -0
  90. package/drizzle/0027_relative_folders.sql +5 -0
  91. package/drizzle/0028_runner_version.sql +5 -0
  92. package/drizzle/meta/_journal.json +29 -1
  93. package/package.json +23 -17
  94. package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
  95. package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
  96. package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
  97. package/.output/public/assets/alert-BT2EVMJP.js +0 -1
  98. package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
  99. package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
  100. package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
  101. package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
  102. package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
  103. package/.output/public/assets/external-link-DC11orFA.js +0 -1
  104. package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
  105. package/.output/public/assets/inbox-CLEXUXCI.js +0 -1
  106. package/.output/public/assets/index-Cndsw84p.js +0 -16
  107. package/.output/public/assets/loader-B4Epchm_.js +0 -1
  108. package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
  109. package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
  110. package/.output/public/assets/new-DPEaXKAY.js +0 -1
  111. package/.output/public/assets/play-DD4xQgvi.js +0 -1
  112. package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
  113. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
  114. package/.output/public/assets/runners-BVBggS5B.js +0 -1
  115. package/.output/public/assets/select-Cppu-hti.js +0 -1
  116. package/.output/public/assets/setting-fields-BT1naVJX.js +0 -3
  117. package/.output/public/assets/styles-D3sonCfX.css +0 -2
  118. package/.output/public/assets/switch-hRIbNGOj.js +0 -1
  119. package/.output/public/assets/task-list-CrptnI1N.js +0 -1
  120. package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
  121. package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
  122. package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
  123. package/.output/server/_connectorId-DcSnoRjk.mjs +0 -421
  124. package/.output/server/_loopId-G_TAkYtS.mjs +0 -272
  125. package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
  126. package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
  127. package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
  128. package/.output/server/_ssr/connectors-4yzKb0KG.mjs +0 -106
  129. package/.output/server/_ssr/inbox-C2VBGLa5.mjs +0 -114
  130. package/.output/server/_ssr/loop-form-D-U-kU4k.mjs +0 -413
  131. package/.output/server/_ssr/loops-D0XJ8eBb.mjs +0 -222
  132. package/.output/server/_ssr/new-CajvgIog.mjs +0 -169
  133. package/.output/server/_ssr/runners-B_WuF8UJ.mjs +0 -32
  134. package/.output/server/_ssr/runners-D-0_lfiq.mjs +0 -106
  135. package/.output/server/_ssr/runners-zqF_zbEc.mjs +0 -116
  136. package/.output/server/_ssr/task-list-BnQJ94Dk.mjs +0 -187
  137. package/.output/server/_tanstack-start-manifest_v-BCHAazE8.mjs +0 -129
  138. package/.output/server/_taskId-CIv0ajIx.mjs +0 -256
@@ -1,13 +1,16 @@
1
- import { n as createServerFn } from "./ssr.mjs";
1
+ import { r as createServerFn } from "./ssr.mjs";
2
2
  import { d as and, f as eq, l as asc, p as inArray, v as sql } from "../_libs/drizzle-orm.mjs";
3
- import { a as connections, c as loops, h as signals, s as db } from "./client-BtVRVhQ2.mjs";
4
- import { a as connectorManifest, i as connectorAction, s as connectorWorkflow } from "./connections-ypex3qUd.mjs";
5
- import { n as agentManifest, r as listAgents } from "./agents-CrUmIQpi.mjs";
6
- import { t as availableAgentIds } from "./runners-D-0_lfiq.mjs";
7
- import { r as enqueueSignal } from "./tasks-B_QmDFVs.mjs";
8
- import { t as createServerRpc } from "./createServerRpc-DPDrnFdH.mjs";
3
+ import { a as connections, c as loops, h as signals, s as db } from "./client-Dk3SD8Il.mjs";
4
+ import { c as connectorRuntime, l as connectorWorkflow, o as connectorAction, s as connectorManifest, u as credentialForConnector } from "./connections-Cqvb6Gw2.mjs";
5
+ import { n as availableAgentIds } from "./runners-CK9Cx9fS.mjs";
6
+ import { i as listAgents, n as agentManifest, r as agentRuntime, s as settingsFor } from "./agents-B_qo1gpL.mjs";
7
+ import { i as enqueueSignal, n as defaultAgentFor, o as folderIsLegacy, p as normalizeFolder } from "./tasks-D7aUvA33.mjs";
8
+ import { t as createServerRpc } from "./createServerRpc-DDrp2nLT.mjs";
9
9
  import { randomUUID } from "node:crypto";
10
- //#region node_modules/.nitro/vite/services/ssr/assets/loops-Og_G3KyZ.js
10
+ import { mkdtempSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ //#region node_modules/.nitro/vite/services/ssr/assets/loops-6WhUe_lc.js
11
14
  var NAME_LIMIT = 80;
12
15
  var PROMPT_LIMIT = 8e3;
13
16
  var GUIDANCE_LIMIT = 4e3;
@@ -73,16 +76,22 @@ function validate(draft) {
73
76
  if (prompt.length > PROMPT_LIMIT) throw new Error(`Keep what you ask under ${PROMPT_LIMIT} characters.`);
74
77
  const guidance = draft.guidance?.trim() || null;
75
78
  if (guidance && guidance.length > GUIDANCE_LIMIT) throw new Error(`Keep the guidance under ${GUIDANCE_LIMIT} characters.`);
79
+ let folder;
76
80
  if (workflow.runsIn === "folder") {
77
- const folder = draft.settings.folder;
78
- if (typeof folder !== "string" || folder.trim() === "") throw new Error("Say which folder this should work in.");
81
+ const given = typeof draft.settings.folder === "string" ? draft.settings.folder : "";
82
+ folder = normalizeFolder(given);
83
+ if (!folder) throw new Error("Say which folder this should work in.");
84
+ if (folderIsLegacy(folder)) throw new Error("Give the folder relative to the runner's home, such as code/web.");
79
85
  }
80
86
  return {
81
87
  ...draft,
82
88
  name,
83
89
  prompt,
84
90
  guidance,
85
- settings: cleanConditions(workflow.settings, draft.settings),
91
+ settings: {
92
+ ...cleanConditions(workflow.settings, draft.settings),
93
+ ...folder ? { folder } : {}
94
+ },
86
95
  actionTarget: cleanConditions(action.target, draft.actionTarget)
87
96
  };
88
97
  }
@@ -173,15 +182,202 @@ async function loopReadiness() {
173
182
  const connected = await db().selectDistinct({ connectorId: connections.connectorId }).from(connections).all();
174
183
  const available = await availableAgentIds();
175
184
  const host = await availableAgentIds(true);
185
+ const folder = await availableAgentIds(false, 2);
176
186
  const chosen = agents.find((agent) => agent.isDefault)?.agentId ?? null;
177
187
  const defaultAgentId = (chosen && available.includes(chosen) ? chosen : null) ?? (available.length === 1 ? available[0] : null);
178
188
  return {
179
189
  connectedConnectorIds: connected.map((row) => row.connectorId),
180
190
  defaultAgentId,
181
191
  availableAgentIds: available,
182
- hostAgentIds: host
192
+ hostAgentIds: host,
193
+ folderAgentIds: folder
183
194
  };
184
195
  }
196
+ var NOTHING_ASKED = "nothing in it was being asked of you";
197
+ /**
198
+ * The batch is numbered rather than named because the agent has to point back
199
+ * at these, and a subject line is not a usable handle: two mails can share
200
+ * one, and any of them can contain the quotes and newlines that would break
201
+ * the answer apart.
202
+ */
203
+ function listFor(signals) {
204
+ return signals.map((signal, index) => {
205
+ const preview = signal.preview ? `\n ${signal.preview.slice(0, 300)}` : "";
206
+ return `${index + 1}. ${signal.title}${preview}`;
207
+ }).join("\n\n");
208
+ }
209
+ function promptFor(signals, guidance) {
210
+ const lines = [
211
+ "Below are things that just arrived. Say which ones are asking something",
212
+ "of the person who received them.",
213
+ "",
214
+ "Asking something means there is a decision to make, a reply expected, a",
215
+ "form to fill, a payment due, a meeting to book, a deadline, or a problem",
216
+ "to look at. Receipts, statements, newsletters, marketing, social",
217
+ "notifications, and automated reports are not asking anything, even when",
218
+ "they are about money, and even when they are worth having read.",
219
+ "",
220
+ "When you cannot tell from what is here, treat it as asking something.",
221
+ "Being told about one thing too many is a small cost; missing the one that",
222
+ "mattered is not."
223
+ ];
224
+ if (guidance) lines.push("", "What this person has said they care about, which outranks the above:", guidance);
225
+ lines.push("", "Answer with JSON only, no other text: {\"needsMe\": [1, 4]}, listing the", "numbers that are asking something. An empty list is a valid answer.", "", listFor(signals));
226
+ return lines.join("\n");
227
+ }
228
+ /**
229
+ * The numbers the agent picked out, as indices. Anything unparseable gives
230
+ * nothing back, which the caller reads as "could not tell" rather than as
231
+ * "none of them": a triage that failed must not be the reason work went
232
+ * unnoticed.
233
+ */
234
+ function parseAnswer(output, count) {
235
+ const start = output.indexOf("{");
236
+ const end = output.lastIndexOf("}");
237
+ if (start === -1 || end <= start) return null;
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(output.slice(start, end + 1));
241
+ } catch {
242
+ return null;
243
+ }
244
+ const listed = parsed?.needsMe;
245
+ if (!Array.isArray(listed)) return null;
246
+ const chosen = /* @__PURE__ */ new Set();
247
+ for (const entry of listed) {
248
+ const value = typeof entry === "number" ? entry : Number.parseInt(String(entry), 10);
249
+ if (Number.isInteger(value) && value >= 1 && value <= count) chosen.add(value - 1);
250
+ }
251
+ return chosen;
252
+ }
253
+ /**
254
+ * Which of these need not be run, and why. An empty result means everything
255
+ * runs, which is what happens whenever the answer cannot be trusted: too few
256
+ * to be worth a run, an agent that failed, or an answer that made no sense.
257
+ */
258
+ async function triage(input) {
259
+ const held = /* @__PURE__ */ new Map();
260
+ if (input.signals.length < 2) return held;
261
+ const result = await agentRuntime(input.agentId).run({
262
+ prompt: promptFor(input.signals, input.guidance),
263
+ cwd: input.cwd,
264
+ settings: await settingsFor(input.agentId),
265
+ signal: input.signal
266
+ });
267
+ if (!result.ok || result.aborted) return held;
268
+ const needsMe = parseAnswer(result.output, input.signals.length);
269
+ if (!needsMe) return held;
270
+ input.signals.forEach((entry, index) => {
271
+ if (!needsMe.has(index)) held.set(entry.key, NOTHING_ASKED);
272
+ });
273
+ return held;
274
+ }
275
+ /** A setting a loop was saved before existing is missing, which is not false. */
276
+ function boolean(value) {
277
+ return value === true;
278
+ }
279
+ /**
280
+ * Two loops can want the same pull request. Only the first one gets it, so
281
+ * this is what one loop tells the next about what it has taken.
282
+ */
283
+ function claimOf(loop, signal) {
284
+ return `${loop.connectorId}:${signal.ref}`;
285
+ }
286
+ /**
287
+ * One look at what a loop is watching for.
288
+ *
289
+ * The first look never acts. Whatever is already waiting when a loop is
290
+ * created is that loop's backlog, and running an agent over a review queue
291
+ * that has been piling up for a month is not what someone turning on a loop
292
+ * is asking for. It is recorded rather than discarded, so it can still be run
293
+ * on purpose afterwards.
294
+ */
295
+ async function pollLoop(loop, claimed) {
296
+ const report = {
297
+ loopId: loop.id,
298
+ loopName: loop.name,
299
+ found: 0,
300
+ queued: 0,
301
+ backlog: 0,
302
+ held: 0,
303
+ superseded: 0,
304
+ triaged: 0,
305
+ error: null
306
+ };
307
+ try {
308
+ const manifest = connectorManifest(loop.connectorId);
309
+ if (!manifest) throw new Error(`Unknown connector: ${loop.connectorId}`);
310
+ const workflow = connectorWorkflow(loop.connectorId, loop.workflowId);
311
+ if (!workflow) throw new Error(`${manifest.name} no longer offers ${loop.workflowId}.`);
312
+ const runtime = connectorRuntime(loop.connectorId);
313
+ if (!runtime.poll) throw new Error(`${manifest.name} cannot watch for anything yet.`);
314
+ const { credential } = await credentialForConnector(loop.connectorId);
315
+ const answer = await runtime.poll({
316
+ workflowId: workflow.triggerId,
317
+ settings: loop.settings,
318
+ credential,
319
+ cursor: loop.pollCursor ?? null
320
+ });
321
+ const found = answer.signals;
322
+ report.found = found.length;
323
+ const known = new Set((await db().select({ key: signals.key }).from(signals).where(eq(signals.loopId, loop.id)).all()).map((row) => row.key));
324
+ const first = loop.polledAt === null;
325
+ let judged = /* @__PURE__ */ new Map();
326
+ if (!first && boolean(loop.settings.needsMeOnly)) judged = await triage({
327
+ signals: found.filter((signal) => !known.has(signal.key) && !signal.hold && !claimed.has(claimOf(loop, signal))),
328
+ agentId: await defaultAgentFor(loop.agentId),
329
+ guidance: loop.guidance,
330
+ cwd: mkdtempSync(join(tmpdir(), "loopable-triage-"))
331
+ });
332
+ for (const signal of found) {
333
+ const claim = claimOf(loop, signal);
334
+ if (known.has(signal.key)) {
335
+ claimed.add(claim);
336
+ continue;
337
+ }
338
+ const hold = signal.hold ?? judged.get(signal.key);
339
+ const outcome = claimed.has(claim) ? "superseded" : hold ? "held" : first ? "backlog" : "queued";
340
+ const task = outcome === "queued" ? await enqueueSignal({
341
+ loop,
342
+ signal
343
+ }) : null;
344
+ await db().insert(signals).values({
345
+ loopId: loop.id,
346
+ key: signal.key,
347
+ outcome,
348
+ sourceKind: signal.kind,
349
+ sourceRef: signal.ref,
350
+ sourceTitle: signal.title,
351
+ sourceUrl: signal.url,
352
+ hold,
353
+ sourcePayload: signal.payload,
354
+ taskId: task?.id
355
+ }).onConflictDoNothing().run();
356
+ claimed.add(claim);
357
+ report[outcome] += 1;
358
+ if (outcome === "held" && !signal.hold) report.triaged += 1;
359
+ }
360
+ await db().update(loops).set({
361
+ polledAt: /* @__PURE__ */ new Date(),
362
+ pollError: null,
363
+ ...answer.cursor !== void 0 ? { pollCursor: answer.cursor } : {}
364
+ }).where(eq(loops.id, loop.id)).run();
365
+ } catch (error) {
366
+ report.error = error instanceof Error ? error.message : String(error);
367
+ await db().update(loops).set({ pollError: report.error }).where(eq(loops.id, loop.id)).run();
368
+ }
369
+ return report;
370
+ }
371
+ /**
372
+ * One look, now, whether the interval says it is due or not. The loop page
373
+ * asks for this when waiting for the dispatcher is the thing being tested.
374
+ */
375
+ async function lookNow(loopId) {
376
+ const loop = await db().select().from(loops).where(eq(loops.id, loopId)).get();
377
+ if (!loop) throw new Error("Loop not found");
378
+ if (!loop.enabled) throw new Error("This loop is off");
379
+ return await pollLoop(loop, /* @__PURE__ */ new Set());
380
+ }
185
381
  function toBacklogItem(row) {
186
382
  return {
187
383
  key: row.key,
@@ -211,14 +407,15 @@ async function loopPollState(loopId) {
211
407
  };
212
408
  }
213
409
  /**
214
- * Run everything the loop is holding. Asked for explicitly, because it is the
215
- * one moment a loop does a month of work at once, and because something held
216
- * back for being too big is being run against the connector's advice.
410
+ * Run what the loop is holding: one item when `key` is given, or all of it.
411
+ * Asked for explicitly, because something held back for being too big is
412
+ * being run against the connector's advice.
217
413
  */
218
- async function runBacklog(loopId) {
414
+ async function runBacklog(loopId, key) {
219
415
  const loop = await db().select().from(loops).where(eq(loops.id, loopId)).get();
220
416
  if (!loop) throw new Error("Loop not found");
221
- const waiting = await db().select().from(signals).where(and(eq(signals.loopId, loopId), inArray(signals.outcome, [...WAITING]))).all();
417
+ const waiting = await db().select().from(signals).where(and(eq(signals.loopId, loopId), inArray(signals.outcome, [...WAITING]), key === void 0 ? void 0 : eq(signals.key, key))).all();
418
+ if (key !== void 0 && waiting.length === 0) throw new Error("That item is no longer waiting.");
222
419
  for (const row of waiting) {
223
420
  const task = await enqueueSignal({
224
421
  loop,
@@ -264,13 +461,13 @@ var getLoopById_createServerFn_handler = createServerRpc({
264
461
  name: "getLoopById",
265
462
  filename: "src/server/functions/loops.ts"
266
463
  }, (opts) => getLoopById.__executeServer(opts));
267
- var getLoopById = createServerFn({ method: "GET" }).inputValidator((data) => data).handler(getLoopById_createServerFn_handler, async ({ data }) => await getLoop(data.id));
464
+ var getLoopById = createServerFn({ method: "GET" }).validator((data) => data).handler(getLoopById_createServerFn_handler, async ({ data }) => await getLoop(data.id));
268
465
  var saveLoop_createServerFn_handler = createServerRpc({
269
466
  id: "e91fc2fd51633c7ca9184fea4beb66fe775ed0b4e6323111a4690da916a78b00",
270
467
  name: "saveLoop",
271
468
  filename: "src/server/functions/loops.ts"
272
469
  }, (opts) => saveLoop.__executeServer(opts));
273
- var saveLoop = createServerFn({ method: "POST" }).inputValidator(edit).handler(saveLoop_createServerFn_handler, async ({ data }) => {
470
+ var saveLoop = createServerFn({ method: "POST" }).validator(edit).handler(saveLoop_createServerFn_handler, async ({ data }) => {
274
471
  const { id, ...values } = data;
275
472
  return id ? await updateLoop(id, values) : await createLoop(values);
276
473
  });
@@ -279,13 +476,13 @@ var toggleLoop_createServerFn_handler = createServerRpc({
279
476
  name: "toggleLoop",
280
477
  filename: "src/server/functions/loops.ts"
281
478
  }, (opts) => toggleLoop.__executeServer(opts));
282
- var toggleLoop = createServerFn({ method: "POST" }).inputValidator((data) => data).handler(toggleLoop_createServerFn_handler, async ({ data }) => await setLoopEnabled(data.id, data.enabled));
479
+ var toggleLoop = createServerFn({ method: "POST" }).validator((data) => data).handler(toggleLoop_createServerFn_handler, async ({ data }) => await setLoopEnabled(data.id, data.enabled));
283
480
  var removeLoop_createServerFn_handler = createServerRpc({
284
481
  id: "b5a89ca2d07f4ccf2827b5132ca52e7c7a42d584e702b0ab3963b2d5763c35b0",
285
482
  name: "removeLoop",
286
483
  filename: "src/server/functions/loops.ts"
287
484
  }, (opts) => removeLoop.__executeServer(opts));
288
- var removeLoop = createServerFn({ method: "POST" }).inputValidator((data) => data).handler(removeLoop_createServerFn_handler, async ({ data }) => {
485
+ var removeLoop = createServerFn({ method: "POST" }).validator((data) => data).handler(removeLoop_createServerFn_handler, async ({ data }) => {
289
486
  await deleteLoop(data.id);
290
487
  return await listLoops();
291
488
  });
@@ -294,18 +491,24 @@ var reorderLoop_createServerFn_handler = createServerRpc({
294
491
  name: "reorderLoop",
295
492
  filename: "src/server/functions/loops.ts"
296
493
  }, (opts) => reorderLoop.__executeServer(opts));
297
- var reorderLoop = createServerFn({ method: "POST" }).inputValidator((data) => data).handler(reorderLoop_createServerFn_handler, async ({ data }) => await moveLoop(data.id, data.direction));
494
+ var reorderLoop = createServerFn({ method: "POST" }).validator((data) => data).handler(reorderLoop_createServerFn_handler, async ({ data }) => await moveLoop(data.id, data.direction));
298
495
  var getLoopPollState_createServerFn_handler = createServerRpc({
299
496
  id: "4fd3855e3e9ed954e102071a55b98d91651c9b1e353161f7223768ed6f2b8c28",
300
497
  name: "getLoopPollState",
301
498
  filename: "src/server/functions/loops.ts"
302
499
  }, (opts) => getLoopPollState.__executeServer(opts));
303
- var getLoopPollState = createServerFn({ method: "GET" }).inputValidator((data) => data).handler(getLoopPollState_createServerFn_handler, async ({ data }) => await loopPollState(data.id));
500
+ var getLoopPollState = createServerFn({ method: "GET" }).validator((data) => data).handler(getLoopPollState_createServerFn_handler, async ({ data }) => await loopPollState(data.id));
304
501
  var runLoopBacklog_createServerFn_handler = createServerRpc({
305
502
  id: "65494cbfd06143dbd9a012808fbba21163d9902e387869503fb8b68b0674902b",
306
503
  name: "runLoopBacklog",
307
504
  filename: "src/server/functions/loops.ts"
308
505
  }, (opts) => runLoopBacklog.__executeServer(opts));
309
- var runLoopBacklog = createServerFn({ method: "POST" }).inputValidator((data) => data).handler(runLoopBacklog_createServerFn_handler, async ({ data }) => ({ queued: await runBacklog(data.id) }));
506
+ var runLoopBacklog = createServerFn({ method: "POST" }).validator((data) => data).handler(runLoopBacklog_createServerFn_handler, async ({ data }) => ({ queued: await runBacklog(data.id, data.key) }));
507
+ var lookLoopNow_createServerFn_handler = createServerRpc({
508
+ id: "965ff21ee2f1e1f5a1cb78d50570cbad6f58ccdbdcc7955f02f816a9c0684f8e",
509
+ name: "lookLoopNow",
510
+ filename: "src/server/functions/loops.ts"
511
+ }, (opts) => lookLoopNow.__executeServer(opts));
512
+ var lookLoopNow = createServerFn({ method: "POST" }).validator((data) => data).handler(lookLoopNow_createServerFn_handler, async ({ data }) => await lookNow(data.id));
310
513
  //#endregion
311
- export { getLoopById_createServerFn_handler, getLoopPollState_createServerFn_handler, getLoopReadiness_createServerFn_handler, getLoopsPage_createServerFn_handler, removeLoop_createServerFn_handler, reorderLoop_createServerFn_handler, runLoopBacklog_createServerFn_handler, saveLoop_createServerFn_handler, toggleLoop_createServerFn_handler };
514
+ export { getLoopById_createServerFn_handler, getLoopPollState_createServerFn_handler, getLoopReadiness_createServerFn_handler, getLoopsPage_createServerFn_handler, lookLoopNow_createServerFn_handler, removeLoop_createServerFn_handler, reorderLoop_createServerFn_handler, runLoopBacklog_createServerFn_handler, saveLoop_createServerFn_handler, toggleLoop_createServerFn_handler };
@@ -0,0 +1,217 @@
1
+ import { i as __toESM } from "../_runtime.mjs";
2
+ import { K as require_jsx_runtime, a as MenuPortal, i as MenuPositioner, n as MenuTrigger, o as MenuPopup, q as require_react, r as MenuRoot, s as MenuItem, t as Separator } from "../_libs/@base-ui/react+[...].mjs";
3
+ import { f as Link, m as useRouter } from "../_libs/@tanstack/react-router+[...].mjs";
4
+ import { d as destinationLabel, l as connectorWorkflow, s as connectorManifest } from "./connections-Cqvb6Gw2.mjs";
5
+ import { n as agentManifest } from "./agents-B_qo1gpL.mjs";
6
+ import { B as ArrowUp, C as Ellipsis, N as CircleAlert, V as ArrowDown, i as Trash2, l as Plus } from "../_libs/lucide-react.mjs";
7
+ import { n as toast } from "../_libs/sonner.mjs";
8
+ import { P as cn, _ as Badge, b as removeLoop, s as Route$11, v as Button$1, w as toggleLoop, x as reorderLoop } from "./router-DdwREiOf.mjs";
9
+ import { t as ConnectorIcon } from "./connector-icon-DrhVbhs6.mjs";
10
+ import { a as List, o as PageHeader, t as Empty } from "./page-DZVxKvdn.mjs";
11
+ import { t as Switch$1 } from "./switch-odxNO_S4.mjs";
12
+ import { n as AlertDescription, r as AlertTitle, t as Alert } from "./alert-C2WBWfzo.mjs";
13
+ import { n as useLiveRefresh } from "./use-live-tasks-BzhTkEOL.mjs";
14
+ import { n as issuesFor, t as gapsFor } from "./gaps-CcvHiLcj.mjs";
15
+ //#region node_modules/.nitro/vite/services/ssr/assets/loops-DHPjzG-t.js
16
+ var import_react = /* @__PURE__ */ __toESM(require_react());
17
+ var import_jsx_runtime = require_jsx_runtime();
18
+ function DropdownMenu({ ...props }) {
19
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuRoot, {
20
+ "data-slot": "dropdown-menu",
21
+ ...props
22
+ });
23
+ }
24
+ function DropdownMenuTrigger({ ...props }) {
25
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuTrigger, {
26
+ "data-slot": "dropdown-menu-trigger",
27
+ ...props
28
+ });
29
+ }
30
+ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom", sideOffset = 4, className, ...props }) {
31
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuPositioner, {
32
+ className: "isolate z-50 outline-none",
33
+ align,
34
+ alignOffset,
35
+ side,
36
+ sideOffset,
37
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuPopup, {
38
+ "data-slot": "dropdown-menu-content",
39
+ className: cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className),
40
+ ...props
41
+ })
42
+ }) });
43
+ }
44
+ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
45
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuItem, {
46
+ "data-slot": "dropdown-menu-item",
47
+ "data-inset": inset,
48
+ "data-variant": variant,
49
+ className: cn("group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", className),
50
+ ...props
51
+ });
52
+ }
53
+ function DropdownMenuSeparator({ className, ...props }) {
54
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Separator, {
55
+ "data-slot": "dropdown-menu-separator",
56
+ className: cn("-mx-1 my-1 h-px bg-border", className),
57
+ ...props
58
+ });
59
+ }
60
+ function LoopsPage() {
61
+ const { loops, readiness } = Route$11.useLoaderData();
62
+ useLiveRefresh();
63
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
64
+ className: "flex flex-col gap-6",
65
+ children: [
66
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageHeader, {
67
+ title: "Loops",
68
+ description: "When two loops match the same thing, the higher one runs.",
69
+ actions: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
70
+ nativeButton: false,
71
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, { to: "/loops/new" }),
72
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, {}), "New loop"]
73
+ })
74
+ }),
75
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Gaps, {
76
+ readiness,
77
+ loops
78
+ }),
79
+ loops.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Empty, {
80
+ action: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
81
+ variant: "outline",
82
+ nativeButton: false,
83
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, { to: "/loops/new" }),
84
+ children: "New loop"
85
+ }),
86
+ children: "No loops yet."
87
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(List, { children: loops.map((loop, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopRow, {
88
+ loop,
89
+ readiness,
90
+ position: index + 1,
91
+ first: index === 0,
92
+ last: index === loops.length - 1
93
+ }, loop.id)) })
94
+ ]
95
+ });
96
+ }
97
+ /** What still stands between these loops and running. */
98
+ function Gaps({ readiness, loops }) {
99
+ const missing = gapsFor(readiness, loops);
100
+ if (missing.length === 0) return null;
101
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, { children: [
102
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleAlert, {}),
103
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Nothing will run yet" }),
104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, {
105
+ className: "flex flex-col gap-0.5",
106
+ children: missing.map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: line }, line))
107
+ })
108
+ ] });
109
+ }
110
+ /** Where this loop answers and with what, in a few words. */
111
+ function summaryOf(loop) {
112
+ if (!connectorWorkflow(loop.connectorId, loop.workflowId)) return "Workflow no longer offered";
113
+ const agent = loop.agentId ? agentManifest(loop.agentId) : void 0;
114
+ return [destinationLabel(loop.actionConnectorId, loop.actionId), agent?.name ?? "Default agent"].join(" · ");
115
+ }
116
+ function LoopRow({ loop, readiness, position, first, last }) {
117
+ const router = useRouter();
118
+ const manifest = connectorManifest(loop.connectorId);
119
+ const issues = issuesFor(readiness, loop);
120
+ const [busy, setBusy] = (0, import_react.useState)(false);
121
+ const run = async (work, message) => {
122
+ setBusy(true);
123
+ try {
124
+ await work();
125
+ await router.invalidate();
126
+ if (message) toast.success(message);
127
+ } catch (error) {
128
+ toast.error(error instanceof Error ? error.message : String(error));
129
+ } finally {
130
+ setBusy(false);
131
+ }
132
+ };
133
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
134
+ className: "flex items-center gap-3 px-4 py-3",
135
+ children: [
136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
137
+ className: "w-4 shrink-0 text-center text-xs text-muted-foreground tabular-nums",
138
+ children: position
139
+ }),
140
+ manifest ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConnectorIcon, {
141
+ icon: manifest.icon,
142
+ accent: manifest.accent,
143
+ className: loop.enabled ? "size-8" : "size-8 opacity-50"
144
+ }) : null,
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Link, {
146
+ to: "/loops/$loopId",
147
+ params: { loopId: loop.id },
148
+ className: "group min-w-0 flex-1",
149
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
150
+ className: "flex items-center gap-2",
151
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152
+ className: "truncate text-sm font-medium group-hover:underline",
153
+ children: loop.name
154
+ }), loop.enabled ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
155
+ variant: "secondary",
156
+ children: "Off"
157
+ })]
158
+ }), issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
159
+ className: "mt-0.5 flex items-center gap-1 truncate text-xs text-amber-700 dark:text-amber-500",
160
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleAlert, { className: "size-3 shrink-0" }), issues[0]]
161
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
162
+ className: "mt-0.5 truncate text-xs text-muted-foreground",
163
+ children: summaryOf(loop)
164
+ })]
165
+ }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
167
+ checked: loop.enabled,
168
+ "aria-label": loop.enabled ? "Turn off" : "Turn on",
169
+ disabled: busy,
170
+ onCheckedChange: (enabled) => run(() => toggleLoop({ data: {
171
+ id: loop.id,
172
+ enabled
173
+ } }), enabled ? `${loop.name} is on` : `${loop.name} is off`)
174
+ }),
175
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownMenuTrigger, {
176
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
177
+ variant: "ghost",
178
+ size: "icon-sm",
179
+ "aria-label": "More",
180
+ disabled: busy
181
+ }),
182
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ellipsis, {})
183
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuContent, {
184
+ align: "end",
185
+ children: [
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
187
+ disabled: first,
188
+ onClick: () => run(() => reorderLoop({ data: {
189
+ id: loop.id,
190
+ direction: "up"
191
+ } })),
192
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUp, {}), "Move up"]
193
+ }),
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
195
+ disabled: last,
196
+ onClick: () => run(() => reorderLoop({ data: {
197
+ id: loop.id,
198
+ direction: "down"
199
+ } })),
200
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowDown, {}), "Move down"]
201
+ }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownMenuSeparator, {}),
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
204
+ variant: "destructive",
205
+ onClick: () => {
206
+ if (!window.confirm(`Delete ${loop.name}? Its runs are deleted with it.`)) return;
207
+ run(() => removeLoop({ data: { id: loop.id } }), `Deleted ${loop.name}`);
208
+ },
209
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, {}), "Delete"]
210
+ })
211
+ ]
212
+ })] })
213
+ ]
214
+ });
215
+ }
216
+ //#endregion
217
+ export { LoopsPage as component };