opencode-forgekeeper 1.0.0 → 2.1.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 (4) hide show
  1. package/README.md +24 -14
  2. package/dist/index.js +76 -71
  3. package/dist/tui.js +444 -101
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -296,8 +296,9 @@ title prefix. It doesn't poll or send review notifications, and the CLI shows no
296
296
 
297
297
  ## RPC
298
298
 
299
- The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) that the CLI
300
- uses, defined in `src/rpc.ts`:
299
+ The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) with the ID
300
+ `opencode-forgekeeper`, defined in `src/rpc.ts`. The CLI uses it, and other plugins can call
301
+ `target` and listen for `targetChanged`:
301
302
 
302
303
  - `target({ sessionID })` returns `{ url, issueUrl?, targets }` for explicit targets, and
303
304
  `{}` in branch mode. `targets` lists every target as `{ url, issueUrl? }`. `url` and
@@ -318,18 +319,27 @@ plugin instance watches its own sessions.
318
319
 
319
320
  ## Migrating from opencode-forge-session-title
320
321
 
321
- Replace `opencode-forge-session-title` with `opencode-forgekeeper` in `opencode.json`. Never
322
- load both: the server keeps `opencode-forge-session-title`'s plugin and RPC IDs, so the two
323
- would collide.
324
-
325
- | Entry point | Plugin ID | Stored state |
326
- | ----------- | ------------------------------ | ---------------------------------------------------------------------------------------- |
327
- | Server | `opencode-forge-session-title` | Session targets, owned title prefixes, human-review baselines, and pending notifications |
328
- | CLI | `pedropombeiro.forgekeeper` | None |
329
-
330
- Because the IDs match, stored targets carry over, and callers of the `target` RPC keep working.
331
- The server reads a session's single stored `target` as a list of one, and writes `targets` from
332
- the next change on.
322
+ Replace `opencode-forge-session-title` with `opencode-forgekeeper` in `opencode.json`. Upgrading
323
+ from `opencode-forgekeeper` 1.x works the same way, because 1.x used the same IDs as
324
+ `opencode-forge-session-title`.
325
+
326
+ | Entry point | Plugin ID | Stored state |
327
+ | ----------- | --------------------------- | ---------------------------------------------------------------------------------------- |
328
+ | Server | `opencode-forgekeeper` | Session targets, owned title prefixes, human-review baselines, and pending notifications |
329
+ | CLI | `pedropombeiro.forgekeeper` | None |
330
+
331
+ OpenCode scopes plugin storage by plugin ID, so nothing stored under the former ID,
332
+ `opencode-forge-session-title`, carries over:
333
+
334
+ - Sessions start in branch-based naming. Ask the agent to set their targets again.
335
+ - A title that kept a single-reference prefix from before, such as `[!456] Review`, now looks
336
+ like a reference that you wrote. Until you set a target, the status follows that PR/MR, labeled
337
+ "from the session title". Setting a target adds a second prefix, such as
338
+ `[#12, !45] [!456] Review`. Remove the older prefix by hand. Older prefixes with two
339
+ references, such as `[#12, !45]`, are replaced automatically.
340
+ - The first look at each PR/MR records a new review baseline, so comments posted before then
341
+ aren't announced.
342
+ - Callers of the RPC need the new ID, `opencode-forgekeeper`.
333
343
 
334
344
  With default options, the new footer and review notifications start right away, and
335
345
  notifications resume the session. Set `reviewStatus` to `false` to keep only titles, or
package/dist/index.js CHANGED
@@ -185,10 +185,14 @@ class ForgeError extends Error {
185
185
  }
186
186
  }
187
187
 
188
+ // packages/forgekeeper/src/graphql.ts
189
+ var query = (text) => text;
190
+ var typed = (transport) => async (host, query2, variables) => await transport(host, query2, variables);
191
+
188
192
  // packages/forgekeeper/src/gitlab-feedback.ts
189
193
  var PAGE_SIZE = 100;
190
194
  var MAX_COMMENT_PAGES = 20;
191
- var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
195
+ var COMMENTS_QUERY = query(`query($project: ID!, $iid: String!, $before: String) {
192
196
  currentUser { id }
193
197
  project(fullPath: $project) {
194
198
  mergeRequest(iid: $iid) {
@@ -202,7 +206,7 @@ var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
202
206
  }
203
207
  }
204
208
  }
205
- }`;
209
+ }`);
206
210
  var noteNumber = (id) => id.slice(id.lastIndexOf("/") + 1);
207
211
  var timestamp = (node) => Date.parse(node.lastEditedAt ?? node.updatedAt ?? node.createdAt);
208
212
  function toComment(node) {
@@ -221,7 +225,8 @@ function toComment(node) {
221
225
  username: node.author.username
222
226
  };
223
227
  }
224
- async function fetchComments(graphql, host, project, iid) {
228
+ async function fetchComments(transport, host, project, iid) {
229
+ const graphql = typed(transport);
225
230
  const pages = [];
226
231
  let viewerID;
227
232
  let before;
@@ -235,8 +240,8 @@ async function fetchComments(graphql, host, project, iid) {
235
240
  if (before)
236
241
  variables.before = before;
237
242
  const data = await graphql(host, COMMENTS_QUERY, variables);
238
- viewerID ??= data?.currentUser?.id ?? undefined;
239
- const connection = data?.project?.mergeRequest?.notes;
243
+ viewerID ??= data.currentUser?.id ?? undefined;
244
+ const connection = data.project?.mergeRequest?.notes;
240
245
  if (!connection)
241
246
  return page === 0 ? undefined : snapshot(false);
242
247
  pages.push(connection.nodes.flatMap((node) => toComment(node) ?? []));
@@ -268,17 +273,17 @@ function mergeRequestsQuery(projectCount) {
268
273
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { ${MERGE_REQUEST_FIELDS} } }
269
274
  }`).join(`
270
275
  `);
271
- return `query($branch: String!, ${variables}) {
276
+ return query(`query($branch: String!, ${variables}) {
272
277
  ${projects}
273
- }`;
278
+ }`);
274
279
  }
275
280
  function mergeRequestQuery(projectCount) {
276
281
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
277
282
  const projects = Array.from({ length: projectCount }, (_, index) => `p${index}: project(fullPath: $p${index}) { mergeRequest(iid: $iid) { ${MERGE_REQUEST_FIELDS} } }`).join(`
278
283
  `);
279
- return `query($iid: String!, ${variables}) {
284
+ return query(`query($iid: String!, ${variables}) {
280
285
  ${projects}
281
- }`;
286
+ }`);
282
287
  }
283
288
  function mergeRequestNumbersQuery(projectCount) {
284
289
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
@@ -286,18 +291,18 @@ function mergeRequestNumbersQuery(projectCount) {
286
291
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { iid updatedAt sourceProject { fullPath } } }
287
292
  }`).join(`
288
293
  `);
289
- return `query($branch: String!, ${variables}) {
294
+ return query(`query($branch: String!, ${variables}) {
290
295
  ${projects}
291
- }`;
296
+ }`);
292
297
  }
293
- var SOURCE_BRANCH_QUERY = `query($project: ID!, $iid: String!) {
298
+ var SOURCE_BRANCH_QUERY = query(`query($project: ID!, $iid: String!) {
294
299
  project(fullPath: $project) { mergeRequest(iid: $iid) { sourceBranch } }
295
- }`;
296
- var DISCUSSIONS_QUERY = `query($project: ID!, $iid: String!, $after: String) {
300
+ }`);
301
+ var DISCUSSIONS_QUERY = query(`query($project: ID!, $iid: String!, $after: String) {
297
302
  project(fullPath: $project) {
298
303
  mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE2}, after: $after) { ${DISCUSSIONS} } }
299
304
  }
300
- }`;
305
+ }`);
301
306
  function classifyError(message, code) {
302
307
  const text = message.trim().replace(/^glab:\s*/, "") || `glab exited with code ${code}`;
303
308
  if (/ENOENT|command not found/i.test(text))
@@ -312,8 +317,8 @@ function classifyError(message, code) {
312
317
  }
313
318
  var isResponse = (body) => typeof body === "object" && body !== null && (!("errors" in body) || Array.isArray(body.errors)) && (!("data" in body) || typeof body.data === "object");
314
319
  function glabGraphQL(run, cwd) {
315
- return async (host, query, variables) => {
316
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
320
+ return async (host, query2, variables) => {
321
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
317
322
  for (const [name, value] of Object.entries(variables))
318
323
  args.push("-f", `${name}=${value}`);
319
324
  const result = await run("glab", args, cwd);
@@ -381,10 +386,10 @@ function duoState(state) {
381
386
  }
382
387
 
383
388
  class GitLabForge {
384
- graphql;
385
389
  traits = gitlabTraits;
390
+ graphql;
386
391
  constructor(graphql) {
387
- this.graphql = graphql;
392
+ this.graphql = typed(graphql);
388
393
  }
389
394
  async findByBranch(repository) {
390
395
  const { source, targets, sourceBranch } = repository;
@@ -395,7 +400,7 @@ class GitLabForge {
395
400
  const seen = new Set;
396
401
  const nodes = [];
397
402
  targets.forEach((_, index) => {
398
- for (const node of data?.[`p${index}`]?.mergeRequests?.nodes ?? []) {
403
+ for (const node of data[`p${index}`]?.mergeRequests?.nodes ?? []) {
399
404
  if (node.sourceProject?.fullPath.toLowerCase() !== sourcePath)
400
405
  continue;
401
406
  const key = `${node.targetProject.fullPath}!${node.iid}`;
@@ -414,7 +419,7 @@ class GitLabForge {
414
419
  targets.forEach((target, index) => variables[`p${index}`] = target.path);
415
420
  const data = await this.graphql(source.host, mergeRequestNumbersQuery(targets.length), variables);
416
421
  const sourcePath = source.path.toLowerCase();
417
- const nodes = targets.flatMap((_, index) => data?.[`p${index}`]?.mergeRequests?.nodes ?? []);
422
+ const nodes = targets.flatMap((_, index) => data[`p${index}`]?.mergeRequests?.nodes ?? []);
418
423
  return nodes.filter((node) => node.sourceProject?.fullPath.toLowerCase() === sourcePath).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))[0]?.iid;
419
424
  }
420
425
  async sourceBranch(ref) {
@@ -422,7 +427,7 @@ class GitLabForge {
422
427
  project: ref.project,
423
428
  iid: ref.iid
424
429
  });
425
- const branch = data?.project?.mergeRequest?.sourceBranch;
430
+ const branch = data.project?.mergeRequest?.sourceBranch;
426
431
  return typeof branch === "string" && branch ? branch : undefined;
427
432
  }
428
433
  async findByNumber(projects, iid) {
@@ -433,7 +438,7 @@ class GitLabForge {
433
438
  projects.forEach((project, index) => variables[`p${index}`] = project.path);
434
439
  const data = await this.graphql(host, mergeRequestQuery(projects.length), variables);
435
440
  for (let index = 0;index < projects.length; index++) {
436
- const node = data?.[`p${index}`]?.mergeRequest ?? undefined;
441
+ const node = data[`p${index}`]?.mergeRequest;
437
442
  if (node)
438
443
  return this.normalize(host, node);
439
444
  }
@@ -449,7 +454,7 @@ class GitLabForge {
449
454
  iid: node.iid,
450
455
  after: page.endCursor
451
456
  });
452
- const next = data?.project?.mergeRequest?.discussions;
457
+ const next = data.project?.mergeRequest?.discussions;
453
458
  if (!next)
454
459
  break;
455
460
  count += countUnresolved(next);
@@ -505,8 +510,8 @@ var fields = `number title url state isDraft mergeable reviewDecision headRefOid
505
510
  } } } } }`;
506
511
  var MAX_PAGES = 50;
507
512
  function ghGraphQL(run, cwd) {
508
- return async (host, query, variables) => {
509
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
513
+ return async (host, query2, variables) => {
514
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
510
515
  for (const [name, value] of Object.entries(variables))
511
516
  args.push(typeof value === "number" ? "-F" : "-f", `${name}=${value}`);
512
517
  const result = await run("gh", args, cwd);
@@ -596,17 +601,17 @@ var githubTraits = {
596
601
  };
597
602
 
598
603
  class GitHubForge {
599
- graphql;
600
604
  traits = githubTraits;
605
+ graphql;
601
606
  constructor(graphql) {
602
- this.graphql = graphql;
607
+ this.graphql = typed(graphql);
603
608
  }
604
609
  async findByNumber(projects, number) {
605
610
  for (const project of projects) {
606
611
  const [owner, repo] = project.path.split("/");
607
- const data = await this.graphql(project.host, `query($owner:String!,$repo:String!,$number:Int!) {
612
+ const data = await this.graphql(project.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
608
613
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { ${fields} } }
609
- }`, { owner, repo, number: Number(number) });
614
+ }`), { owner, repo, number: Number(number) });
610
615
  const node = data.repository?.pullRequest;
611
616
  if (node)
612
617
  return this.normalize(project.host, node);
@@ -617,10 +622,10 @@ class GitHubForge {
617
622
  let newest;
618
623
  for (const target of repository.targets) {
619
624
  const [owner, repo] = target.path.split("/");
620
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!) {
625
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!) {
621
626
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch,orderBy:{field:UPDATED_AT,direction:DESC}) {
622
627
  nodes { number updatedAt headRepository { nameWithOwner } }
623
- } } }`, { owner, repo, branch: repository.sourceBranch });
628
+ } } }`), { owner, repo, branch: repository.sourceBranch });
624
629
  const nodes = data.repository?.pullRequests?.nodes ?? [];
625
630
  for (const node of nodes) {
626
631
  if (node.headRepository?.nameWithOwner.toLowerCase() !== repository.source.path.toLowerCase())
@@ -633,11 +638,10 @@ class GitHubForge {
633
638
  }
634
639
  async sourceBranch(ref) {
635
640
  const [owner, repo] = ref.project.split("/");
636
- const data = await this.graphql(ref.host, `query($owner:String!,$repo:String!,$number:Int!) {
641
+ const data = await this.graphql(ref.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
637
642
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { headRefName } }
638
- }`, { owner, repo, number: Number(ref.iid) });
639
- const branch = data.repository?.pullRequest?.headRefName;
640
- return typeof branch === "string" && branch ? branch : undefined;
643
+ }`), { owner, repo, number: Number(ref.iid) });
644
+ return data.repository?.pullRequest?.headRefName || undefined;
641
645
  }
642
646
  async findByBranch(repository) {
643
647
  const found = new Map;
@@ -646,10 +650,10 @@ class GitHubForge {
646
650
  let after;
647
651
  let complete = false;
648
652
  for (let page = 0;page < MAX_PAGES; page++) {
649
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
653
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
650
654
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch${after ? ",after:$after" : ""}) {
651
655
  nodes { ${fields} } pageInfo { hasNextPage endCursor }
652
- } } }`, { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
656
+ } } }`), { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
653
657
  const pulls = data.repository?.pullRequests;
654
658
  if (!pulls)
655
659
  throw new ForgeError("request", "GitHub repository unavailable");
@@ -677,10 +681,10 @@ class GitHubForge {
677
681
  const threads = [...node.reviewThreads.nodes];
678
682
  let page = node.reviewThreads.pageInfo;
679
683
  for (let count = 1;page?.hasNextPage && page.endCursor && count < MAX_PAGES; count++) {
680
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
684
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
681
685
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
682
686
  reviewThreads(first:100,after:$after) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
683
- } } }`, { owner, repo, number: node.number, after: page.endCursor });
687
+ } } }`), { owner, repo, number: node.number, after: page.endCursor });
684
688
  const next = data.repository?.pullRequest?.reviewThreads;
685
689
  if (!next)
686
690
  break;
@@ -698,10 +702,10 @@ class GitHubForge {
698
702
  const contexts = [...rollup?.nodes ?? []];
699
703
  let cursor = rollup?.pageInfo;
700
704
  for (let count = 1;cursor?.hasNextPage && cursor.endCursor && count < MAX_PAGES; count++) {
701
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
705
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
702
706
  repository(owner:$owner,name:$repo) { object(oid:$oid) { ... on Commit { statusCheckRollup {
703
707
  contexts(first:100,after:$after) { nodes { __typename ... on CheckRun { status conclusion detailsUrl } ... on StatusContext { state targetUrl } } pageInfo { hasNextPage endCursor } }
704
- } } } } }`, { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
708
+ } } } } }`), { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
705
709
  const next = data.repository?.object?.statusCheckRollup?.contexts;
706
710
  if (!next)
707
711
  break;
@@ -910,14 +914,15 @@ var target = {
910
914
  additionalProperties: false
911
915
  };
912
916
  var key = { type: "string" };
913
- var snapshot = { type: "object" };
917
+ var snapshot = { type: "object", title: "Snapshot" };
914
918
  var status = {
915
919
  type: "object",
916
920
  properties: { enabled: { type: "boolean" }, snapshot },
917
- required: ["enabled", "snapshot"]
921
+ required: ["enabled", "snapshot"],
922
+ additionalProperties: false
918
923
  };
919
924
  var ForgeSessionRpc = {
920
- id: "opencode-forge-session-title",
925
+ id: "opencode-forgekeeper",
921
926
  methods: {
922
927
  target: {
923
928
  input: {
@@ -941,8 +946,12 @@ var ForgeSessionRpc = {
941
946
  },
942
947
  output: {
943
948
  type: "object",
944
- properties: { enabled: { type: "boolean" }, statuses: { type: "object" } },
945
- required: ["enabled", "statuses"]
949
+ properties: {
950
+ enabled: { type: "boolean" },
951
+ statuses: { type: "object", additionalProperties: snapshot }
952
+ },
953
+ required: ["enabled", "statuses"],
954
+ additionalProperties: false
946
955
  }
947
956
  },
948
957
  release: {
@@ -952,7 +961,7 @@ var ForgeSessionRpc = {
952
961
  required: ["clientID"],
953
962
  additionalProperties: false
954
963
  },
955
- output: { type: "object" }
964
+ output: { type: "object", additionalProperties: false }
956
965
  },
957
966
  refresh: {
958
967
  input: {
@@ -977,7 +986,8 @@ var ForgeSessionRpc = {
977
986
  schema: {
978
987
  type: "object",
979
988
  properties: { directory: { type: "string" }, key, snapshot },
980
- required: ["directory", "key", "snapshot"]
989
+ required: ["directory", "key", "snapshot"],
990
+ additionalProperties: false
981
991
  }
982
992
  },
983
993
  notice: {
@@ -989,7 +999,8 @@ var ForgeSessionRpc = {
989
999
  message: { type: "string" },
990
1000
  variant: { type: "string", enum: ["info", "error"] }
991
1001
  },
992
- required: ["message", "variant"]
1002
+ required: ["message", "variant"],
1003
+ additionalProperties: false
993
1004
  }
994
1005
  }
995
1006
  }
@@ -997,6 +1008,12 @@ var ForgeSessionRpc = {
997
1008
  var HOME = "";
998
1009
  var HEARTBEAT = 60000;
999
1010
  var LEASE_TIME = 3 * HEARTBEAT;
1011
+ var implement = (handlers) => ({
1012
+ target: (input) => handlers.target(input),
1013
+ watch: (input) => handlers.watch(input),
1014
+ release: (input) => handlers.release(input),
1015
+ refresh: (input) => handlers.refresh(input)
1016
+ });
1000
1017
 
1001
1018
  // packages/forgekeeper/src/automated-review-watch.ts
1002
1019
  var requests = (lookup) => lookup?.kind === "found" ? lookup.requests : [];
@@ -1805,7 +1822,6 @@ var FEEDBACK_PREFIX = "status/feedback/";
1805
1822
  var pendingPrefix = (sessionID) => `${PENDING_PREFIX}${encodeURIComponent(sessionID)}/`;
1806
1823
  var pendingKey = (sessionID, url) => `${pendingPrefix(sessionID)}${encodeURIComponent(url)}`;
1807
1824
  var feedbackRecordKey = (key2) => `${FEEDBACK_PREFIX}${encodeURIComponent(key2)}`;
1808
- var LEGACY_KEYS = ["status/humanReviewFeedback", "status/automatedReviewPending"];
1809
1825
  function userTitle(title, prefix) {
1810
1826
  if (!title || !prefix)
1811
1827
  return title;
@@ -1971,8 +1987,6 @@ async function setupStatus(ctx, settings, deps, host) {
1971
1987
  });
1972
1988
  invalidate = (sessionID) => service.invalidate(sessionID);
1973
1989
  write(async () => {
1974
- for (const key2 of LEGACY_KEYS)
1975
- await ctx.storage.remove(key2);
1976
1990
  const at = (deps.now ?? Date.now)();
1977
1991
  for (const { key: key2, value } of await scanAll(FEEDBACK_PREFIX)) {
1978
1992
  if (isExpired(value, at))
@@ -2095,7 +2109,7 @@ async function setupServer(ctx, lookups, status2) {
2095
2109
  async function stateFor(sessionID) {
2096
2110
  const started = revision(sessionID);
2097
2111
  const stored = await ctx.storage.get(`sessions/${sessionID}`) ?? {};
2098
- const targets = Array.isArray(stored.targets) ? stored.targets : stored.target ? [stored.target] : [];
2112
+ const targets = Array.isArray(stored.targets) ? stored.targets : [];
2099
2113
  if (revision(sessionID) === started)
2100
2114
  known.set(sessionID, targets);
2101
2115
  return { targets, ...stored.prefix ? { prefix: stored.prefix } : {} };
@@ -2115,24 +2129,15 @@ async function setupServer(ctx, lookups, status2) {
2115
2129
  }
2116
2130
  };
2117
2131
  let watcher;
2118
- const rpc = await ctx.rpc.register(ForgeSessionRpc, {
2119
- target: async (input) => {
2120
- const { sessionID } = input;
2121
- return targetOutput((await stateFor(sessionID)).targets);
2122
- },
2123
- watch: async (input) => {
2124
- const { clientID, keys, visible } = input;
2125
- return watcher ? { enabled: true, statuses: json(watcher.service.watch(clientID, keys, visible)) } : { enabled: false, statuses: {} };
2126
- },
2127
- release: async (input) => {
2128
- watcher?.service.release(input.clientID);
2132
+ const rpc = await ctx.rpc.register(ForgeSessionRpc, implement({
2133
+ target: async ({ sessionID }) => targetOutput((await stateFor(sessionID)).targets),
2134
+ watch: async ({ clientID, keys, visible }) => watcher ? { enabled: true, statuses: json(watcher.service.watch(clientID, keys, visible)) } : { enabled: false, statuses: {} },
2135
+ release: async ({ clientID }) => {
2136
+ watcher?.service.release(clientID);
2129
2137
  return {};
2130
2138
  },
2131
- refresh: async (input) => {
2132
- const { key: key2 } = input;
2133
- return watcher ? { enabled: true, snapshot: json(await watcher.service.refresh(key2)) } : DISABLED;
2134
- }
2135
- });
2139
+ refresh: async ({ key: key2 }) => watcher ? { enabled: true, snapshot: json(await watcher.service.refresh(key2)) } : DISABLED
2140
+ }));
2136
2141
  const settings = statusOptions(ctx.options);
2137
2142
  if (status2 && settings.enabled) {
2138
2143
  watcher = await setupStatus(ctx, settings, status2, {
@@ -2299,7 +2304,7 @@ async function setupServer(ctx, lookups, status2) {
2299
2304
 
2300
2305
  // packages/forgekeeper/src/index.ts
2301
2306
  var src_default = {
2302
- id: "opencode-forge-session-title",
2307
+ id: "opencode-forgekeeper",
2303
2308
  async setup(ctx) {
2304
2309
  const hosts = hostsFrom(ctx.options ?? {});
2305
2310
  const stopped = new AbortController;
package/dist/tui.js CHANGED
@@ -69,10 +69,14 @@ class ForgeError extends Error {
69
69
  }
70
70
  }
71
71
 
72
+ // packages/forgekeeper/src/graphql.ts
73
+ var query = (text) => text;
74
+ var typed = (transport) => async (host, query2, variables) => await transport(host, query2, variables);
75
+
72
76
  // packages/forgekeeper/src/gitlab-feedback.ts
73
77
  var PAGE_SIZE = 100;
74
78
  var MAX_COMMENT_PAGES = 20;
75
- var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
79
+ var COMMENTS_QUERY = query(`query($project: ID!, $iid: String!, $before: String) {
76
80
  currentUser { id }
77
81
  project(fullPath: $project) {
78
82
  mergeRequest(iid: $iid) {
@@ -86,7 +90,7 @@ var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
86
90
  }
87
91
  }
88
92
  }
89
- }`;
93
+ }`);
90
94
  var noteNumber = (id) => id.slice(id.lastIndexOf("/") + 1);
91
95
  var timestamp = (node) => Date.parse(node.lastEditedAt ?? node.updatedAt ?? node.createdAt);
92
96
  function toComment(node) {
@@ -105,7 +109,8 @@ function toComment(node) {
105
109
  username: node.author.username
106
110
  };
107
111
  }
108
- async function fetchComments(graphql, host, project, iid) {
112
+ async function fetchComments(transport, host, project, iid) {
113
+ const graphql = typed(transport);
109
114
  const pages = [];
110
115
  let viewerID;
111
116
  let before;
@@ -119,8 +124,8 @@ async function fetchComments(graphql, host, project, iid) {
119
124
  if (before)
120
125
  variables.before = before;
121
126
  const data = await graphql(host, COMMENTS_QUERY, variables);
122
- viewerID ??= data?.currentUser?.id ?? undefined;
123
- const connection = data?.project?.mergeRequest?.notes;
127
+ viewerID ??= data.currentUser?.id ?? undefined;
128
+ const connection = data.project?.mergeRequest?.notes;
124
129
  if (!connection)
125
130
  return page === 0 ? undefined : snapshot(false);
126
131
  pages.push(connection.nodes.flatMap((node) => toComment(node) ?? []));
@@ -152,17 +157,17 @@ function mergeRequestsQuery(projectCount) {
152
157
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { ${MERGE_REQUEST_FIELDS} } }
153
158
  }`).join(`
154
159
  `);
155
- return `query($branch: String!, ${variables}) {
160
+ return query(`query($branch: String!, ${variables}) {
156
161
  ${projects}
157
- }`;
162
+ }`);
158
163
  }
159
164
  function mergeRequestQuery(projectCount) {
160
165
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
161
166
  const projects = Array.from({ length: projectCount }, (_, index) => `p${index}: project(fullPath: $p${index}) { mergeRequest(iid: $iid) { ${MERGE_REQUEST_FIELDS} } }`).join(`
162
167
  `);
163
- return `query($iid: String!, ${variables}) {
168
+ return query(`query($iid: String!, ${variables}) {
164
169
  ${projects}
165
- }`;
170
+ }`);
166
171
  }
167
172
  function mergeRequestNumbersQuery(projectCount) {
168
173
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
@@ -170,18 +175,18 @@ function mergeRequestNumbersQuery(projectCount) {
170
175
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { iid updatedAt sourceProject { fullPath } } }
171
176
  }`).join(`
172
177
  `);
173
- return `query($branch: String!, ${variables}) {
178
+ return query(`query($branch: String!, ${variables}) {
174
179
  ${projects}
175
- }`;
180
+ }`);
176
181
  }
177
- var SOURCE_BRANCH_QUERY = `query($project: ID!, $iid: String!) {
182
+ var SOURCE_BRANCH_QUERY = query(`query($project: ID!, $iid: String!) {
178
183
  project(fullPath: $project) { mergeRequest(iid: $iid) { sourceBranch } }
179
- }`;
180
- var DISCUSSIONS_QUERY = `query($project: ID!, $iid: String!, $after: String) {
184
+ }`);
185
+ var DISCUSSIONS_QUERY = query(`query($project: ID!, $iid: String!, $after: String) {
181
186
  project(fullPath: $project) {
182
187
  mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE2}, after: $after) { ${DISCUSSIONS} } }
183
188
  }
184
- }`;
189
+ }`);
185
190
  function classifyError(message, code) {
186
191
  const text = message.trim().replace(/^glab:\s*/, "") || `glab exited with code ${code}`;
187
192
  if (/ENOENT|command not found/i.test(text))
@@ -196,8 +201,8 @@ function classifyError(message, code) {
196
201
  }
197
202
  var isResponse = (body) => typeof body === "object" && body !== null && (!("errors" in body) || Array.isArray(body.errors)) && (!("data" in body) || typeof body.data === "object");
198
203
  function glabGraphQL(run, cwd) {
199
- return async (host, query, variables) => {
200
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
204
+ return async (host, query2, variables) => {
205
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
201
206
  for (const [name, value] of Object.entries(variables))
202
207
  args.push("-f", `${name}=${value}`);
203
208
  const result = await run("glab", args, cwd);
@@ -265,10 +270,10 @@ function duoState(state) {
265
270
  }
266
271
 
267
272
  class GitLabForge {
268
- graphql;
269
273
  traits = gitlabTraits;
274
+ graphql;
270
275
  constructor(graphql) {
271
- this.graphql = graphql;
276
+ this.graphql = typed(graphql);
272
277
  }
273
278
  async findByBranch(repository) {
274
279
  const { source, targets, sourceBranch } = repository;
@@ -279,7 +284,7 @@ class GitLabForge {
279
284
  const seen = new Set;
280
285
  const nodes = [];
281
286
  targets.forEach((_, index) => {
282
- for (const node of data?.[`p${index}`]?.mergeRequests?.nodes ?? []) {
287
+ for (const node of data[`p${index}`]?.mergeRequests?.nodes ?? []) {
283
288
  if (node.sourceProject?.fullPath.toLowerCase() !== sourcePath)
284
289
  continue;
285
290
  const key = `${node.targetProject.fullPath}!${node.iid}`;
@@ -298,7 +303,7 @@ class GitLabForge {
298
303
  targets.forEach((target, index) => variables[`p${index}`] = target.path);
299
304
  const data = await this.graphql(source.host, mergeRequestNumbersQuery(targets.length), variables);
300
305
  const sourcePath = source.path.toLowerCase();
301
- const nodes = targets.flatMap((_, index) => data?.[`p${index}`]?.mergeRequests?.nodes ?? []);
306
+ const nodes = targets.flatMap((_, index) => data[`p${index}`]?.mergeRequests?.nodes ?? []);
302
307
  return nodes.filter((node) => node.sourceProject?.fullPath.toLowerCase() === sourcePath).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))[0]?.iid;
303
308
  }
304
309
  async sourceBranch(ref) {
@@ -306,7 +311,7 @@ class GitLabForge {
306
311
  project: ref.project,
307
312
  iid: ref.iid
308
313
  });
309
- const branch = data?.project?.mergeRequest?.sourceBranch;
314
+ const branch = data.project?.mergeRequest?.sourceBranch;
310
315
  return typeof branch === "string" && branch ? branch : undefined;
311
316
  }
312
317
  async findByNumber(projects, iid) {
@@ -317,7 +322,7 @@ class GitLabForge {
317
322
  projects.forEach((project, index) => variables[`p${index}`] = project.path);
318
323
  const data = await this.graphql(host, mergeRequestQuery(projects.length), variables);
319
324
  for (let index = 0;index < projects.length; index++) {
320
- const node = data?.[`p${index}`]?.mergeRequest ?? undefined;
325
+ const node = data[`p${index}`]?.mergeRequest;
321
326
  if (node)
322
327
  return this.normalize(host, node);
323
328
  }
@@ -333,7 +338,7 @@ class GitLabForge {
333
338
  iid: node.iid,
334
339
  after: page.endCursor
335
340
  });
336
- const next = data?.project?.mergeRequest?.discussions;
341
+ const next = data.project?.mergeRequest?.discussions;
337
342
  if (!next)
338
343
  break;
339
344
  count += countUnresolved(next);
@@ -389,8 +394,8 @@ var fields = `number title url state isDraft mergeable reviewDecision headRefOid
389
394
  } } } } }`;
390
395
  var MAX_PAGES = 50;
391
396
  function ghGraphQL(run, cwd) {
392
- return async (host, query, variables) => {
393
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
397
+ return async (host, query2, variables) => {
398
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
394
399
  for (const [name, value] of Object.entries(variables))
395
400
  args.push(typeof value === "number" ? "-F" : "-f", `${name}=${value}`);
396
401
  const result = await run("gh", args, cwd);
@@ -480,17 +485,17 @@ var githubTraits = {
480
485
  };
481
486
 
482
487
  class GitHubForge {
483
- graphql;
484
488
  traits = githubTraits;
489
+ graphql;
485
490
  constructor(graphql) {
486
- this.graphql = graphql;
491
+ this.graphql = typed(graphql);
487
492
  }
488
493
  async findByNumber(projects, number) {
489
494
  for (const project of projects) {
490
495
  const [owner, repo] = project.path.split("/");
491
- const data = await this.graphql(project.host, `query($owner:String!,$repo:String!,$number:Int!) {
496
+ const data = await this.graphql(project.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
492
497
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { ${fields} } }
493
- }`, { owner, repo, number: Number(number) });
498
+ }`), { owner, repo, number: Number(number) });
494
499
  const node = data.repository?.pullRequest;
495
500
  if (node)
496
501
  return this.normalize(project.host, node);
@@ -501,10 +506,10 @@ class GitHubForge {
501
506
  let newest;
502
507
  for (const target of repository.targets) {
503
508
  const [owner, repo] = target.path.split("/");
504
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!) {
509
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!) {
505
510
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch,orderBy:{field:UPDATED_AT,direction:DESC}) {
506
511
  nodes { number updatedAt headRepository { nameWithOwner } }
507
- } } }`, { owner, repo, branch: repository.sourceBranch });
512
+ } } }`), { owner, repo, branch: repository.sourceBranch });
508
513
  const nodes = data.repository?.pullRequests?.nodes ?? [];
509
514
  for (const node of nodes) {
510
515
  if (node.headRepository?.nameWithOwner.toLowerCase() !== repository.source.path.toLowerCase())
@@ -517,11 +522,10 @@ class GitHubForge {
517
522
  }
518
523
  async sourceBranch(ref) {
519
524
  const [owner, repo] = ref.project.split("/");
520
- const data = await this.graphql(ref.host, `query($owner:String!,$repo:String!,$number:Int!) {
525
+ const data = await this.graphql(ref.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
521
526
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { headRefName } }
522
- }`, { owner, repo, number: Number(ref.iid) });
523
- const branch = data.repository?.pullRequest?.headRefName;
524
- return typeof branch === "string" && branch ? branch : undefined;
527
+ }`), { owner, repo, number: Number(ref.iid) });
528
+ return data.repository?.pullRequest?.headRefName || undefined;
525
529
  }
526
530
  async findByBranch(repository) {
527
531
  const found = new Map;
@@ -530,10 +534,10 @@ class GitHubForge {
530
534
  let after;
531
535
  let complete = false;
532
536
  for (let page = 0;page < MAX_PAGES; page++) {
533
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
537
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
534
538
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch${after ? ",after:$after" : ""}) {
535
539
  nodes { ${fields} } pageInfo { hasNextPage endCursor }
536
- } } }`, { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
540
+ } } }`), { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
537
541
  const pulls = data.repository?.pullRequests;
538
542
  if (!pulls)
539
543
  throw new ForgeError("request", "GitHub repository unavailable");
@@ -561,10 +565,10 @@ class GitHubForge {
561
565
  const threads = [...node.reviewThreads.nodes];
562
566
  let page = node.reviewThreads.pageInfo;
563
567
  for (let count = 1;page?.hasNextPage && page.endCursor && count < MAX_PAGES; count++) {
564
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
568
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
565
569
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
566
570
  reviewThreads(first:100,after:$after) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
567
- } } }`, { owner, repo, number: node.number, after: page.endCursor });
571
+ } } }`), { owner, repo, number: node.number, after: page.endCursor });
568
572
  const next = data.repository?.pullRequest?.reviewThreads;
569
573
  if (!next)
570
574
  break;
@@ -582,10 +586,10 @@ class GitHubForge {
582
586
  const contexts = [...rollup?.nodes ?? []];
583
587
  let cursor = rollup?.pageInfo;
584
588
  for (let count = 1;cursor?.hasNextPage && cursor.endCursor && count < MAX_PAGES; count++) {
585
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
589
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
586
590
  repository(owner:$owner,name:$repo) { object(oid:$oid) { ... on Commit { statusCheckRollup {
587
591
  contexts(first:100,after:$after) { nodes { __typename ... on CheckRun { status conclusion detailsUrl } ... on StatusContext { state targetUrl } } pageInfo { hasNextPage endCursor } }
588
- } } } } }`, { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
592
+ } } } } }`), { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
589
593
  const next = data.repository?.object?.statusCheckRollup?.contexts;
590
594
  if (!next)
591
595
  break;
@@ -876,14 +880,15 @@ var target = {
876
880
  additionalProperties: false
877
881
  };
878
882
  var key = { type: "string" };
879
- var snapshot = { type: "object" };
883
+ var snapshot = { type: "object", title: "Snapshot" };
880
884
  var status = {
881
885
  type: "object",
882
886
  properties: { enabled: { type: "boolean" }, snapshot },
883
- required: ["enabled", "snapshot"]
887
+ required: ["enabled", "snapshot"],
888
+ additionalProperties: false
884
889
  };
885
890
  var ForgeSessionRpc = {
886
- id: "opencode-forge-session-title",
891
+ id: "opencode-forgekeeper",
887
892
  methods: {
888
893
  target: {
889
894
  input: {
@@ -907,8 +912,12 @@ var ForgeSessionRpc = {
907
912
  },
908
913
  output: {
909
914
  type: "object",
910
- properties: { enabled: { type: "boolean" }, statuses: { type: "object" } },
911
- required: ["enabled", "statuses"]
915
+ properties: {
916
+ enabled: { type: "boolean" },
917
+ statuses: { type: "object", additionalProperties: snapshot }
918
+ },
919
+ required: ["enabled", "statuses"],
920
+ additionalProperties: false
912
921
  }
913
922
  },
914
923
  release: {
@@ -918,7 +927,7 @@ var ForgeSessionRpc = {
918
927
  required: ["clientID"],
919
928
  additionalProperties: false
920
929
  },
921
- output: { type: "object" }
930
+ output: { type: "object", additionalProperties: false }
922
931
  },
923
932
  refresh: {
924
933
  input: {
@@ -943,7 +952,8 @@ var ForgeSessionRpc = {
943
952
  schema: {
944
953
  type: "object",
945
954
  properties: { directory: { type: "string" }, key, snapshot },
946
- required: ["directory", "key", "snapshot"]
955
+ required: ["directory", "key", "snapshot"],
956
+ additionalProperties: false
947
957
  }
948
958
  },
949
959
  notice: {
@@ -955,7 +965,8 @@ var ForgeSessionRpc = {
955
965
  message: { type: "string" },
956
966
  variant: { type: "string", enum: ["info", "error"] }
957
967
  },
958
- required: ["message", "variant"]
968
+ required: ["message", "variant"],
969
+ additionalProperties: false
959
970
  }
960
971
  }
961
972
  }
@@ -1272,6 +1283,337 @@ var tui_default = {
1272
1283
  });
1273
1284
  return true;
1274
1285
  };
1286
+ function DetailField(props) {
1287
+ const foreground = () => props.tone ? color(props.tone) : context.theme.text.base;
1288
+ return (() => {
1289
+ var _el$8 = _$createElement("text"), _el$9 = _$createTextNode(`: `), _el$0 = _$createElement("span");
1290
+ _$insertNode(_el$8, _el$9);
1291
+ _$insertNode(_el$8, _el$0);
1292
+ _$setProp(_el$8, "onMouseUp", () => props.url && openUrl(props.url));
1293
+ _$insert(_el$8, () => props.label, _el$9);
1294
+ _$insert(_el$0, _$createComponent(Show, {
1295
+ get when() {
1296
+ return props.url;
1297
+ },
1298
+ get fallback() {
1299
+ return (() => {
1300
+ var _el$1 = _$createElement("b");
1301
+ _$insert(_el$1, () => props.value);
1302
+ return _el$1;
1303
+ })();
1304
+ },
1305
+ children: (url) => (() => {
1306
+ var _el$10 = _$createElement("a"), _el$11 = _$createElement("u"), _el$12 = _$createElement("b");
1307
+ _$insertNode(_el$10, _el$11);
1308
+ _$insertNode(_el$11, _el$12);
1309
+ _$insert(_el$12, () => props.value);
1310
+ _$effect((_$p) => _$setProp(_el$10, "href", url(), _$p));
1311
+ return _el$10;
1312
+ })()
1313
+ }));
1314
+ _$effect((_p$) => {
1315
+ var _v$3 = context.theme.text.muted, _v$4 = {
1316
+ fg: foreground()
1317
+ };
1318
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$8, "fg", _v$3, _p$.e));
1319
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$0, "style", _v$4, _p$.t));
1320
+ return _p$;
1321
+ }, {
1322
+ e: undefined,
1323
+ t: undefined
1324
+ });
1325
+ return _el$8;
1326
+ })();
1327
+ }
1328
+ function RequestDetails(props) {
1329
+ const request = () => props.request;
1330
+ const traits = () => traitsOf(request());
1331
+ const review = () => automatedReview(request());
1332
+ return (() => {
1333
+ var _el$13 = _$createElement("box"), _el$14 = _$createElement("text"), _el$15 = _$createElement("a"), _el$16 = _$createElement("u"), _el$17 = _$createElement("b"), _el$18 = _$createTextNode(` `), _el$19 = _$createElement("box"), _el$20 = _$createElement("box"), _el$21 = _$createElement("text"), _el$22 = _$createElement("b");
1334
+ _$insertNode(_el$13, _el$14);
1335
+ _$insertNode(_el$13, _el$19);
1336
+ _$insertNode(_el$13, _el$20);
1337
+ _$setProp(_el$13, "gap", 1);
1338
+ _$setProp(_el$13, "flexShrink", 0);
1339
+ _$insertNode(_el$14, _el$15);
1340
+ _$setProp(_el$14, "onMouseUp", () => openUrl(request().url));
1341
+ _$insertNode(_el$15, _el$16);
1342
+ _$insertNode(_el$16, _el$17);
1343
+ _$insertNode(_el$17, _el$18);
1344
+ _$insert(_el$17, () => reference(request()), _el$18);
1345
+ _$insert(_el$17, () => request().title, null);
1346
+ _$setProp(_el$19, "flexShrink", 0);
1347
+ _$insert(_el$19, _$createComponent(DetailField, {
1348
+ label: "State",
1349
+ get value() {
1350
+ return request().state;
1351
+ },
1352
+ get tone() {
1353
+ return request().state === "merged" ? "success" : undefined;
1354
+ }
1355
+ }), null);
1356
+ _$insert(_el$19, _$createComponent(DetailField, {
1357
+ label: "Target",
1358
+ get value() {
1359
+ return `${request().targetProject} \u2192 ${request().targetBranch}`;
1360
+ }
1361
+ }), null);
1362
+ _$insert(_el$19, _$createComponent(DetailField, {
1363
+ get label() {
1364
+ return traits().ci.charAt(0).toUpperCase() + traits().ci.slice(1);
1365
+ },
1366
+ get value() {
1367
+ return request().pipeline?.label ?? "none";
1368
+ },
1369
+ get tone() {
1370
+ return pipelineTone(request().pipeline);
1371
+ },
1372
+ get url() {
1373
+ return request().pipeline?.url;
1374
+ }
1375
+ }), null);
1376
+ _$insert(_el$19, _$createComponent(DetailField, {
1377
+ label: "Unresolved threads",
1378
+ get value() {
1379
+ return `${request().unresolvedThreads}${request().threadsComplete ? "" : "+"}`;
1380
+ },
1381
+ get tone() {
1382
+ return request().unresolvedThreads || !request().threadsComplete ? "warning" : undefined;
1383
+ }
1384
+ }), null);
1385
+ _$insert(_el$19, _$createComponent(DetailField, {
1386
+ label: "Draft",
1387
+ get value() {
1388
+ return yesNo(request().draft);
1389
+ },
1390
+ get tone() {
1391
+ return request().draft ? "warning" : undefined;
1392
+ }
1393
+ }), null);
1394
+ _$insert(_el$19, _$createComponent(DetailField, {
1395
+ label: "Conflicts",
1396
+ get value() {
1397
+ return _$memo(() => !!request().conflictsKnown)() ? yesNo(request().conflicts) : "unknown";
1398
+ },
1399
+ get tone() {
1400
+ return request().conflicts ? "error" : undefined;
1401
+ }
1402
+ }), null);
1403
+ _$insertNode(_el$20, _el$21);
1404
+ _$setProp(_el$20, "flexShrink", 0);
1405
+ _$insertNode(_el$21, _el$22);
1406
+ _$insertNode(_el$22, _$createTextNode(`Review and approval`));
1407
+ _$insert(_el$20, _$createComponent(For, {
1408
+ get each() {
1409
+ return traits().details(request());
1410
+ },
1411
+ children: (line) => {
1412
+ const separator = line.indexOf(": ");
1413
+ return separator < 0 ? (() => {
1414
+ var _el$29 = _$createElement("text");
1415
+ _$insert(_el$29, line);
1416
+ _$effect((_$p) => _$setProp(_el$29, "fg", context.theme.text.base, _$p));
1417
+ return _el$29;
1418
+ })() : _$createComponent(DetailField, {
1419
+ get label() {
1420
+ return line.slice(0, separator);
1421
+ },
1422
+ get value() {
1423
+ return line.slice(separator + 2);
1424
+ }
1425
+ });
1426
+ }
1427
+ }), null);
1428
+ _$insert(_el$20, _$createComponent(Show, {
1429
+ get when() {
1430
+ return review();
1431
+ },
1432
+ children: (value) => _$createComponent(DetailField, {
1433
+ get label() {
1434
+ return `${value().name} review`;
1435
+ },
1436
+ get value() {
1437
+ return value().label;
1438
+ },
1439
+ get tone() {
1440
+ return value().state === "running" ? "warning" : undefined;
1441
+ }
1442
+ })
1443
+ }), null);
1444
+ _$insert(_el$20, _$createComponent(Show, {
1445
+ get when() {
1446
+ return request().awaitingReviewers.length;
1447
+ },
1448
+ get children() {
1449
+ return _$createComponent(DetailField, {
1450
+ label: "Awaiting review",
1451
+ get value() {
1452
+ return request().awaitingReviewers.map((username) => `@${username}`).join(", ");
1453
+ }
1454
+ });
1455
+ }
1456
+ }), null);
1457
+ _$insert(_el$13, _$createComponent(Show, {
1458
+ get when() {
1459
+ return !request().reviewersComplete;
1460
+ },
1461
+ get children() {
1462
+ var _el$24 = _$createElement("text");
1463
+ _$insertNode(_el$24, _$createTextNode(`Reviewer list is partial`));
1464
+ _$effect((_$p) => _$setProp(_el$24, "fg", color("warning"), _$p));
1465
+ return _el$24;
1466
+ }
1467
+ }), null);
1468
+ _$insert(_el$13, _$createComponent(Show, {
1469
+ get when() {
1470
+ return _$memo(() => !!(props.head && request().headSha))() && props.head !== request().headSha;
1471
+ },
1472
+ get children() {
1473
+ var _el$26 = _$createElement("text"), _el$27 = _$createTextNode(`Local HEAD differs from the `), _el$28 = _$createTextNode(` head commit (unpushed or not fetched)`);
1474
+ _$insertNode(_el$26, _el$27);
1475
+ _$insertNode(_el$26, _el$28);
1476
+ _$insert(_el$26, () => traits().noun, _el$28);
1477
+ _$effect((_$p) => _$setProp(_el$26, "fg", color("warning"), _$p));
1478
+ return _el$26;
1479
+ }
1480
+ }), null);
1481
+ _$effect((_p$) => {
1482
+ var _v$5 = context.theme.text.base, _v$6 = request().url, _v$7 = context.theme.text.base;
1483
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$14, "fg", _v$5, _p$.e));
1484
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$15, "href", _v$6, _p$.t));
1485
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$21, "fg", _v$7, _p$.a));
1486
+ return _p$;
1487
+ }, {
1488
+ e: undefined,
1489
+ t: undefined,
1490
+ a: undefined
1491
+ });
1492
+ return _el$13;
1493
+ })();
1494
+ }
1495
+ function StatusDetails(props) {
1496
+ const lookup = () => {
1497
+ const value = props.snapshot.lookup;
1498
+ return value?.kind === "found" ? value : undefined;
1499
+ };
1500
+ return (() => {
1501
+ var _el$30 = _$createElement("box");
1502
+ _$setProp(_el$30, "gap", 1);
1503
+ _$setProp(_el$30, "flexShrink", 0);
1504
+ _$insert(_el$30, _$createComponent(Show, {
1505
+ get when() {
1506
+ return lookup();
1507
+ },
1508
+ get fallback() {
1509
+ return (() => {
1510
+ var _el$31 = _$createElement("text");
1511
+ _$insert(_el$31, () => detailsMessage(props.snapshot));
1512
+ _$effect((_$p) => _$setProp(_el$31, "fg", props.snapshot.error ? color("error") : context.theme.text.muted, _$p));
1513
+ return _el$31;
1514
+ })();
1515
+ },
1516
+ children: (found) => [_$createComponent(Show, {
1517
+ get when() {
1518
+ return found().repository;
1519
+ },
1520
+ children: (repository) => (() => {
1521
+ var _el$36 = _$createElement("box");
1522
+ _$setProp(_el$36, "flexShrink", 0);
1523
+ _$insert(_el$36, _$createComponent(DetailField, {
1524
+ label: "Repository",
1525
+ get value() {
1526
+ return repository().source.path;
1527
+ }
1528
+ }), null);
1529
+ _$insert(_el$36, _$createComponent(DetailField, {
1530
+ label: "Branch",
1531
+ get value() {
1532
+ return _$memo(() => repository().branch === repository().sourceBranch)() ? repository().branch : `${repository().branch} (pushes to ${repository().sourceBranch})`;
1533
+ }
1534
+ }), null);
1535
+ return _el$36;
1536
+ })()
1537
+ }), _$createComponent(Show, {
1538
+ get when() {
1539
+ return _$memo(() => !!!found().repository)() && found().sessionTarget;
1540
+ },
1541
+ get children() {
1542
+ return _$createComponent(DetailField, {
1543
+ label: "Session target",
1544
+ get value() {
1545
+ return found().sessionTarget;
1546
+ }
1547
+ });
1548
+ }
1549
+ }), _$createComponent(For, {
1550
+ get each() {
1551
+ return found().failed ?? [];
1552
+ },
1553
+ children: (failure) => (() => {
1554
+ var _el$37 = _$createElement("text"), _el$38 = _$createElement("b"), _el$40 = _$createTextNode(`
1555
+ `), _el$41 = _$createTextNode(`
1556
+ `);
1557
+ _$insertNode(_el$37, _el$38);
1558
+ _$insertNode(_el$37, _el$40);
1559
+ _$insertNode(_el$37, _el$41);
1560
+ _$insertNode(_el$38, _$createTextNode(`Unavailable`));
1561
+ _$insert(_el$37, () => failure.url, _el$41);
1562
+ _$insert(_el$37, () => failure.reason, null);
1563
+ _$effect((_$p) => _$setProp(_el$37, "fg", color("warning"), _$p));
1564
+ return _el$37;
1565
+ })()
1566
+ }), _$createComponent(Show, {
1567
+ get when() {
1568
+ return found().requests.length === 0;
1569
+ },
1570
+ get children() {
1571
+ var _el$32 = _$createElement("text");
1572
+ _$insertNode(_el$32, _$createTextNode(`No PR/MR status available.`));
1573
+ _$effect((_$p) => _$setProp(_el$32, "fg", context.theme.text.muted, _$p));
1574
+ return _el$32;
1575
+ }
1576
+ }), _$createComponent(For, {
1577
+ get each() {
1578
+ return found().requests;
1579
+ },
1580
+ children: (request) => _$createComponent(RequestDetails, {
1581
+ request,
1582
+ get head() {
1583
+ return found().repository?.head;
1584
+ }
1585
+ })
1586
+ }), _$createComponent(Show, {
1587
+ get when() {
1588
+ return props.snapshot.error;
1589
+ },
1590
+ children: (error) => (() => {
1591
+ var _el$42 = _$createElement("text"), _el$43 = _$createElement("b"), _el$44 = _$createTextNode(`Last refresh failed at `), _el$45 = _$createTextNode(`
1592
+ `);
1593
+ _$insertNode(_el$42, _el$43);
1594
+ _$insertNode(_el$42, _el$45);
1595
+ _$insertNode(_el$43, _el$44);
1596
+ _$insert(_el$43, () => new Date(error().at).toLocaleTimeString(), null);
1597
+ _$insert(_el$42, () => error().message, null);
1598
+ _$effect((_$p) => _$setProp(_el$42, "fg", color("error"), _$p));
1599
+ return _el$42;
1600
+ })()
1601
+ }), _$createComponent(Show, {
1602
+ get when() {
1603
+ return props.snapshot.fetchedAt !== undefined;
1604
+ },
1605
+ get children() {
1606
+ var _el$34 = _$createElement("text"), _el$35 = _$createTextNode(`Last fetched: `);
1607
+ _$insertNode(_el$34, _el$35);
1608
+ _$insert(_el$34, () => new Date(props.snapshot.fetchedAt).toLocaleTimeString(), null);
1609
+ _$effect((_$p) => _$setProp(_el$34, "fg", context.theme.text.muted, _$p));
1610
+ return _el$34;
1611
+ }
1612
+ })]
1613
+ }));
1614
+ return _el$30;
1615
+ })();
1616
+ }
1275
1617
  function StatusDialog(props) {
1276
1618
  const [terminalHeight, setTerminalHeight] = createSignal(context.renderer.height);
1277
1619
  const onResize = () => setTerminalHeight(context.renderer.height);
@@ -1321,52 +1663,55 @@ var tui_default = {
1321
1663
  }]
1322
1664
  }));
1323
1665
  return (() => {
1324
- var _el$8 = _$createElement("box"), _el$9 = _$createElement("box"), _el$0 = _$createElement("text"), _el$1 = _$createElement("b"), _el$11 = _$createElement("text"), _el$13 = _$createElement("scrollbox"), _el$14 = _$createElement("text"), _el$15 = _$createElement("box");
1325
- _$insertNode(_el$8, _el$9);
1326
- _$insertNode(_el$8, _el$13);
1327
- _$insertNode(_el$8, _el$15);
1328
- _$setProp(_el$8, "paddingLeft", 2);
1329
- _$setProp(_el$8, "paddingRight", 2);
1330
- _$setProp(_el$8, "paddingBottom", 1);
1331
- _$setProp(_el$8, "gap", 1);
1332
- _$insertNode(_el$9, _el$0);
1333
- _$insertNode(_el$9, _el$11);
1334
- _$setProp(_el$9, "height", 1);
1335
- _$setProp(_el$9, "flexShrink", 0);
1336
- _$setProp(_el$9, "flexDirection", "row");
1337
- _$setProp(_el$9, "justifyContent", "space-between");
1338
- _$insertNode(_el$0, _el$1);
1339
- _$insertNode(_el$1, _$createTextNode(`PR/MR status`));
1340
- _$insertNode(_el$11, _$createTextNode(`esc`));
1341
- _$setProp(_el$11, "onMouseUp", close);
1342
- _$insertNode(_el$13, _el$14);
1343
- _$setProp(_el$13, "minHeight", 1);
1344
- _$setProp(_el$13, "flexShrink", 0);
1345
- _$setProp(_el$13, "scrollX", false);
1346
- _$setProp(_el$13, "contentOptions", {
1666
+ var _el$46 = _$createElement("box"), _el$47 = _$createElement("box"), _el$48 = _$createElement("text"), _el$49 = _$createElement("b"), _el$51 = _$createElement("text"), _el$53 = _$createElement("scrollbox"), _el$54 = _$createElement("box");
1667
+ _$insertNode(_el$46, _el$47);
1668
+ _$insertNode(_el$46, _el$53);
1669
+ _$insertNode(_el$46, _el$54);
1670
+ _$setProp(_el$46, "paddingLeft", 2);
1671
+ _$setProp(_el$46, "paddingRight", 2);
1672
+ _$setProp(_el$46, "paddingBottom", 1);
1673
+ _$setProp(_el$46, "gap", 1);
1674
+ _$insertNode(_el$47, _el$48);
1675
+ _$insertNode(_el$47, _el$51);
1676
+ _$setProp(_el$47, "height", 1);
1677
+ _$setProp(_el$47, "flexShrink", 0);
1678
+ _$setProp(_el$47, "flexDirection", "row");
1679
+ _$setProp(_el$47, "justifyContent", "space-between");
1680
+ _$insertNode(_el$48, _el$49);
1681
+ _$insertNode(_el$49, _$createTextNode(`PR/MR status`));
1682
+ _$insertNode(_el$51, _$createTextNode(`esc`));
1683
+ _$setProp(_el$51, "onMouseUp", close);
1684
+ _$setProp(_el$53, "minHeight", 1);
1685
+ _$setProp(_el$53, "flexShrink", 0);
1686
+ _$setProp(_el$53, "scrollX", false);
1687
+ _$setProp(_el$53, "contentOptions", {
1347
1688
  flexShrink: 0
1348
1689
  });
1349
- _$insert(_el$14, () => detailsMessage(snapshot2()));
1350
- _$setProp(_el$15, "height", 1);
1351
- _$setProp(_el$15, "flexShrink", 0);
1352
- _$setProp(_el$15, "flexDirection", "row");
1353
- _$setProp(_el$15, "justifyContent", "flex-end");
1354
- _$insert(_el$15, _$createComponent(For, {
1690
+ _$insert(_el$53, _$createComponent(StatusDetails, {
1691
+ get snapshot() {
1692
+ return snapshot2();
1693
+ }
1694
+ }));
1695
+ _$setProp(_el$54, "height", 1);
1696
+ _$setProp(_el$54, "flexShrink", 0);
1697
+ _$setProp(_el$54, "flexDirection", "row");
1698
+ _$setProp(_el$54, "justifyContent", "flex-end");
1699
+ _$insert(_el$54, _$createComponent(For, {
1355
1700
  each: ["close", "refresh"],
1356
1701
  children: (action) => {
1357
1702
  const disabled = () => action === "refresh" && snapshot2().loading;
1358
1703
  const active = () => activeAction() === action && !disabled();
1359
1704
  return (() => {
1360
- var _el$16 = _$createElement("box"), _el$17 = _$createElement("text");
1361
- _$insertNode(_el$16, _el$17);
1362
- _$setProp(_el$16, "paddingLeft", 1);
1363
- _$setProp(_el$16, "paddingRight", 1);
1364
- _$setProp(_el$16, "flexShrink", 0);
1365
- _$setProp(_el$16, "onMouseMove", () => {
1705
+ var _el$55 = _$createElement("box"), _el$56 = _$createElement("text");
1706
+ _$insertNode(_el$55, _el$56);
1707
+ _$setProp(_el$55, "paddingLeft", 1);
1708
+ _$setProp(_el$55, "paddingRight", 1);
1709
+ _$setProp(_el$55, "flexShrink", 0);
1710
+ _$setProp(_el$55, "onMouseMove", () => {
1366
1711
  if (!disabled())
1367
1712
  setActiveAction(action);
1368
1713
  });
1369
- _$setProp(_el$16, "onMouseUp", () => {
1714
+ _$setProp(_el$55, "onMouseUp", () => {
1370
1715
  if (disabled())
1371
1716
  return;
1372
1717
  setActiveAction(action);
@@ -1375,37 +1720,35 @@ var tui_default = {
1375
1720
  else
1376
1721
  close();
1377
1722
  });
1378
- _$setProp(_el$17, "wrapMode", "none");
1379
- _$insert(_el$17, () => action === "close" ? "Close" : snapshot2().loading ? "Refreshing\u2026" : "Refresh");
1723
+ _$setProp(_el$56, "wrapMode", "none");
1724
+ _$insert(_el$56, () => action === "close" ? "Close" : snapshot2().loading ? "Refreshing\u2026" : "Refresh");
1380
1725
  _$effect((_p$) => {
1381
- var _v$8 = active() ? context.theme.background.action.primary.focused : undefined, _v$9 = disabled() ? context.theme.text.action.primary.disabled : active() ? context.theme.text.action.primary.focused : context.theme.text.muted;
1382
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$16, "backgroundColor", _v$8, _p$.e));
1383
- _v$9 !== _p$.t && (_p$.t = _$setProp(_el$17, "fg", _v$9, _p$.t));
1726
+ var _v$10 = active() ? context.theme.background.action.primary.focused : undefined, _v$11 = disabled() ? context.theme.text.action.primary.disabled : active() ? context.theme.text.action.primary.focused : context.theme.text.muted;
1727
+ _v$10 !== _p$.e && (_p$.e = _$setProp(_el$55, "backgroundColor", _v$10, _p$.e));
1728
+ _v$11 !== _p$.t && (_p$.t = _$setProp(_el$56, "fg", _v$11, _p$.t));
1384
1729
  return _p$;
1385
1730
  }, {
1386
1731
  e: undefined,
1387
1732
  t: undefined
1388
1733
  });
1389
- return _el$16;
1734
+ return _el$55;
1390
1735
  })();
1391
1736
  }
1392
1737
  }));
1393
1738
  _$effect((_p$) => {
1394
- var _v$3 = contentHeight() + 5, _v$4 = context.theme.text.base, _v$5 = context.theme.text.muted, _v$6 = contentHeight(), _v$7 = context.theme.text.base;
1395
- _v$3 !== _p$.e && (_p$.e = _$setProp(_el$8, "height", _v$3, _p$.e));
1396
- _v$4 !== _p$.t && (_p$.t = _$setProp(_el$0, "fg", _v$4, _p$.t));
1397
- _v$5 !== _p$.a && (_p$.a = _$setProp(_el$11, "fg", _v$5, _p$.a));
1398
- _v$6 !== _p$.o && (_p$.o = _$setProp(_el$13, "height", _v$6, _p$.o));
1399
- _v$7 !== _p$.i && (_p$.i = _$setProp(_el$14, "fg", _v$7, _p$.i));
1739
+ var _v$8 = contentHeight() + 5, _v$9 = context.theme.text.base, _v$0 = context.theme.text.muted, _v$1 = contentHeight();
1740
+ _v$8 !== _p$.e && (_p$.e = _$setProp(_el$46, "height", _v$8, _p$.e));
1741
+ _v$9 !== _p$.t && (_p$.t = _$setProp(_el$48, "fg", _v$9, _p$.t));
1742
+ _v$0 !== _p$.a && (_p$.a = _$setProp(_el$51, "fg", _v$0, _p$.a));
1743
+ _v$1 !== _p$.o && (_p$.o = _$setProp(_el$53, "height", _v$1, _p$.o));
1400
1744
  return _p$;
1401
1745
  }, {
1402
1746
  e: undefined,
1403
1747
  t: undefined,
1404
1748
  a: undefined,
1405
- o: undefined,
1406
- i: undefined
1749
+ o: undefined
1407
1750
  });
1408
- return _el$8;
1751
+ return _el$46;
1409
1752
  })();
1410
1753
  }
1411
1754
  function showStatus(place = currentPlace()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-forgekeeper",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
5
5
  "author": {
6
6
  "name": "Pedro Pombeiro",