mergestorm 0.3.9 → 0.3.15

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 (76) hide show
  1. package/README.md +67 -24
  2. package/dist/api.d.ts +106 -40
  3. package/dist/api.js +245 -9
  4. package/dist/client/index.d.ts +5 -0
  5. package/dist/client/index.js +5 -0
  6. package/dist/commands/branches.d.ts +6 -2
  7. package/dist/commands/branches.js +29 -14
  8. package/dist/commands/browse.d.ts +39 -0
  9. package/dist/commands/browse.js +259 -0
  10. package/dist/commands/credits.js +30 -12
  11. package/dist/commands/help.d.ts +5 -0
  12. package/dist/commands/help.js +93 -0
  13. package/dist/commands/jobs.d.ts +11 -2
  14. package/dist/commands/jobs.js +64 -15
  15. package/dist/commands/login.js +32 -10
  16. package/dist/commands/logout.js +2 -1
  17. package/dist/commands/pr.d.ts +22 -0
  18. package/dist/commands/pr.js +127 -0
  19. package/dist/commands/queue.d.ts +12 -0
  20. package/dist/commands/queue.js +160 -0
  21. package/dist/commands/registry.d.ts +1 -1
  22. package/dist/commands/registry.js +115 -33
  23. package/dist/commands/review-client.d.ts +106 -0
  24. package/dist/commands/review-client.js +399 -0
  25. package/dist/commands/review.d.ts +36 -6
  26. package/dist/commands/review.js +395 -145
  27. package/dist/commands/settings.d.ts +17 -0
  28. package/dist/commands/settings.js +89 -0
  29. package/dist/commands/skill.d.ts +20 -0
  30. package/dist/commands/skill.js +145 -0
  31. package/dist/commands/stack.d.ts +36 -2
  32. package/dist/commands/stack.js +341 -113
  33. package/dist/commands/status.d.ts +18 -2
  34. package/dist/commands/status.js +236 -21
  35. package/dist/commands/whoami.d.ts +3 -1
  36. package/dist/commands/whoami.js +21 -7
  37. package/dist/errors.d.ts +18 -2
  38. package/dist/errors.js +22 -1
  39. package/dist/git-stack.d.ts +2 -0
  40. package/dist/git-stack.js +4 -0
  41. package/dist/git.d.ts +12 -1
  42. package/dist/git.js +52 -7
  43. package/dist/shell.d.ts +1 -1
  44. package/dist/shell.js +39 -28
  45. package/dist/stack-dto.d.ts +266 -0
  46. package/dist/stack-dto.js +7 -0
  47. package/dist/submit-pr-base.d.ts +31 -0
  48. package/dist/submit-pr-base.js +69 -0
  49. package/dist/ui/ansi.d.ts +2 -0
  50. package/dist/ui/ansi.js +2 -0
  51. package/dist/ui/banner.d.ts +34 -2
  52. package/dist/ui/banner.js +170 -79
  53. package/dist/ui/box.d.ts +13 -9
  54. package/dist/ui/box.js +28 -44
  55. package/dist/ui/findings.d.ts +4 -1
  56. package/dist/ui/findings.js +14 -7
  57. package/dist/ui/job-envelope.d.ts +68 -0
  58. package/dist/ui/job-envelope.js +35 -0
  59. package/dist/ui/line-tabs.d.ts +27 -0
  60. package/dist/ui/line-tabs.js +170 -0
  61. package/dist/ui/logo.d.ts +10 -2
  62. package/dist/ui/logo.js +13 -9
  63. package/dist/ui/present.d.ts +10 -0
  64. package/dist/ui/present.js +13 -0
  65. package/dist/ui/prompt.d.ts +148 -0
  66. package/dist/ui/prompt.js +433 -71
  67. package/dist/ui/select.js +1 -1
  68. package/dist/ui/tabs.d.ts +125 -0
  69. package/dist/ui/tabs.js +480 -0
  70. package/dist/ui/usage.d.ts +29 -1
  71. package/dist/ui/usage.js +71 -2
  72. package/dist/ui/width.d.ts +26 -0
  73. package/dist/ui/width.js +262 -0
  74. package/package.json +6 -1
  75. package/skill/SKILL.md +98 -0
  76. package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/api.js CHANGED
@@ -1,9 +1,42 @@
1
1
  import { apiBase, loadConfig, resolveApiKey } from "./config.js";
2
- import { CommandError } from "./errors.js";
2
+ import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
3
3
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
4
4
  export const DEFAULT_API_TIMEOUT_MS = 30_000;
5
5
  /** Message prefix of the CommandError thrown when a request exceeds its timeout. */
6
6
  export const API_TIMEOUT_PREFIX = "Request timed out after";
7
+ function asPositiveSeconds(value) {
8
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
9
+ return value;
10
+ }
11
+ if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim())) {
12
+ return Number(value.trim());
13
+ }
14
+ return undefined;
15
+ }
16
+ function parseRetryAfterHeader(header) {
17
+ if (!header)
18
+ return undefined;
19
+ const trimmed = header.trim();
20
+ const asNumber = asPositiveSeconds(trimmed);
21
+ if (asNumber !== undefined)
22
+ return asNumber;
23
+ const when = Date.parse(trimmed);
24
+ if (!Number.isFinite(when))
25
+ return undefined;
26
+ return Math.max(0, (when - Date.now()) / 1000);
27
+ }
28
+ /** Prefer the larger of `Retry-After` and JSON `retry_after_seconds`. */
29
+ export function parseRetryAfterSeconds(input) {
30
+ const fromBody = input.body && typeof input.body === "object"
31
+ ? asPositiveSeconds(input.body.retry_after_seconds)
32
+ : undefined;
33
+ const fromHeader = parseRetryAfterHeader(input.header);
34
+ if (fromBody === undefined)
35
+ return fromHeader;
36
+ if (fromHeader === undefined)
37
+ return fromBody;
38
+ return Math.max(fromBody, fromHeader);
39
+ }
7
40
  function isAbortLike(err) {
8
41
  return (err instanceof Error &&
9
42
  (err.name === "AbortError" || err.name === "TimeoutError"));
@@ -82,7 +115,15 @@ export async function apiFetch(cfg, route, init = {}) {
82
115
  if (res.status === 401) {
83
116
  throw new CommandError("API key invalid or revoked. Run `mergestorm login`.", 1, "auth_invalid");
84
117
  }
85
- return { status: res.status, body: parsed };
118
+ const retryAfterSeconds = parseRetryAfterSeconds({
119
+ header: res.headers.get("retry-after"),
120
+ body: parsed,
121
+ });
122
+ return {
123
+ status: res.status,
124
+ body: parsed,
125
+ ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
126
+ };
86
127
  }
87
128
  export async function devicePost(base, route, json, opts = {}) {
88
129
  const timeoutMs = opts.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
@@ -113,6 +154,71 @@ export async function devicePost(base, route, json, opts = {}) {
113
154
  }
114
155
  return { status: res.status, body: parsed };
115
156
  }
157
+ /**
158
+ * Bearer-writable settings keys, mirroring the `/api/v1/settings` allowlist.
159
+ * The connected flags are read-only there and deliberately absent here.
160
+ */
161
+ export const SETTINGS_WRITABLE_KEYS = [
162
+ "auto_review_enabled",
163
+ "auto_patch_enabled",
164
+ "vortex_show_thinking_traces",
165
+ "repo_overview_enabled",
166
+ "review_unit_land_prs_enabled",
167
+ "cyclone_review_unit_land_prs_enabled",
168
+ "vortex_seam_specialist_enabled",
169
+ ];
170
+ /**
171
+ * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
172
+ * degrade (same contract as {@link getMe}), while 401 still surfaces.
173
+ */
174
+ export async function getSettings(cfg, opts) {
175
+ const resolved = cfg ?? (await loadConfig());
176
+ try {
177
+ const { status, body } = await apiFetch(resolved, "/api/v1/settings", {
178
+ timeoutMs: opts?.timeoutMs,
179
+ });
180
+ if (status !== 200)
181
+ return null;
182
+ return body;
183
+ }
184
+ catch (err) {
185
+ if (err instanceof CommandError) {
186
+ if (err.code === "api_timeout")
187
+ return null;
188
+ throw err;
189
+ }
190
+ return null;
191
+ }
192
+ }
193
+ /**
194
+ * PATCH Bearer /api/v1/settings and return the stored result. Rejections
195
+ * (unknown key, cyclone_not_connected, …) surface the API's own `message`
196
+ * so the CLI never invents a reason.
197
+ */
198
+ export async function patchSettings(patch, cfg, opts) {
199
+ const resolved = cfg ?? (await loadConfig());
200
+ if (Object.keys(patch).length === 0) {
201
+ throw new CommandError("No settings to update.", 2, "usage");
202
+ }
203
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/settings", {
204
+ method: "PATCH",
205
+ json: patch,
206
+ timeoutMs: opts?.timeoutMs,
207
+ });
208
+ if (status === 404) {
209
+ throw new CommandError("Settings API is not available on this server yet. Deploy the API update or use the dashboard.");
210
+ }
211
+ if (status === 429) {
212
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
213
+ }
214
+ if (status !== 200) {
215
+ const message = body?.message;
216
+ throw new CommandError(typeof message === "string" && message.trim()
217
+ ? message
218
+ : `Failed to update settings (HTTP ${status}): ${JSON.stringify(body)}`);
219
+ }
220
+ return body;
221
+ }
116
222
  /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
117
223
  export async function getMe(cfg, opts) {
118
224
  const resolved = cfg ?? (await loadConfig());
@@ -137,10 +243,12 @@ export async function getMe(cfg, opts) {
137
243
  return null;
138
244
  }
139
245
  }
140
- export async function listJobs(limit = 10, cfg) {
246
+ export async function listJobs(limit = 10, cfg, opts) {
141
247
  const resolved = cfg ?? (await loadConfig());
142
248
  const capped = Math.min(Math.max(limit, 1), 50);
143
- const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`);
249
+ const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`, {
250
+ timeoutMs: opts?.timeoutMs,
251
+ });
144
252
  if (status === 404) {
145
253
  throw new CommandError("Job list is not available on this server yet. Update the API or use the dashboard.");
146
254
  }
@@ -150,19 +258,91 @@ export async function listJobs(limit = 10, cfg) {
150
258
  const items = body.items;
151
259
  return Array.isArray(items) ? items : [];
152
260
  }
261
+ /** Fetch one review job (Bearer GET /api/v1/reviews/:id). */
262
+ export async function getReview(jobId, cfg, opts) {
263
+ const resolved = cfg ?? (await loadConfig());
264
+ const id = jobId.trim();
265
+ if (!id) {
266
+ throw new CommandError("job_id is required", 2, "usage");
267
+ }
268
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/reviews/${encodeURIComponent(id)}`, { signal: opts?.signal, timeoutMs: opts?.timeoutMs });
269
+ if (status === 404) {
270
+ throw new CommandError(`Review not found: ${id}`);
271
+ }
272
+ if (status === 429) {
273
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
274
+ }
275
+ if (status !== 200) {
276
+ throw new CommandError(`Failed to get review (HTTP ${status}): ${JSON.stringify(body)}`);
277
+ }
278
+ return body;
279
+ }
280
+ /** Fetch the latest Vortex pass for one GitHub PR. */
281
+ export async function getPrVortexReview(owner, repo, prNumber, cfg, opts) {
282
+ const resolved = cfg ?? (await loadConfig());
283
+ const cleanOwner = owner.trim();
284
+ const cleanRepo = repo.trim();
285
+ if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
286
+ throw new CommandError("owner, repo, and a positive pr_number are required", 2, "usage");
287
+ }
288
+ const query = new URLSearchParams({
289
+ owner: cleanOwner,
290
+ repo: cleanRepo,
291
+ pr_number: String(prNumber),
292
+ });
293
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/stacks/pr-review?${query.toString()}`, { signal: opts?.signal, timeoutMs: opts?.timeoutMs });
294
+ if (status === 404) {
295
+ throw new CommandError(`Vortex review not found: ${cleanOwner}/${cleanRepo}#${prNumber}`, REVIEW_EXIT.failed, "not_found");
296
+ }
297
+ if (status === 429) {
298
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
299
+ }
300
+ if (status !== 200) {
301
+ throw new CommandError(`Failed to get PR review (HTTP ${status}): ${JSON.stringify(body)}`, REVIEW_EXIT.failed, "review_failed");
302
+ }
303
+ return body;
304
+ }
153
305
  /** List stacks (Bearer GET /api/v1/stacks). */
154
- export async function listStacks(cfg) {
306
+ export async function listStacks(cfg, opts) {
155
307
  const resolved = cfg ?? (await loadConfig());
156
- const { status, body } = await apiFetch(resolved, "/api/v1/stacks");
308
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/stacks", {
309
+ timeoutMs: opts?.timeoutMs,
310
+ });
157
311
  if (status === 404) {
158
312
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
159
313
  }
314
+ if (status === 429) {
315
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
316
+ }
160
317
  if (status !== 200) {
161
318
  throw new CommandError(`Failed to list stacks (HTTP ${status}): ${JSON.stringify(body)}`);
162
319
  }
163
320
  const stacks = body.stacks;
164
321
  return Array.isArray(stacks) ? stacks : [];
165
322
  }
323
+ /** Fetch one owned stack with enriched GitHub checks and agent state. */
324
+ export async function getEnrichedStack(stackId, cfg, opts) {
325
+ const resolved = cfg ?? (await loadConfig());
326
+ const id = stackId.trim();
327
+ if (!id) {
328
+ throw new CommandError("stack_id is required", 2, "usage");
329
+ }
330
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/stacks/enrich?stackId=${encodeURIComponent(id)}`, { timeoutMs: opts?.timeoutMs });
331
+ if (status === 404) {
332
+ throw new CommandError("Stack enrichment is not available on this server yet. Deploy the API update or use the dashboard.");
333
+ }
334
+ if (status === 429) {
335
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
336
+ }
337
+ if (status !== 200) {
338
+ throw new CommandError(`Failed to get stack status (HTTP ${status}): ${JSON.stringify(body)}`);
339
+ }
340
+ const stacks = body.stacks;
341
+ if (!Array.isArray(stacks)) {
342
+ throw new CommandError(`Failed to get stack status (HTTP 200): ${JSON.stringify(body)}`);
343
+ }
344
+ return stacks.find((stack) => stack?.id === id) ?? null;
345
+ }
166
346
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
167
347
  export async function adoptStack(owner, repo, prNumber, cfg) {
168
348
  const resolved = cfg ?? (await loadConfig());
@@ -178,6 +358,21 @@ export async function adoptStack(owner, repo, prNumber, cfg) {
178
358
  }
179
359
  return body;
180
360
  }
361
+ /** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
362
+ export async function ensureUpperPark(stackId, cfg) {
363
+ const body = await stackMutation(stackId, "/ensure-upper-park", "POST", {}, "Failed to ensure upper-park freeze", cfg);
364
+ if (typeof body !== "object" || body === null) {
365
+ throw new CommandError("Unexpected response shape from ensure-upper-park");
366
+ }
367
+ const freezeBranch = body.freezeBranch;
368
+ if (typeof freezeBranch !== "string" || !freezeBranch.trim()) {
369
+ throw new CommandError("ensure-upper-park did not return a freezeBranch");
370
+ }
371
+ return {
372
+ freezeBranch: freezeBranch.trim(),
373
+ created: body.created === true,
374
+ };
375
+ }
181
376
  async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg) {
182
377
  const resolved = cfg ?? (await loadConfig());
183
378
  const { status, body } = await apiFetch(resolved, `/api/v1/stacks/${encodeURIComponent(stackId)}${pathSuffix}`, method === "PATCH"
@@ -206,9 +401,50 @@ export async function landNextStack(stackId, cfg, opts) {
206
401
  : {};
207
402
  return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
208
403
  }
209
- /** Toggle auto-promote when green (Bearer PATCH /api/v1/stacks/:id). */
210
- export async function setStackAutoPromote(stackId, autoPromoteWhenGreen, cfg) {
211
- return stackMutation(stackId, "", "PATCH", { autoPromoteWhenGreen }, "Failed to update auto-promote", cfg);
404
+ /** List the authenticated user's live merge-queue entries. */
405
+ export async function listMergeQueueEntries(cfg) {
406
+ const resolved = cfg ?? (await loadConfig());
407
+ const { status, body } = await apiFetch(resolved, "/api/v1/stacks/queue");
408
+ if (status === 404) {
409
+ throw new CommandError("Merge queue API is not available on this server yet. Deploy the API update or use the dashboard.");
410
+ }
411
+ if (status !== 200) {
412
+ throw new CommandError(`Failed to list merge queue (HTTP ${status}): ${JSON.stringify(body)}`);
413
+ }
414
+ const entries = body.entries;
415
+ if (!Array.isArray(entries)) {
416
+ throw new CommandError(`Failed to list merge queue (HTTP 200): ${JSON.stringify(body)}`);
417
+ }
418
+ return entries;
419
+ }
420
+ async function queueMutation(route, failLabel, cfg) {
421
+ const resolved = cfg ?? (await loadConfig());
422
+ const { status, body } = await apiFetch(resolved, route, {
423
+ method: "POST",
424
+ json: {},
425
+ });
426
+ if (status === 404) {
427
+ const error = body?.error;
428
+ if (error === "stack_not_found") {
429
+ throw new CommandError("Stack not found");
430
+ }
431
+ if (error === "entry_not_found") {
432
+ throw new CommandError("Merge queue entry not found");
433
+ }
434
+ throw new CommandError("Merge queue API is not available on this server yet. Deploy the API update or use the dashboard.");
435
+ }
436
+ if (status < 200 || status >= 300) {
437
+ throw new CommandError(`${failLabel} (HTTP ${status}): ${JSON.stringify(body)}`);
438
+ }
439
+ return body;
440
+ }
441
+ /** Enqueue a stack for verified landing. */
442
+ export async function enqueueStack(stackId, cfg) {
443
+ return queueMutation(`/api/v1/stacks/${encodeURIComponent(stackId)}/enqueue`, "Failed to add stack to merge queue", cfg);
444
+ }
445
+ /** Cancel a live merge-queue entry by entry id. */
446
+ export async function cancelMergeQueueEntry(entryId, cfg) {
447
+ return queueMutation(`/api/v1/stacks/queue/${encodeURIComponent(entryId)}/cancel`, "Failed to remove merge queue entry", cfg);
212
448
  }
213
449
  /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
214
450
  export async function listThreads(limit = 20, cfg) {
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type ThreadDetail, type ThreadListItem, } from "../api.js";
3
+ export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
4
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
5
+ export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, } from "../api.js";
3
+ export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
4
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
5
+ export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
@@ -2,9 +2,13 @@ import { type ThreadListItem } from "../api.js";
2
2
  /** One-line label for a branch/thread row (also used by tests). */
3
3
  export declare function formatBranchRow(item: ThreadListItem): string;
4
4
  /** Interactive list of recently reviewed branches → open chain on select. */
5
- export declare function cmdBranches(args?: string[]): Promise<void>;
5
+ export declare function cmdBranches(args?: string[], opts?: {
6
+ mode?: "oneshot" | "shell";
7
+ }): Promise<void>;
6
8
  /**
7
9
  * Show the review-chain timeline for a branch/thread.
8
10
  * Defaults to `local/<current-git-branch>` when no slug is given.
9
11
  */
10
- export declare function cmdChain(args?: string[]): Promise<void>;
12
+ export declare function cmdChain(args?: string[], opts?: {
13
+ mode?: "oneshot" | "shell";
14
+ }): Promise<void>;
@@ -4,8 +4,10 @@ import { loadConfig } from "../config.js";
4
4
  import { CommandError } from "../errors.js";
5
5
  import { git } from "../git.js";
6
6
  import { ansi } from "../ui/ansi.js";
7
+ import { present } from "../ui/present.js";
7
8
  import { selectFromList } from "../ui/select.js";
8
- import { cmdThread, relativeWhen } from "./jobs.js";
9
+ import { buildChainHelpLines, cmdThread, relativeWhen } from "./jobs.js";
10
+ import { showLinePanel } from "../ui/line-tabs.js";
9
11
  /** One-line label for a branch/thread row (also used by tests). */
10
12
  export function formatBranchRow(item) {
11
13
  const name = (item.branch?.trim() || item.slug).slice(0, 36);
@@ -18,7 +20,7 @@ export function formatBranchRow(item) {
18
20
  const closed = item.status === "closed" ? " closed" : "";
19
21
  return `${name.padEnd(36)} ${jobs.padEnd(8)} ${verdict.padEnd(12)} ${when}${pr}${closed}`;
20
22
  }
21
- function printBranchTable(items) {
23
+ function formatBranchTableLines(items) {
22
24
  const header = ["BRANCH / SLUG", "JOBS", "VERDICT", "WHEN", "PR"];
23
25
  const rows = items.map((t) => [
24
26
  (t.branch?.trim() || t.slug).slice(0, 36),
@@ -29,28 +31,38 @@ function printBranchTable(items) {
29
31
  ]);
30
32
  const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
31
33
  const fmt = (cols) => " " + cols.map((c, i) => c.padEnd(widths[i])).join(" ");
32
- console.log(ansi.dim(fmt(header)));
33
- for (const r of rows)
34
- console.log(fmt(r));
35
- console.log(ansi.dim(" open a chain: chain <slug> · or re-run on a TTY to pick"));
34
+ return [
35
+ ansi.dim(fmt(header)),
36
+ ...rows.map((r) => fmt(r)),
37
+ ansi.dim(" open a chain: chain <slug> · or re-run on a TTY to pick"),
38
+ ];
36
39
  }
37
40
  /** Interactive list of recently reviewed branches → open chain on select. */
38
- export async function cmdBranches(args = []) {
41
+ export async function cmdBranches(args = [], opts = {}) {
39
42
  const asJson = args.includes("--json");
40
43
  const nArg = args.find((a) => /^\d+$/.test(a));
41
44
  const limit = nArg ? Number.parseInt(nArg, 10) : 20;
42
45
  const cfg = await loadConfig();
43
46
  const items = await listThreads(limit, cfg);
44
47
  if (asJson) {
45
- console.log(JSON.stringify({ items }, null, 2));
48
+ const text = JSON.stringify({ items }, null, 2);
49
+ if (opts.mode === "shell") {
50
+ await present("Branches", text.split("\n"));
51
+ return;
52
+ }
53
+ console.log(text);
46
54
  return;
47
55
  }
48
56
  if (items.length === 0) {
49
- console.log(ansi.dim(" No reviewed branches yet. Run `review` on a branch first."));
57
+ await present("Branches", [
58
+ ` ${ansi.bold("No reviewed branches yet")}`,
59
+ "",
60
+ ` ${ansi.brightGreen("review")} submit a diff on this branch`,
61
+ ]);
50
62
  return;
51
63
  }
52
64
  if (!input.isTTY || !output.isTTY) {
53
- printBranchTable(items);
65
+ await present("Branches", formatBranchTableLines(items));
54
66
  return;
55
67
  }
56
68
  const picked = await selectFromList({
@@ -59,7 +71,6 @@ export async function cmdBranches(args = []) {
59
71
  render: (item) => formatBranchRow(item),
60
72
  });
61
73
  if (!picked) {
62
- console.log(ansi.dim(" cancelled"));
63
74
  return;
64
75
  }
65
76
  await cmdThread(picked.slug, []);
@@ -68,7 +79,7 @@ export async function cmdBranches(args = []) {
68
79
  * Show the review-chain timeline for a branch/thread.
69
80
  * Defaults to `local/<current-git-branch>` when no slug is given.
70
81
  */
71
- export async function cmdChain(args = []) {
82
+ export async function cmdChain(args = [], opts = {}) {
72
83
  const asJson = args.includes("--json");
73
84
  const positional = args.filter((a) => a !== "--json");
74
85
  let slug = positional[0]?.trim() ?? "";
@@ -78,8 +89,12 @@ export async function cmdChain(args = []) {
78
89
  slug = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
79
90
  }
80
91
  catch {
81
- throw new CommandError("usage: chain <slug> (or run inside a git repo to use the current branch)");
92
+ if (asJson || !process.stdin.isTTY || !process.stdout.isTTY) {
93
+ throw new CommandError("usage: chain <slug> (or run inside a git repo to use the current branch)");
94
+ }
95
+ await showLinePanel("Chain", buildChainHelpLines({ reason: "no-repo" }));
96
+ return;
82
97
  }
83
98
  }
84
- await cmdThread(slug, asJson ? ["--json"] : []);
99
+ await cmdThread(slug, asJson ? ["--json"] : [], opts);
85
100
  }
@@ -0,0 +1,39 @@
1
+ import { type JobListItem, type SettingsResponse, type SettingsWritableKey } from "../api.js";
2
+ import { type ReviewJobRow } from "../ui/job-envelope.js";
3
+ import { type TabId, type TabsConfigRow } from "../ui/tabs.js";
4
+ import { type UsagePanelJob } from "../ui/usage.js";
5
+ /** The tabbed browser needs a real terminal on both ends. */
6
+ export declare function canBrowse(): boolean;
7
+ /** Shared job row mapper for the Usage tab and the static `credits` panel. */
8
+ export declare function toPanelJob(job: JobListItem): UsagePanelJob;
9
+ export type StatusTabInput = {
10
+ keyPrefix: string;
11
+ keyName: string | null;
12
+ plan: string | null;
13
+ apiBase: string;
14
+ reachable: boolean;
15
+ /** getMe surfaced a 401: the key is invalid/revoked, not the API down. */
16
+ keyInvalid: boolean;
17
+ used?: number;
18
+ limit?: number | null;
19
+ remaining?: number | null;
20
+ resetsAt?: string | null;
21
+ now?: Date;
22
+ };
23
+ /** Short chrome note — lives on the hint row, not in the Status body. */
24
+ export declare function statusReachabilityNote(input: Pick<StatusTabInput, "reachable" | "keyInvalid">): string;
25
+ /** Status tab body: whoami + credits from `/me`. Reachability is the footer. */
26
+ export declare function buildStatusTabLines(input: StatusTabInput): string[];
27
+ /** In-place job detail for the Jobs tab (same envelope as `status <id>`). */
28
+ export declare function formatJobDetailLines(row: ReviewJobRow): string[];
29
+ /** Human labels for the Config tab and the static `settings` printout. */
30
+ export declare const SETTINGS_LABELS: Record<SettingsWritableKey, string>;
31
+ /** Config rows in a stable order: writable toggles, then connected flags. */
32
+ export declare function buildConfigRows(settings: SettingsResponse): TabsConfigRow[];
33
+ /**
34
+ * Data + entry for the tabbed Status / Usage / Jobs / Config browser (F-9).
35
+ * Degrades honestly: a null `/me` marks the API unreachable and the Usage
36
+ * tab says so, and a failed settings GET leaves the Config tab reporting
37
+ * itself unavailable instead of drawing toggles we do not have.
38
+ */
39
+ export declare function openTabsBrowser(initial: TabId): Promise<void>;