mergestorm 0.3.13 → 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 (60) hide show
  1. package/README.md +14 -15
  2. package/dist/api.d.ts +82 -5
  3. package/dist/api.js +166 -7
  4. package/dist/client/index.d.ts +2 -2
  5. package/dist/client/index.js +2 -2
  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 +7 -10
  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 +4 -3
  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.js +93 -18
  22. package/dist/commands/review-client.d.ts +13 -0
  23. package/dist/commands/review-client.js +110 -0
  24. package/dist/commands/review.d.ts +6 -0
  25. package/dist/commands/review.js +18 -7
  26. package/dist/commands/settings.d.ts +17 -0
  27. package/dist/commands/settings.js +89 -0
  28. package/dist/commands/skill.d.ts +20 -0
  29. package/dist/commands/skill.js +145 -0
  30. package/dist/commands/stack.d.ts +2 -0
  31. package/dist/commands/stack.js +91 -57
  32. package/dist/commands/status.d.ts +2 -0
  33. package/dist/commands/status.js +30 -15
  34. package/dist/commands/whoami.d.ts +3 -1
  35. package/dist/commands/whoami.js +21 -7
  36. package/dist/errors.d.ts +1 -1
  37. package/dist/shell.js +39 -31
  38. package/dist/stack-dto.d.ts +82 -1
  39. package/dist/ui/ansi.d.ts +2 -0
  40. package/dist/ui/ansi.js +2 -0
  41. package/dist/ui/banner.d.ts +34 -2
  42. package/dist/ui/banner.js +163 -79
  43. package/dist/ui/findings.d.ts +2 -1
  44. package/dist/ui/findings.js +13 -7
  45. package/dist/ui/line-tabs.d.ts +27 -0
  46. package/dist/ui/line-tabs.js +170 -0
  47. package/dist/ui/logo.d.ts +10 -2
  48. package/dist/ui/logo.js +13 -9
  49. package/dist/ui/present.d.ts +10 -0
  50. package/dist/ui/present.js +13 -0
  51. package/dist/ui/prompt.d.ts +58 -1
  52. package/dist/ui/prompt.js +225 -42
  53. package/dist/ui/select.js +1 -1
  54. package/dist/ui/tabs.d.ts +125 -0
  55. package/dist/ui/tabs.js +480 -0
  56. package/dist/ui/usage.d.ts +4 -0
  57. package/dist/ui/usage.js +22 -2
  58. package/package.json +2 -1
  59. package/skill/SKILL.md +98 -0
  60. package/skill/mergestorm-pr-loop/SKILL.md +76 -0
@@ -1,5 +1,7 @@
1
1
  import { getReview } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
+ import { showLinePanel } from "../ui/line-tabs.js";
4
+ import { canBrowse, formatJobDetailLines } from "./browse.js";
3
5
  import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, } from "../errors.js";
4
6
  import { ansi } from "../ui/ansi.js";
5
7
  import { renderFindings } from "../ui/findings.js";
@@ -77,6 +79,13 @@ function printPretty(row) {
77
79
  emptyMessage: envelope.status === "completed",
78
80
  });
79
81
  }
82
+ async function showPretty(row) {
83
+ if (canBrowse()) {
84
+ await showLinePanel("Job", formatJobDetailLines(row));
85
+ return;
86
+ }
87
+ printPretty(row);
88
+ }
80
89
  export async function cmdStatus(args, opts = {}) {
81
90
  const parsed = parseStatusArgs(args, {
82
91
  format: opts.defaultFormat ?? "json",
@@ -85,8 +94,14 @@ export async function cmdStatus(args, opts = {}) {
85
94
  const interactive = Boolean(opts.interactive && !json);
86
95
  const fallback = { jobId: parsed.jobId };
87
96
  let jsonPrinted = false;
88
- const printJson = (row, extra = {}) => {
89
- console.log(JSON.stringify(toReviewJobEnvelope(row, { ...fallback, ...extra })));
97
+ const printJson = async (row, extra = {}) => {
98
+ const text = JSON.stringify(toReviewJobEnvelope(row, { ...fallback, ...extra }), null, opts.mode === "shell" ? 2 : undefined);
99
+ if (opts.mode === "shell") {
100
+ await showLinePanel("Job", text.split("\n"));
101
+ }
102
+ else {
103
+ console.log(text);
104
+ }
90
105
  jsonPrinted = true;
91
106
  };
92
107
  try {
@@ -105,12 +120,12 @@ export async function cmdStatus(args, opts = {}) {
105
120
  throw err;
106
121
  if (isCommandErrorCode(err, "api_timeout")) {
107
122
  if (json)
108
- printJson(null, { status: "in_progress", error: err.message });
123
+ await printJson(null, { status: "in_progress", error: err.message });
109
124
  throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
110
125
  }
111
126
  if (isCommandErrorCode(err, "rate_limited")) {
112
127
  if (json) {
113
- printJson(null, {
128
+ await printJson(null, {
114
129
  status: "rate_limited",
115
130
  error: err.message,
116
131
  retryAfterSeconds: err.retryAfterSeconds,
@@ -129,15 +144,15 @@ export async function cmdStatus(args, opts = {}) {
129
144
  : { job_id: parsed.jobId, status: "failed", error: "Invalid review response" };
130
145
  if (row.status === "failed" || row.status === "quota_exceeded") {
131
146
  if (json)
132
- printJson(row);
147
+ await printJson(row);
133
148
  else
134
- printPretty(row);
149
+ await showPretty(row);
135
150
  throw new CommandError(`Review ${row.status}: ${row.error ?? ""}`, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
136
151
  }
137
152
  if (json)
138
- printJson(row);
153
+ await printJson(row);
139
154
  else
140
- printPretty(row);
155
+ await showPretty(row);
141
156
  return;
142
157
  }
143
158
  if (!json) {
@@ -171,7 +186,7 @@ export async function cmdStatus(args, opts = {}) {
171
186
  if (err instanceof ReviewPollTimeoutError) {
172
187
  const message = `Timed out waiting for review ${parsed.jobId}`;
173
188
  if (json) {
174
- printJson(err.lastRow, {
189
+ await printJson(err.lastRow, {
175
190
  jobId: parsed.jobId,
176
191
  status: err.lastRow.status ?? "in_progress",
177
192
  error: "Timed out waiting for review.",
@@ -181,7 +196,7 @@ export async function cmdStatus(args, opts = {}) {
181
196
  }
182
197
  if (isCommandErrorCode(err, "rate_limited")) {
183
198
  if (json) {
184
- printJson(null, {
199
+ await printJson(null, {
185
200
  status: "rate_limited",
186
201
  error: err.message,
187
202
  retryAfterSeconds: err.retryAfterSeconds,
@@ -200,15 +215,15 @@ export async function cmdStatus(args, opts = {}) {
200
215
  if (row.status === "failed" || row.status === "quota_exceeded") {
201
216
  const message = `Review ${row.status}: ${row.error ?? ""}`;
202
217
  if (json)
203
- printJson(row);
218
+ await printJson(row);
204
219
  else
205
- printPretty(row);
220
+ await showPretty(row);
206
221
  throw new CommandError(message, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
207
222
  }
208
223
  if (json)
209
- printJson(row);
224
+ await printJson(row);
210
225
  else
211
- printPretty(row);
226
+ await showPretty(row);
212
227
  }
213
228
  catch (err) {
214
229
  if (err instanceof DetachedError)
@@ -217,7 +232,7 @@ export async function cmdStatus(args, opts = {}) {
217
232
  !jsonPrinted &&
218
233
  !(err instanceof Error && err.name === "AbortError")) {
219
234
  const message = err instanceof Error ? err.message : String(err);
220
- printJson(null, { status: "failed", error: message });
235
+ await printJson(null, { status: "failed", error: message });
221
236
  }
222
237
  if (err instanceof CommandError &&
223
238
  (err.code === "auth_invalid" || err.code === "missing_api_key")) {
@@ -1 +1,3 @@
1
- export declare function cmdWhoami(args: string[]): Promise<void>;
1
+ export declare function cmdWhoami(args: string[], opts?: {
2
+ mode?: "oneshot" | "shell";
3
+ }): Promise<void>;
@@ -2,13 +2,19 @@ import { getMe } from "../api.js";
2
2
  import { apiBase, configPath, loadConfig, resolveApiKey, keyDisplay } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { ansi } from "../ui/ansi.js";
5
- export async function cmdWhoami(args) {
5
+ import { present } from "../ui/present.js";
6
+ import { canBrowse, openTabsBrowser } from "./browse.js";
7
+ export async function cmdWhoami(args, opts = {}) {
6
8
  const asJson = args.includes("--json");
7
9
  const cfg = await loadConfig();
8
10
  const key = resolveApiKey(cfg);
9
11
  if (!key) {
10
12
  throw new CommandError("Not logged in. Run `mergestorm login`.");
11
13
  }
14
+ if (!asJson && canBrowse()) {
15
+ await openTabsBrowser("status");
16
+ return;
17
+ }
12
18
  const me = await getMe(cfg);
13
19
  const payload = {
14
20
  key_prefix: me?.key.prefix ?? keyDisplay(key),
@@ -20,16 +26,24 @@ export async function cmdWhoami(args) {
20
26
  usage: me?.usage ?? null,
21
27
  };
22
28
  if (asJson) {
23
- console.log(JSON.stringify(payload, null, 2));
29
+ const text = JSON.stringify(payload, null, 2);
30
+ if (opts.mode === "shell") {
31
+ await present("Account", text.split("\n"));
32
+ return;
33
+ }
34
+ console.log(text);
24
35
  return;
25
36
  }
26
- console.log(` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`);
37
+ const lines = [
38
+ ` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`,
39
+ ];
27
40
  if (payload.plan_key) {
28
- console.log(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
41
+ lines.push(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
29
42
  }
30
- console.log(` API ${payload.api_base}`);
31
- console.log(` Config ${payload.config_path}`);
43
+ lines.push(` API ${payload.api_base}`);
44
+ lines.push(` Config ${payload.config_path}`);
32
45
  if (!me) {
33
- console.log(ansi.dim(" (live account details unavailable — older API or offline)"));
46
+ lines.push(ansi.dim(" (live account details unavailable — older API or offline)"));
34
47
  }
48
+ await present("Account", lines);
35
49
  }
package/dist/errors.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Stable machine codes for CommandError control flow.
3
3
  * UX copy in `message` may change freely; match on `code`, never on message text.
4
4
  */
5
- export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
5
+ export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
6
6
  /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
7
7
  export declare const REVIEW_EXIT: {
8
8
  readonly success: 0;
package/dist/shell.js CHANGED
@@ -5,21 +5,15 @@ import { dispatchCommand, shellCommandSpecs, } from "./commands/registry.js";
5
5
  import { ansi } from "./ui/ansi.js";
6
6
  import { printBannerHeader, shellPrompt } from "./ui/banner.js";
7
7
  import { CTRL_C_EXIT_HINT, CtrlCExitGate } from "./ui/ctrl-c-exit.js";
8
- import { askLine, formatSlashLabel, PromptClosedError } from "./ui/prompt.js";
8
+ import { openHelpBrowser } from "./commands/help.js";
9
+ import { showLinePanel } from "./ui/line-tabs.js";
10
+ import { askLine, PromptClosedError } from "./ui/prompt.js";
11
+ async function showShellNotice(title, message) {
12
+ const lines = message.split("\n").flatMap((line) => (line ? [` ${line}`] : [""]));
13
+ await showLinePanel(title, [...lines, "", ` ${ansi.dim("q to return")}`]);
14
+ }
9
15
  /** Shell autocomplete + `/help` � derived from the command registry (STRUCT-01). */
10
16
  export const COMMANDS = shellCommandSpecs();
11
- function printHelp() {
12
- const rows = COMMANDS.map((c) => {
13
- const label = formatSlashLabel(c).slice(1);
14
- return ` ${ansi.green("/" + label.padEnd(20))} ${c.summary}`;
15
- }).join("\n");
16
- console.log(`
17
- ${ansi.bold("Commands")}
18
- ${rows}
19
-
20
- Dashboard: https://mergestorm.ai/settings#api
21
- `);
22
- }
23
17
  /** Strips an optional leading "/" so both "review" and "/review" work. */
24
18
  export function parseLine(line) {
25
19
  let trimmed = line.trim();
@@ -34,10 +28,22 @@ export function parseLine(line) {
34
28
  return { cmd, args: parts.slice(1) };
35
29
  }
36
30
  export async function runShell() {
37
- try {
38
- await printBannerHeader();
39
- }
40
- catch { /* banner is best-effort */ }
31
+ // Idle is always the home screen: header + docked prompt. Command TUIs
32
+ // (usage / credits / status) and printed output become scrollback when
33
+ // the next askLine homes + ED 0. Do not retire the header after the
34
+ // first command � that is what floated a bare prompt at the top.
35
+ let banner = null;
36
+ const showBanner = async () => {
37
+ try {
38
+ banner = await printBannerHeader({ paint: false });
39
+ }
40
+ catch {
41
+ // Keep the previous chrome on a transient refresh failure rather than
42
+ // blanking the always-on home header.
43
+ }
44
+ return banner;
45
+ };
46
+ banner = await showBanner();
41
47
  const history = [];
42
48
  let busyAbort = null;
43
49
  const idleCtrlC = new CtrlCExitGate();
@@ -71,6 +77,11 @@ export async function runShell() {
71
77
  prompt: shellPrompt(loggedIn),
72
78
  history,
73
79
  commands: COMMANDS,
80
+ ...(banner
81
+ ? {
82
+ header: (columns, variant) => banner.rows(columns, variant),
83
+ }
84
+ : {}),
74
85
  });
75
86
  }
76
87
  catch (err) {
@@ -90,15 +101,12 @@ export async function runShell() {
90
101
  if (cmd === "exit" || cmd === "quit")
91
102
  break;
92
103
  if (cmd === "help" || cmd === "?") {
93
- printHelp();
104
+ await openHelpBrowser();
94
105
  continue;
95
106
  }
96
107
  if (cmd === "clear") {
97
108
  output.write("");
98
- try {
99
- await printBannerHeader();
100
- }
101
- catch { /* banner is best-effort */ }
109
+ await showBanner();
102
110
  continue;
103
111
  }
104
112
  busyAbort = new AbortController();
@@ -107,33 +115,33 @@ export async function runShell() {
107
115
  mode: "shell",
108
116
  signal: busyAbort.signal,
109
117
  afterAuth: async () => {
110
- try {
111
- await printBannerHeader();
112
- }
113
- catch { /* banner is best-effort */ }
118
+ await showBanner();
114
119
  },
115
120
  });
116
121
  if (!ok) {
117
- console.error(ansi.dim(`unknown command "${cmd}" -- type help`));
122
+ await showShellNotice("Error", `unknown command "${cmd}"\ntype help for the list`);
118
123
  }
119
124
  }
120
125
  catch (err) {
121
126
  if (err instanceof DetachedError) {
122
- console.log(`\n${ansi.yellow(err.message)}`);
127
+ await showShellNotice("Detached", err.message);
123
128
  }
124
129
  else if (err instanceof CommandError) {
125
- console.error(ansi.red(err.message));
130
+ await showShellNotice("Error", err.message);
126
131
  }
127
132
  else if (err instanceof Error && err.name === "AbortError") {
128
- console.log(ansi.dim("\naborted"));
133
+ await showShellNotice("Aborted", "aborted");
129
134
  }
130
135
  else {
131
- console.error(ansi.red(err instanceof Error ? err.message : String(err)));
136
+ await showShellNotice("Error", err instanceof Error ? err.message : String(err));
132
137
  }
133
138
  }
134
139
  finally {
135
140
  busyAbort = null;
136
141
  }
142
+ // Re-capture the home header so usage/credits shown above the prompt
143
+ // reflect what the command just consumed (e.g. review spending a credit).
144
+ await showBanner();
137
145
  }
138
146
  }
139
147
  finally {
@@ -45,6 +45,25 @@ export type StackLayerChecks = {
45
45
  pending: number;
46
46
  failure: number;
47
47
  failingName: string | null;
48
+ /**
49
+ * True when the persisted `ci_checks` carries a `namedRuns` seed (enrich
50
+ * writes it so CI webhook deliveries fold into the snapshot). Pre-seeding
51
+ * rows are false — the webhook cannot fold into them, so the interval poll
52
+ * must keep enriching until the first enrich seeds one (#1602). Status-only
53
+ * layers (empty `namedRuns` over a non-empty rollup) are also false — the
54
+ * webhook refuses to fold into an empty map, so the poll keeps enriching
55
+ * them (#1602).
56
+ */
57
+ namedRunsSeeded?: boolean;
58
+ /**
59
+ * True when the persisted `namedRuns` carries the reserved synthetic
60
+ * "commit status" run at a pending/failure verdict. Webhook deliveries
61
+ * re-key by check/context name and can never advance that seed, so the
62
+ * interval poll must keep enriching until the next enrich re-seeds it green
63
+ * (or drops it). Absent (false) when the seed is green — folds keep that
64
+ * layer fresh (#1602).
65
+ */
66
+ combinedStatusSeeded?: boolean;
48
67
  };
49
68
  /** Closing / Development-sidebar issue. Cap 3 on the wire. */
50
69
  export type StackLinkedIssue = {
@@ -70,6 +89,14 @@ export type StackLayerDto = {
70
89
  branch: string;
71
90
  parentBranch: string | null;
72
91
  prNumber: number;
92
+ /** Persisted PR timing and diff snapshots from pr_stats. */
93
+ openedAt: string | null;
94
+ mergedAt: string | null;
95
+ closedAt: string | null;
96
+ additions: number | null;
97
+ deletions: number | null;
98
+ openAdditions: number | null;
99
+ openDeletions: number | null;
73
100
  position: number;
74
101
  state: StackBranchState;
75
102
  title: string | null;
@@ -96,6 +123,12 @@ export type StackLayerDto = {
96
123
  mergeable: boolean | null;
97
124
  /** GitHub REST mergeable_state; null when unknown. */
98
125
  mergeableState: string | null;
126
+ /** Most recently observed PR head SHA. */
127
+ headSha?: string | null;
128
+ /** Head SHA that produced mergeable / mergeableState. */
129
+ mergeableHeadSha?: string | null;
130
+ /** GitHub PR draft flag from enrich; the auto-land watcher never enqueues a draft. */
131
+ draft?: boolean | null;
99
132
  /**
100
133
  * Vortex-style linked issues (keywords + Development sidebar), cap 3.
101
134
  * null until first enrich finds any; kept on merged unit members (unlike CI).
@@ -121,6 +154,14 @@ export type StackLayerDto = {
121
154
  };
122
155
  export type StackUnitMemberDto = {
123
156
  prNumber: number;
157
+ /** Persisted PR timing and diff snapshots from pr_stats. */
158
+ openedAt: string | null;
159
+ mergedAt: string | null;
160
+ closedAt: string | null;
161
+ additions: number | null;
162
+ deletions: number | null;
163
+ openAdditions: number | null;
164
+ openDeletions: number | null;
124
165
  branch: string;
125
166
  position: number;
126
167
  seamState: string;
@@ -173,13 +214,53 @@ export type StackDto = {
173
214
  trunkBranch: string;
174
215
  /** Real trunk the review unit eventually lands into. */
175
216
  landTarget: string;
176
- autoPromoteWhenGreen: boolean;
177
217
  /**
178
218
  * Dashboard-only archive (#963): set hides the stack from the active Work
179
219
  * list; GitHub PRs stay open. Only PATCH /stacks/:id toggles it.
180
220
  */
181
221
  archivedAt: string | null;
222
+ /**
223
+ * Auto land (#1511): enqueue this unit-less single-PR stack when eligible.
224
+ * Missing on older payloads is off.
225
+ */
226
+ autoEnqueueWhenReady?: boolean;
182
227
  layers: StackLayerDto[];
183
228
  /** Present when a review-unit row exists for this stack. */
184
229
  unit?: StackUnitDto;
185
230
  };
231
+ export type MergeQueueEntryState = "queued" | "running" | "waiting" | "landed" | "bounced" | "cancelled";
232
+ export type MergeQueueEnqueuedBy = "human" | "agent";
233
+ export type MergeQueueEnqueuedVia = "dashboard" | "cli" | "chat" | "api" | "mcp";
234
+ export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "merge_failed" | "gh_error";
235
+ /** Structured bounce handoff — enough for the bar chip and the chat quote. */
236
+ export type MergeQueueBounceDetail = {
237
+ kind: MergeQueueBounceKind;
238
+ prNumber?: number;
239
+ headSha?: string;
240
+ failingCheck?: string;
241
+ conflictBranch?: string;
242
+ conflictDetail?: string;
243
+ message?: string;
244
+ };
245
+ export type MergeQueueEntryDto = {
246
+ id: string;
247
+ stackId: string;
248
+ owner: string;
249
+ repo: string;
250
+ state: MergeQueueEntryState;
251
+ /** 1-based FIFO among this user's live entries; `0` on bounced history rows (MQ-7). */
252
+ position: number;
253
+ waitReason: string | null;
254
+ bounceReason: string | null;
255
+ bounceDetail: MergeQueueBounceDetail | null;
256
+ enqueuedBy: MergeQueueEnqueuedBy;
257
+ enqueuedVia: MergeQueueEnqueuedVia;
258
+ enqueuedAt: string;
259
+ attempts: number;
260
+ landedPrNumbers: number[];
261
+ /** Combined PR head the queue verified. Null until the worker records it. */
262
+ verifyHeadSha: string | null;
263
+ /** Target tip included in the verified PR head. Null until recorded. */
264
+ verifyBaseSha: string | null;
265
+ finishedAt: string | null;
266
+ };
package/dist/ui/ansi.d.ts CHANGED
@@ -9,4 +9,6 @@ export declare const ansi: {
9
9
  brightGreen: (t: string) => string;
10
10
  invert: (t: string) => string;
11
11
  boldGreen: (t: string) => string;
12
+ /** Prompt / mark ink. Never 32 — that reads as sick dark green. */
13
+ boldBrightGreen: (t: string) => string;
12
14
  };
package/dist/ui/ansi.js CHANGED
@@ -24,4 +24,6 @@ export const ansi = {
24
24
  brightGreen: (t) => wrap("92", t),
25
25
  invert: (t) => wrap("7", t),
26
26
  boldGreen: (t) => (computeEnabled() ? `${t}` : t),
27
+ /** Prompt / mark ink. Never 32 — that reads as sick dark green. */
28
+ boldBrightGreen: (t) => (computeEnabled() ? `${t}` : t),
27
29
  };
@@ -1,4 +1,36 @@
1
+ import { type MeResponse } from "../api.js";
1
2
  /** Used by the non-interactive `usage()` path in cli.ts — the boxed banner is shell-only. */
2
3
  export declare function printWordmark(): void;
3
- export declare function printBannerHeader(): Promise<void>;
4
- export declare function shellPrompt(loggedIn: boolean): string;
4
+ /** Everything the welcome panel needs, captured once so redraws stay sync. */
5
+ export type BannerState = {
6
+ version: string;
7
+ me: MeResponse | null;
8
+ key: string | undefined;
9
+ keyInvalid: boolean;
10
+ };
11
+ /** Full welcome, mid pane, or Claude-style short chrome that still shows the mark. */
12
+ export type BannerDensity = "full" | "compact" | "mini" | "nano";
13
+ /** `true` = compact. Strings pick the short-pane densities. */
14
+ export type BannerVariant = boolean | "mini" | "nano";
15
+ /** Handle returned by {@link printBannerHeader}; `rows` reflows for a resize. */
16
+ export type BannerHandle = {
17
+ rows(columns?: number, variant?: BannerVariant): string[];
18
+ };
19
+ /**
20
+ * The whole welcome panel as rows, reflowed to `columns`. Every row fits in
21
+ * `columns - 1` cells so a redraw's cursor math stays honest; below
22
+ * MIN_BOXED_COLUMNS the frame drops away instead of wrapping.
23
+ */
24
+ export declare function buildBannerRows(state: BannerState, columns?: number, opts?: {
25
+ compact?: boolean;
26
+ density?: BannerDensity;
27
+ }): string[];
28
+ /**
29
+ * Print the welcome panel and return a handle whose `rows()` reflows the
30
+ * same data for a new width (used by the prompt's resize redraw).
31
+ */
32
+ export declare function printBannerHeader(opts?: {
33
+ /** When false, only build the handle; the prompt paints the rows. */
34
+ paint?: boolean;
35
+ }): Promise<BannerHandle>;
36
+ export declare function shellPrompt(_loggedIn: boolean): string;