@jeffober/thatch 0.1.37 → 0.1.38

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -110,6 +110,24 @@ export const server: Plugin = async ({ client, worktree }) => {
110
110
  parts: [{ type: "text", text: watcherNotificationNudge(events[0]?.target ?? "watched target", events), synthetic: true }],
111
111
  },
112
112
  });
113
+ // Toast: the notification part is TUI-hidden, so without this the
114
+ // user watching the session sees the model wake up with no visible
115
+ // cause. Announce what fired. Failed checks/workflows surface as a
116
+ // warning variant; routine events as info. Best-effort - the TUI may
117
+ // not be connected (headless mode).
118
+ const failed = events.some((e) => e.summary.includes("failure"));
119
+ const more = events.length > 1 ? ` +${events.length - 1} more` : "";
120
+ try {
121
+ await client.tui.showToast({
122
+ body: {
123
+ message: `\u23F0 ${events[0]?.target ?? "watched target"}: ${events[0]?.summary ?? ""}${more}`,
124
+ variant: failed ? "warning" : "info",
125
+ duration: 5000,
126
+ },
127
+ });
128
+ } catch {
129
+ // TUI may not be connected. Best-effort.
130
+ }
113
131
  },
114
132
  canDeliver: (sessionID) =>
115
133
  !compacting.has(sessionID) && sessionStatus.get(sessionID) === "idle",
package/src/watchers.ts CHANGED
@@ -97,6 +97,9 @@ export interface WorkflowRunRef {
97
97
  url: string;
98
98
  /** What triggered the run: push, pull_request, schedule, workflow_run, ... */
99
99
  event: string;
100
+ /** The branch the run's head commit is on (a tag-triggered run puts the TAG name here). */
101
+ headBranch: string;
102
+ headSha: string;
100
103
  }
101
104
 
102
105
  // ---------------------------------------------------------------------------
@@ -303,6 +306,8 @@ function parseWorkflowRuns(raw: unknown): Record<string, WorkflowRunRef> {
303
306
  conclusion?: string | null;
304
307
  html_url?: string;
305
308
  event?: string;
309
+ head_branch?: string;
310
+ head_sha?: string;
306
311
  }> }).workflow_runs ?? [];
307
312
  for (const run of rawRuns) {
308
313
  if (run.id === undefined) continue;
@@ -312,6 +317,8 @@ function parseWorkflowRuns(raw: unknown): Record<string, WorkflowRunRef> {
312
317
  conclusion: run.conclusion ?? null,
313
318
  url: run.html_url ?? "",
314
319
  event: run.event ?? "unknown",
320
+ headBranch: run.head_branch ?? "",
321
+ headSha: run.head_sha ?? "",
315
322
  };
316
323
  }
317
324
  return runs;
@@ -375,20 +382,30 @@ export async function fetchPrState(gh: GhRunner, repo: string, pr: number): Prom
375
382
 
376
383
  /**
377
384
  * Fetches the current state of a branch: the head commit, check runs on that
378
- * head, and the branch's recent workflow runs (last 20). The branch and
385
+ * head, and recent workflow runs relevant to the branch. The branch and
379
386
  * workflow-run calls run in parallel; check runs need the head SHA first.
387
+ *
388
+ * Workflow runs are fetched WITHOUT the API's branch filter and filtered
389
+ * client-side: keep runs whose head_branch matches the branch (push runs)
390
+ * or whose head_sha is the branch's current head (tag-triggered runs like a
391
+ * release Publish run - their head_branch is the tag name, so the API's
392
+ * branch filter would hide exactly the runs a release watch exists for).
380
393
  */
381
394
  export async function fetchBranchState(gh: GhRunner, repo: string, branch: string): Promise<BranchState> {
382
395
  const [branchInfo, runs] = await Promise.all([
383
396
  gh([`/repos/${repo}/branches/${branch}`]),
384
- gh([`/repos/${repo}/actions/runs?branch=${branch}&per_page=20`]),
397
+ gh([`/repos/${repo}/actions/runs?per_page=30`]),
385
398
  ]);
386
399
  const headSha = (branchInfo as { commit?: { sha?: string } }).commit?.sha ?? "";
400
+ const allRuns = parseWorkflowRuns(runs);
401
+ const relevant = Object.fromEntries(
402
+ Object.entries(allRuns).filter(([, run]) => run.headBranch === branch || run.headSha === headSha),
403
+ );
387
404
  const checkRuns = await gh([`/repos/${repo}/commits/${headSha}/check-runs?per_page=100`]);
388
405
  return {
389
406
  headSha,
390
407
  checkRuns: parseCheckRuns(checkRuns),
391
- workflowRuns: parseWorkflowRuns(runs),
408
+ workflowRuns: relevant,
392
409
  };
393
410
  }
394
411