agent-board-runner 0.1.16 → 0.1.18

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 (3) hide show
  1. package/dist/cli.js +194 -25
  2. package/dist/index.js +194 -25
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -897,12 +897,37 @@ async function getRunningSessionForProject(supabase, projectId, runnerId) {
897
897
  const row = data;
898
898
  return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
899
899
  }
900
+ function compareQueueOrder(a, b) {
901
+ const orderA = a.cards.dev_queue_order;
902
+ const orderB = b.cards.dev_queue_order;
903
+ if (orderA !== null && orderB !== null) return orderA - orderB;
904
+ if (orderA !== null) return -1;
905
+ if (orderB !== null) return 1;
906
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
907
+ }
908
+ function applyDevQueueOrder(queued) {
909
+ const byProject = /* @__PURE__ */ new Map();
910
+ for (const entry of queued) {
911
+ const list = byProject.get(entry.cards.project_id) ?? [];
912
+ list.push(entry);
913
+ byProject.set(entry.cards.project_id, list);
914
+ }
915
+ for (const entries of byProject.values()) entries.sort(compareQueueOrder);
916
+ const seenProjects = /* @__PURE__ */ new Set();
917
+ const result = [];
918
+ for (const entry of queued) {
919
+ if (seenProjects.has(entry.cards.project_id)) continue;
920
+ seenProjects.add(entry.cards.project_id);
921
+ result.push(...byProject.get(entry.cards.project_id));
922
+ }
923
+ return result;
924
+ }
900
925
  async function listQueuedSessionsForRunner(supabase, runnerId) {
901
926
  const { data, error } = await supabase.from("sessions").select(
902
- "id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
927
+ "id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved, dev_queue_order)"
903
928
  ).eq("runner_id", runnerId).eq("status", "queued").order("created_at", { ascending: true });
904
929
  if (error) throw error;
905
- return data;
930
+ return applyDevQueueOrder(data);
906
931
  }
907
932
  async function listPausedSessionsForRunner(supabase, runnerId) {
908
933
  const { data, error } = await supabase.from("sessions").select("card_id, cards!card_id!inner(project_id)").eq("runner_id", runnerId).eq("status", "paused");
@@ -1010,7 +1035,8 @@ async function updateCard(supabase, cardId, changes) {
1010
1035
  ...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
1011
1036
  ...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
1012
1037
  ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
1013
- ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
1038
+ ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {},
1039
+ ...changes.returnToBacklogRequestedAt !== void 0 ? { return_to_backlog_requested_at: changes.returnToBacklogRequestedAt } : {}
1014
1040
  }).eq("id", cardId);
1015
1041
  if (error) throw error;
1016
1042
  }
@@ -2077,7 +2103,22 @@ function parseGitDiffNameStatus(output) {
2077
2103
  const columns = line.split(" ");
2078
2104
  const status = columns[0].slice(0, 1);
2079
2105
  const path = columns[columns.length - 1];
2080
- return { path, status };
2106
+ return { path, status, additions: null, deletions: null, binary: false };
2107
+ });
2108
+ }
2109
+ function parseGitDiffNumstat(output) {
2110
+ return output.split("\n").filter((line) => line.length > 0).map((line) => {
2111
+ const [added, deleted, path] = line.split(" ");
2112
+ const binary = added === "-" && deleted === "-";
2113
+ return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
2114
+ });
2115
+ }
2116
+ function mergeNumstat(files, numstat) {
2117
+ const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
2118
+ return files.map((file) => {
2119
+ const stat = byPath.get(file.path);
2120
+ if (!stat) return file;
2121
+ return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
2081
2122
  });
2082
2123
  }
2083
2124
  async function getRepoDiffForTarget(label, cwd) {
@@ -2111,14 +2152,84 @@ async function getRepoDiffForTarget(label, cwd) {
2111
2152
  files = parseGitDiffNameStatus(stdout);
2112
2153
  } catch (err) {
2113
2154
  const message = err instanceof Error ? err.message : String(err);
2114
- return { label, files: [], diff: "", error: message };
2155
+ return { label, files: [], diff: "", branch, error: message };
2156
+ }
2157
+ if (files.length === 0) return { label, files, diff: "", branch };
2158
+ try {
2159
+ const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
2160
+ files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
2161
+ } catch {
2115
2162
  }
2116
- if (files.length === 0) return { label, files, diff: "" };
2117
2163
  try {
2118
2164
  const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
2119
- return { label, files, diff: stdout };
2165
+ return { label, files, diff: stdout, branch };
2120
2166
  } catch {
2121
- return { label, files, diff: "" };
2167
+ return { label, files, diff: "", branch };
2168
+ }
2169
+ }
2170
+ async function resolveDiffTargetCwd(entry, projectId, label) {
2171
+ if (!label) return entry.repoRoot;
2172
+ if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
2173
+ const children = await getProjectRepos(statuslineSupabase, projectId);
2174
+ const child2 = children.find((candidate) => candidate.subpath === label);
2175
+ if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
2176
+ return join9(entry.repoRoot, child2.subpath);
2177
+ }
2178
+ async function readFileAtRevision(cwd, ref, relPath) {
2179
+ try {
2180
+ const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
2181
+ cwd,
2182
+ maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
2183
+ encoding: "buffer"
2184
+ });
2185
+ return stdout;
2186
+ } catch {
2187
+ return null;
2188
+ }
2189
+ }
2190
+ function toFileRevisionJson(buffer, imageContentType) {
2191
+ if (buffer === null) return { exists: false, content: null, binary: false };
2192
+ if (looksBinary(buffer)) {
2193
+ return {
2194
+ exists: true,
2195
+ content: null,
2196
+ binary: true,
2197
+ ...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
2198
+ };
2199
+ }
2200
+ return { exists: true, content: buffer.toString("utf8"), binary: false };
2201
+ }
2202
+ async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
2203
+ if (!relPath) {
2204
+ sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
2205
+ return;
2206
+ }
2207
+ try {
2208
+ const cwd = await resolveDiffTargetCwd(entry, projectId, label);
2209
+ resolveWithinRepo(cwd, relPath);
2210
+ const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
2211
+ const branch = branchOut.trim();
2212
+ if (!branch) {
2213
+ sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
2214
+ return;
2215
+ }
2216
+ const [originalBuffer, modifiedBuffer] = await Promise.all([
2217
+ readFileAtRevision(cwd, `origin/${branch}`, relPath),
2218
+ readFileAtRevision(cwd, "HEAD", relPath)
2219
+ ]);
2220
+ const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
2221
+ const imageContentType = MIME_TYPES_BY_EXT[ext];
2222
+ sendJson(res, 200, {
2223
+ original: toFileRevisionJson(originalBuffer, imageContentType),
2224
+ modified: toFileRevisionJson(modifiedBuffer, imageContentType)
2225
+ });
2226
+ } catch (err) {
2227
+ if (err instanceof HttpError) {
2228
+ sendJson(res, err.status, { error: err.message });
2229
+ return;
2230
+ }
2231
+ logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
2232
+ sendJson(res, 500, { error: "Erro interno do runner." });
2122
2233
  }
2123
2234
  }
2124
2235
  async function handleDiffRequest(entry, res, projectId) {
@@ -2303,6 +2414,12 @@ function handleRequest(req, res) {
2303
2414
  void handleDiffRequest(entry, res, projectId);
2304
2415
  return;
2305
2416
  }
2417
+ if (pathname === "/diff-file") {
2418
+ const projectId = typeof query.projectId === "string" ? query.projectId : "";
2419
+ const label = typeof query.label === "string" ? query.label : null;
2420
+ void handleDiffFileRequest(entry, res, projectId, relPath, label);
2421
+ return;
2422
+ }
2306
2423
  try {
2307
2424
  if (pathname === "/tree") {
2308
2425
  sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
@@ -2551,13 +2668,49 @@ function toSessionName(title) {
2551
2668
  // src/agentSession.ts
2552
2669
  var TUI_IDLE_MS = 1500;
2553
2670
  var TUI_READY_TIMEOUT_MS = 6e4;
2554
- var SUBMIT_DELAY_MS = 150;
2671
+ var PASTE_SETTLE_MS = 200;
2672
+ var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2555
2673
  var PASTE_START = "\x1B[200~";
2556
2674
  var PASTE_END = "\x1B[201~";
2557
2675
  var SHIFT_TAB = "\x1B[Z";
2558
2676
  var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2559
2677
  var SHIFT_TAB_DELAY_MS = 400;
2560
2678
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2679
+ function writePasteAndSubmit(pty, cardId, message, context) {
2680
+ pty.write(`${PASTE_START}${message}${PASTE_END}`);
2681
+ let settleTimer;
2682
+ let ceilingTimer;
2683
+ let dataSubscription;
2684
+ let exitSubscription;
2685
+ let submitted = false;
2686
+ const submit = () => {
2687
+ if (submitted) return;
2688
+ submitted = true;
2689
+ clearTimeout(settleTimer);
2690
+ clearTimeout(ceilingTimer);
2691
+ dataSubscription?.dispose();
2692
+ exitSubscription?.dispose();
2693
+ try {
2694
+ pty.write("\r");
2695
+ } catch (err) {
2696
+ logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2697
+ }
2698
+ };
2699
+ const cancel = () => {
2700
+ submitted = true;
2701
+ clearTimeout(settleTimer);
2702
+ clearTimeout(ceilingTimer);
2703
+ dataSubscription?.dispose();
2704
+ exitSubscription?.dispose();
2705
+ };
2706
+ dataSubscription = pty.onData(() => {
2707
+ clearTimeout(settleTimer);
2708
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2709
+ });
2710
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2711
+ ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2712
+ exitSubscription = pty.onExit(cancel);
2713
+ }
2561
2714
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2562
2715
  ensureDirTrusted(repoPath);
2563
2716
  ensureMcpServersTrusted(repoPath);
@@ -2601,14 +2754,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2601
2754
  if (!stopWaiting()) return;
2602
2755
  if (planFirst) await enterPlanMode(pty, cardId);
2603
2756
  try {
2604
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2605
- setTimeout(() => {
2606
- try {
2607
- pty.write("\r");
2608
- } catch (err) {
2609
- logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
2610
- }
2611
- }, SUBMIT_DELAY_MS);
2757
+ writePasteAndSubmit(pty, cardId, message, "the card instructions");
2612
2758
  } catch (err) {
2613
2759
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2614
2760
  }
@@ -2644,14 +2790,7 @@ async function enterPlanMode(pty, cardId) {
2644
2790
  }
2645
2791
  function sendMessageToIdleSession(pty, cardId, message) {
2646
2792
  try {
2647
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2648
- setTimeout(() => {
2649
- try {
2650
- pty.write("\r");
2651
- } catch (err) {
2652
- logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
2653
- }
2654
- }, SUBMIT_DELAY_MS);
2793
+ writePasteAndSubmit(pty, cardId, message, "a message");
2655
2794
  } catch (err) {
2656
2795
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2657
2796
  }
@@ -3413,6 +3552,24 @@ async function restartCardSession(supabase, config, card) {
3413
3552
  await updateCard(supabase, card.id, { restartRequestedAt: null });
3414
3553
  await enqueueCard(supabase, config, card);
3415
3554
  }
3555
+ async function returnCardToBacklog(supabase, config, card) {
3556
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3557
+ if (sessionId) {
3558
+ intentionallyStoppedSessionIds.add(sessionId);
3559
+ stopPtySession(sessionId);
3560
+ cancelUsageResume(sessionId);
3561
+ activeSessionByCard.delete(card.id);
3562
+ await updateSession(
3563
+ supabase,
3564
+ sessionId,
3565
+ { status: "done", endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3566
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3567
+ );
3568
+ logger.card.info(card.id, `Session ${sessionId} stopped by "voltar ao backlog".`);
3569
+ }
3570
+ await updateCard(supabase, card.id, { column: "backlog", returnToBacklogRequestedAt: null });
3571
+ await dispatchRunnerQueue(supabase, config);
3572
+ }
3416
3573
 
3417
3574
  // src/columnActions.ts
3418
3575
  var execAsync = promisify5(exec);
@@ -4206,6 +4363,8 @@ async function startRunner() {
4206
4363
  const lastKnownColumn = /* @__PURE__ */ new Map();
4207
4364
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
4208
4365
  const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
4366
+ const lastKnownDevQueueOrder = /* @__PURE__ */ new Map();
4367
+ const lastKnownReturnToBacklogRequestedAt = /* @__PURE__ */ new Map();
4209
4368
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
4210
4369
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
4211
4370
  for (const project of config.registeredProjects) {
@@ -4311,6 +4470,16 @@ async function startRunner() {
4311
4470
  if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
4312
4471
  void restartCardSession(supabase, config, card);
4313
4472
  }
4473
+ const previousReturnToBacklogRequestedAt = lastKnownReturnToBacklogRequestedAt.get(card.id);
4474
+ lastKnownReturnToBacklogRequestedAt.set(card.id, card.return_to_backlog_requested_at);
4475
+ if (card.return_to_backlog_requested_at && card.return_to_backlog_requested_at !== previousReturnToBacklogRequestedAt) {
4476
+ void returnCardToBacklog(supabase, config, card);
4477
+ }
4478
+ const previousDevQueueOrder = lastKnownDevQueueOrder.get(card.id);
4479
+ lastKnownDevQueueOrder.set(card.id, card.dev_queue_order);
4480
+ if (card.column_name === "desenvolvimento" && card.dev_queue_order !== previousDevQueueOrder) {
4481
+ void dispatchRunnerQueue(supabase, config);
4482
+ }
4314
4483
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
4315
4484
  void clearColumnActionStatus(supabase, card);
4316
4485
  void handleColumnEntered(supabase, config, card);
package/dist/index.js CHANGED
@@ -617,12 +617,37 @@ async function getRunningSessionForProject(supabase, projectId, runnerId) {
617
617
  const row = data;
618
618
  return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
619
619
  }
620
+ function compareQueueOrder(a, b) {
621
+ const orderA = a.cards.dev_queue_order;
622
+ const orderB = b.cards.dev_queue_order;
623
+ if (orderA !== null && orderB !== null) return orderA - orderB;
624
+ if (orderA !== null) return -1;
625
+ if (orderB !== null) return 1;
626
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
627
+ }
628
+ function applyDevQueueOrder(queued) {
629
+ const byProject = /* @__PURE__ */ new Map();
630
+ for (const entry of queued) {
631
+ const list = byProject.get(entry.cards.project_id) ?? [];
632
+ list.push(entry);
633
+ byProject.set(entry.cards.project_id, list);
634
+ }
635
+ for (const entries of byProject.values()) entries.sort(compareQueueOrder);
636
+ const seenProjects = /* @__PURE__ */ new Set();
637
+ const result = [];
638
+ for (const entry of queued) {
639
+ if (seenProjects.has(entry.cards.project_id)) continue;
640
+ seenProjects.add(entry.cards.project_id);
641
+ result.push(...byProject.get(entry.cards.project_id));
642
+ }
643
+ return result;
644
+ }
620
645
  async function listQueuedSessionsForRunner(supabase, runnerId) {
621
646
  const { data, error } = await supabase.from("sessions").select(
622
- "id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
647
+ "id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved, dev_queue_order)"
623
648
  ).eq("runner_id", runnerId).eq("status", "queued").order("created_at", { ascending: true });
624
649
  if (error) throw error;
625
- return data;
650
+ return applyDevQueueOrder(data);
626
651
  }
627
652
  async function listPausedSessionsForRunner(supabase, runnerId) {
628
653
  const { data, error } = await supabase.from("sessions").select("card_id, cards!card_id!inner(project_id)").eq("runner_id", runnerId).eq("status", "paused");
@@ -730,7 +755,8 @@ async function updateCard(supabase, cardId, changes) {
730
755
  ...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
731
756
  ...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
732
757
  ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
733
- ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
758
+ ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {},
759
+ ...changes.returnToBacklogRequestedAt !== void 0 ? { return_to_backlog_requested_at: changes.returnToBacklogRequestedAt } : {}
734
760
  }).eq("id", cardId);
735
761
  if (error) throw error;
736
762
  }
@@ -1797,7 +1823,22 @@ function parseGitDiffNameStatus(output) {
1797
1823
  const columns = line.split(" ");
1798
1824
  const status = columns[0].slice(0, 1);
1799
1825
  const path = columns[columns.length - 1];
1800
- return { path, status };
1826
+ return { path, status, additions: null, deletions: null, binary: false };
1827
+ });
1828
+ }
1829
+ function parseGitDiffNumstat(output) {
1830
+ return output.split("\n").filter((line) => line.length > 0).map((line) => {
1831
+ const [added, deleted, path] = line.split(" ");
1832
+ const binary = added === "-" && deleted === "-";
1833
+ return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
1834
+ });
1835
+ }
1836
+ function mergeNumstat(files, numstat) {
1837
+ const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
1838
+ return files.map((file) => {
1839
+ const stat = byPath.get(file.path);
1840
+ if (!stat) return file;
1841
+ return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
1801
1842
  });
1802
1843
  }
1803
1844
  async function getRepoDiffForTarget(label, cwd) {
@@ -1831,14 +1872,84 @@ async function getRepoDiffForTarget(label, cwd) {
1831
1872
  files = parseGitDiffNameStatus(stdout);
1832
1873
  } catch (err) {
1833
1874
  const message = err instanceof Error ? err.message : String(err);
1834
- return { label, files: [], diff: "", error: message };
1875
+ return { label, files: [], diff: "", branch, error: message };
1876
+ }
1877
+ if (files.length === 0) return { label, files, diff: "", branch };
1878
+ try {
1879
+ const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
1880
+ files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
1881
+ } catch {
1835
1882
  }
1836
- if (files.length === 0) return { label, files, diff: "" };
1837
1883
  try {
1838
1884
  const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
1839
- return { label, files, diff: stdout };
1885
+ return { label, files, diff: stdout, branch };
1840
1886
  } catch {
1841
- return { label, files, diff: "" };
1887
+ return { label, files, diff: "", branch };
1888
+ }
1889
+ }
1890
+ async function resolveDiffTargetCwd(entry, projectId, label) {
1891
+ if (!label) return entry.repoRoot;
1892
+ if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
1893
+ const children = await getProjectRepos(statuslineSupabase, projectId);
1894
+ const child2 = children.find((candidate) => candidate.subpath === label);
1895
+ if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
1896
+ return join7(entry.repoRoot, child2.subpath);
1897
+ }
1898
+ async function readFileAtRevision(cwd, ref, relPath) {
1899
+ try {
1900
+ const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
1901
+ cwd,
1902
+ maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
1903
+ encoding: "buffer"
1904
+ });
1905
+ return stdout;
1906
+ } catch {
1907
+ return null;
1908
+ }
1909
+ }
1910
+ function toFileRevisionJson(buffer, imageContentType) {
1911
+ if (buffer === null) return { exists: false, content: null, binary: false };
1912
+ if (looksBinary(buffer)) {
1913
+ return {
1914
+ exists: true,
1915
+ content: null,
1916
+ binary: true,
1917
+ ...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
1918
+ };
1919
+ }
1920
+ return { exists: true, content: buffer.toString("utf8"), binary: false };
1921
+ }
1922
+ async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
1923
+ if (!relPath) {
1924
+ sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
1925
+ return;
1926
+ }
1927
+ try {
1928
+ const cwd = await resolveDiffTargetCwd(entry, projectId, label);
1929
+ resolveWithinRepo(cwd, relPath);
1930
+ const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
1931
+ const branch = branchOut.trim();
1932
+ if (!branch) {
1933
+ sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
1934
+ return;
1935
+ }
1936
+ const [originalBuffer, modifiedBuffer] = await Promise.all([
1937
+ readFileAtRevision(cwd, `origin/${branch}`, relPath),
1938
+ readFileAtRevision(cwd, "HEAD", relPath)
1939
+ ]);
1940
+ const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
1941
+ const imageContentType = MIME_TYPES_BY_EXT[ext];
1942
+ sendJson(res, 200, {
1943
+ original: toFileRevisionJson(originalBuffer, imageContentType),
1944
+ modified: toFileRevisionJson(modifiedBuffer, imageContentType)
1945
+ });
1946
+ } catch (err) {
1947
+ if (err instanceof HttpError) {
1948
+ sendJson(res, err.status, { error: err.message });
1949
+ return;
1950
+ }
1951
+ logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
1952
+ sendJson(res, 500, { error: "Erro interno do runner." });
1842
1953
  }
1843
1954
  }
1844
1955
  async function handleDiffRequest(entry, res, projectId) {
@@ -2023,6 +2134,12 @@ function handleRequest(req, res) {
2023
2134
  void handleDiffRequest(entry, res, projectId);
2024
2135
  return;
2025
2136
  }
2137
+ if (pathname === "/diff-file") {
2138
+ const projectId = typeof query.projectId === "string" ? query.projectId : "";
2139
+ const label = typeof query.label === "string" ? query.label : null;
2140
+ void handleDiffFileRequest(entry, res, projectId, relPath, label);
2141
+ return;
2142
+ }
2026
2143
  try {
2027
2144
  if (pathname === "/tree") {
2028
2145
  sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
@@ -2439,13 +2556,49 @@ function toSessionName(title) {
2439
2556
  // src/agentSession.ts
2440
2557
  var TUI_IDLE_MS = 1500;
2441
2558
  var TUI_READY_TIMEOUT_MS = 6e4;
2442
- var SUBMIT_DELAY_MS = 150;
2559
+ var PASTE_SETTLE_MS = 200;
2560
+ var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2443
2561
  var PASTE_START = "\x1B[200~";
2444
2562
  var PASTE_END = "\x1B[201~";
2445
2563
  var SHIFT_TAB = "\x1B[Z";
2446
2564
  var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2447
2565
  var SHIFT_TAB_DELAY_MS = 400;
2448
2566
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2567
+ function writePasteAndSubmit(pty, cardId, message, context) {
2568
+ pty.write(`${PASTE_START}${message}${PASTE_END}`);
2569
+ let settleTimer;
2570
+ let ceilingTimer;
2571
+ let dataSubscription;
2572
+ let exitSubscription;
2573
+ let submitted = false;
2574
+ const submit = () => {
2575
+ if (submitted) return;
2576
+ submitted = true;
2577
+ clearTimeout(settleTimer);
2578
+ clearTimeout(ceilingTimer);
2579
+ dataSubscription?.dispose();
2580
+ exitSubscription?.dispose();
2581
+ try {
2582
+ pty.write("\r");
2583
+ } catch (err) {
2584
+ logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2585
+ }
2586
+ };
2587
+ const cancel = () => {
2588
+ submitted = true;
2589
+ clearTimeout(settleTimer);
2590
+ clearTimeout(ceilingTimer);
2591
+ dataSubscription?.dispose();
2592
+ exitSubscription?.dispose();
2593
+ };
2594
+ dataSubscription = pty.onData(() => {
2595
+ clearTimeout(settleTimer);
2596
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2597
+ });
2598
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2599
+ ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2600
+ exitSubscription = pty.onExit(cancel);
2601
+ }
2449
2602
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2450
2603
  ensureDirTrusted(repoPath);
2451
2604
  ensureMcpServersTrusted(repoPath);
@@ -2489,14 +2642,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2489
2642
  if (!stopWaiting()) return;
2490
2643
  if (planFirst) await enterPlanMode(pty, cardId);
2491
2644
  try {
2492
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2493
- setTimeout(() => {
2494
- try {
2495
- pty.write("\r");
2496
- } catch (err) {
2497
- logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
2498
- }
2499
- }, SUBMIT_DELAY_MS);
2645
+ writePasteAndSubmit(pty, cardId, message, "the card instructions");
2500
2646
  } catch (err) {
2501
2647
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2502
2648
  }
@@ -2532,14 +2678,7 @@ async function enterPlanMode(pty, cardId) {
2532
2678
  }
2533
2679
  function sendMessageToIdleSession(pty, cardId, message) {
2534
2680
  try {
2535
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2536
- setTimeout(() => {
2537
- try {
2538
- pty.write("\r");
2539
- } catch (err) {
2540
- logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
2541
- }
2542
- }, SUBMIT_DELAY_MS);
2681
+ writePasteAndSubmit(pty, cardId, message, "a message");
2543
2682
  } catch (err) {
2544
2683
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2545
2684
  }
@@ -3301,6 +3440,24 @@ async function restartCardSession(supabase, config, card) {
3301
3440
  await updateCard(supabase, card.id, { restartRequestedAt: null });
3302
3441
  await enqueueCard(supabase, config, card);
3303
3442
  }
3443
+ async function returnCardToBacklog(supabase, config, card) {
3444
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3445
+ if (sessionId) {
3446
+ intentionallyStoppedSessionIds.add(sessionId);
3447
+ stopPtySession(sessionId);
3448
+ cancelUsageResume(sessionId);
3449
+ activeSessionByCard.delete(card.id);
3450
+ await updateSession(
3451
+ supabase,
3452
+ sessionId,
3453
+ { status: "done", endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3454
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3455
+ );
3456
+ logger.card.info(card.id, `Session ${sessionId} stopped by "voltar ao backlog".`);
3457
+ }
3458
+ await updateCard(supabase, card.id, { column: "backlog", returnToBacklogRequestedAt: null });
3459
+ await dispatchRunnerQueue(supabase, config);
3460
+ }
3304
3461
 
3305
3462
  // src/columnActions.ts
3306
3463
  var execAsync = promisify5(exec);
@@ -4094,6 +4251,8 @@ async function startRunner() {
4094
4251
  const lastKnownColumn = /* @__PURE__ */ new Map();
4095
4252
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
4096
4253
  const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
4254
+ const lastKnownDevQueueOrder = /* @__PURE__ */ new Map();
4255
+ const lastKnownReturnToBacklogRequestedAt = /* @__PURE__ */ new Map();
4097
4256
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
4098
4257
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
4099
4258
  for (const project of config.registeredProjects) {
@@ -4199,6 +4358,16 @@ async function startRunner() {
4199
4358
  if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
4200
4359
  void restartCardSession(supabase, config, card);
4201
4360
  }
4361
+ const previousReturnToBacklogRequestedAt = lastKnownReturnToBacklogRequestedAt.get(card.id);
4362
+ lastKnownReturnToBacklogRequestedAt.set(card.id, card.return_to_backlog_requested_at);
4363
+ if (card.return_to_backlog_requested_at && card.return_to_backlog_requested_at !== previousReturnToBacklogRequestedAt) {
4364
+ void returnCardToBacklog(supabase, config, card);
4365
+ }
4366
+ const previousDevQueueOrder = lastKnownDevQueueOrder.get(card.id);
4367
+ lastKnownDevQueueOrder.set(card.id, card.dev_queue_order);
4368
+ if (card.column_name === "desenvolvimento" && card.dev_queue_order !== previousDevQueueOrder) {
4369
+ void dispatchRunnerQueue(supabase, config);
4370
+ }
4202
4371
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
4203
4372
  void clearColumnActionStatus(supabase, card);
4204
4373
  void handleColumnEntered(supabase, config, card);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-board-runner",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
5
5
  "type": "module",
6
6
  "bin": {