mergestorm 0.3.9 → 0.3.13

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 (43) hide show
  1. package/README.md +57 -13
  2. package/dist/api.d.ts +27 -38
  3. package/dist/api.js +81 -4
  4. package/dist/client/index.d.ts +5 -0
  5. package/dist/client/index.js +5 -0
  6. package/dist/commands/credits.js +33 -12
  7. package/dist/commands/login.js +28 -7
  8. package/dist/commands/registry.d.ts +1 -1
  9. package/dist/commands/registry.js +26 -19
  10. package/dist/commands/review-client.d.ts +93 -0
  11. package/dist/commands/review-client.js +289 -0
  12. package/dist/commands/review.d.ts +30 -6
  13. package/dist/commands/review.js +385 -146
  14. package/dist/commands/stack.d.ts +34 -2
  15. package/dist/commands/stack.js +252 -58
  16. package/dist/commands/status.d.ts +16 -2
  17. package/dist/commands/status.js +221 -21
  18. package/dist/errors.d.ts +18 -2
  19. package/dist/errors.js +22 -1
  20. package/dist/git-stack.d.ts +2 -0
  21. package/dist/git-stack.js +4 -0
  22. package/dist/git.d.ts +12 -1
  23. package/dist/git.js +52 -7
  24. package/dist/shell.d.ts +1 -1
  25. package/dist/shell.js +6 -3
  26. package/dist/stack-dto.d.ts +185 -0
  27. package/dist/stack-dto.js +7 -0
  28. package/dist/submit-pr-base.d.ts +31 -0
  29. package/dist/submit-pr-base.js +69 -0
  30. package/dist/ui/banner.js +7 -0
  31. package/dist/ui/box.d.ts +13 -9
  32. package/dist/ui/box.js +28 -44
  33. package/dist/ui/findings.d.ts +2 -0
  34. package/dist/ui/findings.js +2 -1
  35. package/dist/ui/job-envelope.d.ts +68 -0
  36. package/dist/ui/job-envelope.js +35 -0
  37. package/dist/ui/prompt.d.ts +91 -0
  38. package/dist/ui/prompt.js +241 -62
  39. package/dist/ui/usage.d.ts +25 -1
  40. package/dist/ui/usage.js +50 -1
  41. package/dist/ui/width.d.ts +26 -0
  42. package/dist/ui/width.js +262 -0
  43. package/package.json +5 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * GitHub PR base for `stack submit` (#1104).
3
+ *
4
+ * Git parent stays the previous layer (local rebase / `--onto`). Position 3+
5
+ * must open onto the review-unit park freeze so adopt does not retarget after
6
+ * `opened` and double-wake Vortex.
7
+ */
8
+ import type { StackDto } from "./api.js";
9
+ export declare const MG_PARK_PREFIX = "mg-park-";
10
+ export declare function isMgParkBranch(branch: string): boolean;
11
+ /** Live freeze already used as a GitHub base on this stack, if any. */
12
+ export declare function parkFreezeBranchFromStack(stack: StackDto): string | null;
13
+ export type SubmitLayerPlan = {
14
+ branch: string;
15
+ gitParent: string;
16
+ position: number;
17
+ /** GitHub `--base` when known. `null` means mint the freeze first. */
18
+ githubBase: string | null;
19
+ needsPark: boolean;
20
+ stackId: string | null;
21
+ };
22
+ export declare function planSubmitLayerBases(input: {
23
+ layers: readonly {
24
+ branch: string;
25
+ parentBranch: string;
26
+ }[];
27
+ trunk: string;
28
+ registered: readonly StackDto[];
29
+ owner: string;
30
+ repo: string;
31
+ }): SubmitLayerPlan[];
@@ -0,0 +1,69 @@
1
+ function registeredParentHit(stacks, parentBranch, owner, repo) {
2
+ const want = parentBranch.trim();
3
+ if (!want)
4
+ return null;
5
+ for (const stack of stacks) {
6
+ if (stack.owner !== owner || stack.repo !== repo)
7
+ continue;
8
+ for (const layer of stack.layers) {
9
+ if (layer.branch === want) {
10
+ return { stackId: stack.id, position: layer.position };
11
+ }
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ export const MG_PARK_PREFIX = "mg-park-";
17
+ export function isMgParkBranch(branch) {
18
+ return branch.trim().toLowerCase().startsWith(MG_PARK_PREFIX);
19
+ }
20
+ /** Live freeze already used as a GitHub base on this stack, if any. */
21
+ export function parkFreezeBranchFromStack(stack) {
22
+ for (const layer of stack.layers) {
23
+ const parent = layer.parentBranch?.trim() ?? "";
24
+ if (isMgParkBranch(parent))
25
+ return parent;
26
+ }
27
+ return null;
28
+ }
29
+ export function planSubmitLayerBases(input) {
30
+ const { layers, trunk, registered, owner, repo } = input;
31
+ const localIndex = new Map(layers.map((layer, i) => [layer.branch, i]));
32
+ const plans = [];
33
+ for (let i = 0; i < layers.length; i++) {
34
+ const layer = layers[i];
35
+ const gitParent = layer.parentBranch.trim() || trunk;
36
+ const priorLocal = localIndex.get(gitParent);
37
+ let position;
38
+ let stackId = null;
39
+ if (priorLocal !== undefined && plans[priorLocal]) {
40
+ position = plans[priorLocal].position + 1;
41
+ stackId = plans[priorLocal].stackId;
42
+ }
43
+ else {
44
+ const hit = registeredParentHit(registered, gitParent, owner, repo);
45
+ if (hit) {
46
+ position = hit.position + 1;
47
+ stackId = hit.stackId;
48
+ }
49
+ else {
50
+ position = 1;
51
+ }
52
+ }
53
+ const stack = stackId
54
+ ? registered.find((row) => row.id === stackId) ?? null
55
+ : null;
56
+ const existingPark = stack ? parkFreezeBranchFromStack(stack) : null;
57
+ const needsPark = position >= 3;
58
+ const githubBase = needsPark ? existingPark : gitParent;
59
+ plans.push({
60
+ branch: layer.branch,
61
+ gitParent,
62
+ position,
63
+ githubBase,
64
+ needsPark,
65
+ stackId,
66
+ });
67
+ }
68
+ return plans;
69
+ }
package/dist/ui/banner.js CHANGED
@@ -66,6 +66,13 @@ const WHATS_NEW = {
66
66
  "npm description points at MIT build-from-source URL",
67
67
  "Dashboard links → Settings → API / Work (product URL cleanup)",
68
68
  ],
69
+ "0.3.10": [
70
+ "README: verify public git SHA + npm integrity (build from mergestorm-cli)",
71
+ ],
72
+ "0.3.12": [
73
+ "review --router off|standard|max|manual (same smart router as Agents)",
74
+ "review --specialists security,frontend,… to pin or invoke lanes",
75
+ ],
69
76
  };
70
77
  function formatStatusLines(me, key, barWidth, keyInvalid = false) {
71
78
  if (!key) {
package/dist/ui/box.d.ts CHANGED
@@ -1,24 +1,30 @@
1
- export declare function visibleWidth(str: string): number;
1
+ import { padVisible, sliceVisible, visibleWidth } from "./width.js";
2
+ export { padVisible, sliceVisible, visibleWidth };
2
3
  /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
3
4
  export declare const MIN_BOXED_COLUMNS = 48;
4
- /** Usable terminal columns (TTY), with a sensible fallback for pipes/tests. */
5
- export declare function terminalColumns(fallback?: number): number;
5
+ /**
6
+ * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
7
+ * Pass `columns` to measure a stream other than process.stdout (or a
8
+ * post-resize value); `undefined` reads stdout at call time.
9
+ */
10
+ export declare function terminalColumns(fallback?: number, columns?: number): number;
6
11
  /**
7
12
  * Whether rounded/full-bleed boxes fit without wrapping.
8
13
  * Non-TTY (unknown columns) keeps boxes for tests and CI logs.
9
14
  */
10
- export declare function preferBoxedUi(): boolean;
15
+ export declare function preferBoxedUi(columns?: number): boolean;
11
16
  /**
12
17
  * Outer box width for Claude-style full-bleed frames.
13
18
  * Never wider than the terminal (minus 1 so the right border doesn't wrap).
14
19
  */
15
- export declare function frameWidth(fallback?: number): number;
20
+ export declare function frameWidth(fallback?: number, columns?: number): number;
16
21
  export interface RoundedBoxOptions {
17
22
  title?: string;
18
23
  padding?: number;
19
24
  /**
20
- * Exact outer width (including borders). When set, content is padded to fill.
21
- * Prefer this for full-width Claude-style frames.
25
+ * Exact outer width (including borders). When set, content is padded to
26
+ * fill and *clipped* to fit: no row is ever wider than this, because a row
27
+ * that wraps is a row the redraw math doesn't know about.
22
28
  */
23
29
  width?: number;
24
30
  /** Grow to at least this outer width (content may still be wider). */
@@ -29,8 +35,6 @@ export interface RoundedBoxOptions {
29
35
  * `minWidth` to stretch to the terminal like Claude Code.
30
36
  */
31
37
  export declare function roundedBox(lines: string[], opts?: RoundedBoxOptions): string[];
32
- /** Pad/truncate a line to an exact visible width (ANSI-safe). */
33
- export declare function padVisible(line: string, width: number): string;
34
38
  /**
35
39
  * Join left/right columns into rows of equal left-column width.
36
40
  * Extra rows on either side are blank-padded.
package/dist/ui/box.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import { stdout } from "node:process";
2
2
  import { ansi } from "./ansi.js";
3
- const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
4
- export function visibleWidth(str) {
5
- return str.replace(ANSI_PATTERN, "").length;
6
- }
3
+ import { padVisible, sliceVisible, visibleWidth } from "./width.js";
4
+ // Cell arithmetic lives in width.ts; re-exported so existing `./box.js`
5
+ // importers (banner, prompt) keep working.
6
+ export { padVisible, sliceVisible, visibleWidth };
7
7
  /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
8
8
  export const MIN_BOXED_COLUMNS = 48;
9
- /** Usable terminal columns (TTY), with a sensible fallback for pipes/tests. */
10
- export function terminalColumns(fallback = 80) {
11
- const cols = stdout.columns;
9
+ /**
10
+ * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
11
+ * Pass `columns` to measure a stream other than process.stdout (or a
12
+ * post-resize value); `undefined` reads stdout at call time.
13
+ */
14
+ export function terminalColumns(fallback = 80, columns) {
15
+ const cols = columns ?? stdout.columns;
12
16
  if (typeof cols === "number" && Number.isFinite(cols) && cols >= 1) {
13
17
  return cols;
14
18
  }
@@ -18,8 +22,8 @@ export function terminalColumns(fallback = 80) {
18
22
  * Whether rounded/full-bleed boxes fit without wrapping.
19
23
  * Non-TTY (unknown columns) keeps boxes for tests and CI logs.
20
24
  */
21
- export function preferBoxedUi() {
22
- const cols = stdout.columns;
25
+ export function preferBoxedUi(columns) {
26
+ const cols = columns ?? stdout.columns;
23
27
  if (typeof cols !== "number" || !Number.isFinite(cols))
24
28
  return true;
25
29
  return cols >= MIN_BOXED_COLUMNS;
@@ -28,8 +32,8 @@ export function preferBoxedUi() {
28
32
  * Outer box width for Claude-style full-bleed frames.
29
33
  * Never wider than the terminal (minus 1 so the right border doesn't wrap).
30
34
  */
31
- export function frameWidth(fallback = 80) {
32
- return Math.max(20, terminalColumns(fallback) - 1);
35
+ export function frameWidth(fallback = 80, columns) {
36
+ return Math.max(20, terminalColumns(fallback, columns) - 1);
33
37
  }
34
38
  const ROUNDED = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
35
39
  const ASCII = { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
@@ -49,12 +53,17 @@ function centerLabel(label, width, fill) {
49
53
  export function roundedBox(lines, opts = {}) {
50
54
  const padding = Math.max(0, opts.padding ?? 1);
51
55
  const chars = boxChars();
52
- const title = opts.title ? ` ${opts.title} ` : "";
56
+ let title = opts.title ? ` ${opts.title} ` : "";
53
57
  const contentInner = Math.max(0, ...lines.map(visibleWidth), visibleWidth(title)) + padding * 2;
54
58
  let innerWidth = contentInner;
59
+ let body = lines;
55
60
  if (opts.width !== undefined) {
56
- // outer = inner + 2 border cols
57
- innerWidth = Math.max(contentInner, opts.width - 2);
61
+ // outer = inner + 2 border cols; clamp, never grow.
62
+ innerWidth = Math.max(0, opts.width - 2);
63
+ const contentCells = Math.max(0, innerWidth - padding * 2);
64
+ body = lines.map((line) => visibleWidth(line) > contentCells ? padVisible(line, contentCells) : line);
65
+ if (visibleWidth(title) > innerWidth)
66
+ title = sliceVisible(title, 0, innerWidth);
58
67
  }
59
68
  else if (opts.minWidth !== undefined) {
60
69
  innerWidth = Math.max(contentInner, opts.minWidth - 2);
@@ -63,37 +72,12 @@ export function roundedBox(lines, opts = {}) {
63
72
  ? `${chars.tl}${centerLabel(title, innerWidth, chars.h)}${chars.tr}`
64
73
  : `${chars.tl}${chars.h.repeat(innerWidth)}${chars.tr}`);
65
74
  const bottom = ansi.gray(`${chars.bl}${chars.h.repeat(innerWidth)}${chars.br}`);
66
- const body = lines.map((line) => {
67
- const gap = Math.max(0, innerWidth - padding - visibleWidth(line));
68
- return `${ansi.gray(chars.v)}${" ".repeat(padding)}${line}${" ".repeat(gap)}${ansi.gray(chars.v)}`;
75
+ const leftPad = Math.min(padding, innerWidth);
76
+ const rows = body.map((line) => {
77
+ const gap = Math.max(0, innerWidth - leftPad - visibleWidth(line));
78
+ return `${ansi.gray(chars.v)}${" ".repeat(leftPad)}${line}${" ".repeat(gap)}${ansi.gray(chars.v)}`;
69
79
  });
70
- return [top, ...body, bottom];
71
- }
72
- /** Pad/truncate a line to an exact visible width (ANSI-safe). */
73
- export function padVisible(line, width) {
74
- const w = visibleWidth(line);
75
- if (w === width)
76
- return line;
77
- if (w < width)
78
- return `${line}${" ".repeat(width - w)}`;
79
- // Truncate while preserving ANSI codes: walk the string counting only
80
- // visible characters, then slice the original at that position.
81
- let visible = 0;
82
- let i = 0;
83
- const re = /\u001b\[[0-9;]*m/g;
84
- while (visible < width && i < line.length) {
85
- re.lastIndex = i;
86
- const m = re.exec(line);
87
- if (m && m.index === i) {
88
- i = re.lastIndex;
89
- }
90
- else {
91
- const code = line.charCodeAt(i);
92
- i += code >= 0xd800 && code <= 0xdbff ? 2 : 1;
93
- visible++;
94
- }
95
- }
96
- return line.slice(0, i);
80
+ return [top, ...rows, bottom];
97
81
  }
98
82
  /**
99
83
  * Join left/right columns into rows of equal left-column width.
@@ -1,11 +1,13 @@
1
1
  /** Shared review-job findings payload (API may send snake or camel for off-diff). */
2
2
  export type ReviewFindings = {
3
+ specialists_run?: string[];
3
4
  inline?: {
4
5
  path: string;
5
6
  line: number;
6
7
  severity: string;
7
8
  body: string;
8
9
  title?: string;
10
+ specialist?: string;
9
11
  }[];
10
12
  off_diff?: {
11
13
  path?: string;
@@ -3,7 +3,8 @@ export function renderFindings(findings, opts = {}) {
3
3
  const inline = findings?.inline ?? [];
4
4
  const off = findings?.off_diff ?? findings?.offDiff ?? [];
5
5
  for (const c of inline) {
6
- console.log(`\n[${c.severity}] ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
6
+ const lane = c.specialist ? `[${c.specialist}]` : "";
7
+ console.log(`\n[${c.severity}]${lane} ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
7
8
  console.log(c.body);
8
9
  }
9
10
  for (const c of off) {
@@ -0,0 +1,68 @@
1
+ import type { ReviewFindings } from "./findings.js";
2
+ export declare const REVIEW_JOB_ENVELOPE_SCHEMA: "mergestorm.review_job/v1";
3
+ export type ReviewJobRow = {
4
+ job_id?: string | null;
5
+ id?: string | null;
6
+ status?: string | null;
7
+ thread_slug?: string | null;
8
+ thread_job_number?: number | null;
9
+ base_label?: string | null;
10
+ head_label?: string | null;
11
+ router_mode?: string | null;
12
+ specialists_requested?: string[] | null;
13
+ specialists_run?: string[] | null;
14
+ verdict?: string | null;
15
+ summary?: string | null;
16
+ findings?: ReviewFindings | null;
17
+ truncated?: boolean | null;
18
+ error?: string | null;
19
+ credits?: {
20
+ standard: number;
21
+ } | null;
22
+ webhook_secret?: string | null;
23
+ created_at?: string | null;
24
+ finished_at?: string | null;
25
+ retry_after_seconds?: number | null;
26
+ };
27
+ export type ReviewJobEnvelope = {
28
+ schema: typeof REVIEW_JOB_ENVELOPE_SCHEMA;
29
+ job_id: string | null;
30
+ status: string;
31
+ thread: {
32
+ slug: string;
33
+ job_number: number | null;
34
+ } | null;
35
+ base_label: string | null;
36
+ head_label: string | null;
37
+ router_mode: string | null;
38
+ specialists_requested: string[];
39
+ specialists_run: string[];
40
+ verdict: string | null;
41
+ summary: string | null;
42
+ findings: ReviewFindings | null;
43
+ truncated: boolean;
44
+ error: string | null;
45
+ credits: {
46
+ standard: number;
47
+ } | null;
48
+ webhook_secret: string | null;
49
+ created_at: string | null;
50
+ finished_at: string | null;
51
+ retry_after_seconds?: number;
52
+ };
53
+ export type ReviewEnvelopeFallbacks = {
54
+ jobId?: string | null;
55
+ status?: string;
56
+ threadSlug?: string | null;
57
+ baseLabel?: string | null;
58
+ headLabel?: string | null;
59
+ routerMode?: string | null;
60
+ specialistsRequested?: string[];
61
+ error?: string | null;
62
+ retryAfterSeconds?: number | null;
63
+ };
64
+ /**
65
+ * Stable machine envelope shared by submit and terminal review output.
66
+ * Fields added by the API-envelope work are nullable until that API lands.
67
+ */
68
+ export declare function toReviewJobEnvelope(row: ReviewJobRow | null | undefined, fallback?: ReviewEnvelopeFallbacks): ReviewJobEnvelope;
@@ -0,0 +1,35 @@
1
+ export const REVIEW_JOB_ENVELOPE_SCHEMA = "mergestorm.review_job/v1";
2
+ /**
3
+ * Stable machine envelope shared by submit and terminal review output.
4
+ * Fields added by the API-envelope work are nullable until that API lands.
5
+ */
6
+ export function toReviewJobEnvelope(row, fallback = {}) {
7
+ const slug = row?.thread_slug ?? fallback.threadSlug ?? null;
8
+ const findings = row?.findings ?? null;
9
+ const retryAfter = fallback.retryAfterSeconds ?? row?.retry_after_seconds ?? undefined;
10
+ return {
11
+ schema: REVIEW_JOB_ENVELOPE_SCHEMA,
12
+ job_id: row?.job_id ?? row?.id ?? fallback.jobId ?? null,
13
+ status: row?.status ?? fallback.status ?? "failed",
14
+ thread: slug
15
+ ? { slug, job_number: row?.thread_job_number ?? null }
16
+ : null,
17
+ base_label: row?.base_label ?? fallback.baseLabel ?? null,
18
+ head_label: row?.head_label ?? fallback.headLabel ?? null,
19
+ router_mode: row?.router_mode ?? fallback.routerMode ?? null,
20
+ specialists_requested: row?.specialists_requested ?? fallback.specialistsRequested ?? [],
21
+ specialists_run: row?.specialists_run ?? findings?.specialists_run ?? [],
22
+ verdict: row?.verdict ?? null,
23
+ summary: row?.summary ?? null,
24
+ findings,
25
+ truncated: row?.truncated ?? false,
26
+ error: fallback.error !== undefined ? fallback.error : (row?.error ?? null),
27
+ credits: row?.credits ?? null,
28
+ webhook_secret: row?.webhook_secret ?? null,
29
+ created_at: row?.created_at ?? null,
30
+ finished_at: row?.finished_at ?? null,
31
+ ...(retryAfter != null && Number.isFinite(retryAfter)
32
+ ? { retry_after_seconds: retryAfter }
33
+ : {}),
34
+ };
35
+ }
@@ -1,16 +1,107 @@
1
1
  export interface CommandSpec {
2
2
  name: string;
3
+ aliases?: string[];
3
4
  summary: string;
4
5
  }
5
6
  export interface AskLineOptions {
6
7
  prompt?: string;
7
8
  history?: string[];
8
9
  commands?: CommandSpec[];
10
+ /** Terminal rows; defaults to output.rows. Caps the slash viewport. */
11
+ rows?: number;
12
+ /**
13
+ * Terminal columns; defaults to output.columns, re-read on every render so
14
+ * a resize mid-prompt reflows the frame instead of smearing it.
15
+ */
16
+ columns?: number;
17
+ /** Streams (defaults: process.stdin / process.stdout). Injectable for tests. */
18
+ input?: NodeJS.ReadStream;
19
+ output?: NodeJS.WriteStream;
9
20
  }
10
21
  /** Thrown when the user asks to close the prompt (Ctrl+D empty, or Ctrl+C twice). */
11
22
  export declare class PromptClosedError extends Error {
12
23
  constructor();
13
24
  }
25
+ /**
26
+ * Redraw prefix from the parked content-row cursor.
27
+ * CUU keeps the column, so CR must come before ED 0 or leftover
28
+ * prompt cells survive (`merg` + `mergestorm` -> `mergmmmmmergestorm`).
29
+ */
30
+ export declare function promptRedrawPrefix(inputRow: number): string;
31
+ /**
32
+ * From the content row, move to the first row after the input chrome
33
+ * and erase the slash overlay. Boxed chrome is top + content + bottom
34
+ * (`down(2)`). Plain chrome is the content line (`down(1)`).
35
+ */
36
+ export declare function promptClearOverlay(boxed: boolean): string;
37
+ export declare const DROPDOWN_MAX_COMMAND_ROWS = 8;
38
+ export declare function formatSlashLabel(spec: CommandSpec): string;
39
+ export declare function specMatchesTerm(spec: CommandSpec, term: string): boolean;
40
+ export declare function slashToken(buffer: string): string | null;
41
+ export declare function matchSlashCommands(buffer: string, specs: CommandSpec[], dismissed?: boolean): CommandSpec[];
42
+ export declare function isExactSlashCommand(buffer: string, specs: CommandSpec[]): boolean;
43
+ export declare function dropdownMaxRows(terminalRows: number | undefined, inputRow: number): number;
44
+ export declare function dropdownViewport(count: number, index: number, maxRows: number): {
45
+ start: number;
46
+ moreAbove: number;
47
+ moreBelow: number;
48
+ };
49
+ export type OverlayRow = {
50
+ text: string;
51
+ selected?: boolean;
52
+ };
53
+ /** Overlay rows (no invert). At most `maxRows` commands plus edge markers. */
54
+ export declare function dropdownOverlayRows(dropdown: CommandSpec[], index: number, maxRows: number): OverlayRow[];
55
+ /** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
56
+ export declare const INPUT_PREFIX_GLYPH = "\u203A";
57
+ /** Cells the `> ` prefix occupies before the buffer. */
58
+ export declare const INPUT_PREFIX_CELLS = 2;
59
+ /** Padding inside the boxed input frame. */
60
+ export declare const INPUT_BOX_PADDING = 1;
61
+ export interface InputWindow {
62
+ /** First buffer cell shown. */
63
+ start: number;
64
+ /** The visible slice of the buffer (ANSI-free, at most `cells` wide). */
65
+ text: string;
66
+ /** Cursor column within the window, 0-based. */
67
+ cursorCol: number;
68
+ }
69
+ /**
70
+ * Horizontal scroll for a one-row editor: pick the `cells`-wide window of
71
+ * `buffer` that keeps the cursor on screen, moving the previous window only
72
+ * when the cursor leaves it. Never wraps onto a second physical row.
73
+ */
74
+ export declare function inputWindow(buffer: string, cursor: number, cells: number, prevStart?: number): InputWindow;
75
+ /** Cells available to the buffer (after the prefix) on a content row. */
76
+ export declare function inputCells(columns: number | undefined): {
77
+ boxed: boolean;
78
+ cells: number;
79
+ };
80
+ export interface PromptFrameInput {
81
+ prompt?: string;
82
+ buffer: string;
83
+ cursor: number;
84
+ /** Terminal columns at draw time; undefined = unknown (boxed at 80). */
85
+ columns?: number;
86
+ /** Previous scroll origin, so left/right only move the window at the edges. */
87
+ scrollStart?: number;
88
+ overlay?: OverlayRow[];
89
+ }
90
+ export interface PromptFrame {
91
+ rows: string[];
92
+ boxed: boolean;
93
+ /** Row (0-based) of the content line within `rows`. */
94
+ inputRow: number;
95
+ /** 1-based column to park the cursor on the content row. */
96
+ targetCol: number;
97
+ scrollStart: number;
98
+ }
99
+ /**
100
+ * Everything one prompt redraw writes, computed from the current terminal
101
+ * width. Every row fits in `columns - 1` cells so nothing wraps and the
102
+ * CUU count on the next redraw stays honest.
103
+ */
104
+ export declare function buildPromptFrame(params: PromptFrameInput): PromptFrame;
14
105
  /**
15
106
  * Hand-rolled raw-mode line editor: bordered input box, history, and a live
16
107
  * slash-command dropdown. Falls back to plain readline on a non-TTY stream.