@yagni-app/code-staging 0.2.1-staging.1038.1 → 0.2.1-staging.1039.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -12,6 +12,27 @@
12
12
  * plus a configured pi spawn. Everything that makes this "YAGNI Code" lives in
13
13
  * pi-extension-yagni and the YAGNI backend.
14
14
  */
15
+ /**
16
+ * Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
17
+ * input is padded to match the chat/output area (`outputPad`) and the status
18
+ * bar. Fills the key in only when absent — an explicit user choice (including
19
+ * 0) is never overwritten. Best-effort: a missing/corrupt settings file or a
20
+ * write failure must never block the launch.
21
+ */
22
+ /**
23
+ * Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
24
+ * input aligns with the chat/output area (`outputPad`) and the status bar.
25
+ *
26
+ * This writes to a user-owned file, so it is deliberately conservative:
27
+ * - fills the key in ONLY when absent — an explicit user choice (including 0)
28
+ * is never overwritten;
29
+ * - backs off on a missing-dir, corrupt, non-object, or symlinked settings
30
+ * file rather than risk clobbering anything;
31
+ * - writes ATOMICALLY (temp file + rename) and preserves the existing file's
32
+ * permissions, so a crash mid-write can never truncate the user's settings.
33
+ * Any failure is swallowed: seeding must never block or break a launch.
34
+ */
35
+ export declare function seedEditorPadding(piAgentDir: string): void;
15
36
  export declare const HELP_TEXT: string;
16
37
  /** Parse `use <name> [--base-url <url>]` argv into its parts. */
17
38
  export declare function parseUseArgs(args: string[]): {
package/dist/cli.js CHANGED
@@ -13,7 +13,8 @@
13
13
  * pi-extension-yagni and the YAGNI backend.
14
14
  */
15
15
  import { spawn } from "node:child_process";
16
- import { mkdirSync, realpathSync } from "node:fs";
16
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
17
+ import { join } from "node:path";
17
18
  import { createInterface } from "node:readline/promises";
18
19
  import { fileURLToPath } from "node:url";
19
20
  import { PI_CONFIG_NAME } from "./branding.js";
@@ -27,6 +28,7 @@ import { runDoctor } from "./doctor.js";
27
28
  import { installProcessCrashHandlers } from "./crashReport.js";
28
29
  import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
29
30
  import { maybeRefreshAtLaunch } from "./refresh.js";
31
+ import { PAD_X } from "./padding.js";
30
32
  import { ensureShadowPiPackage } from "./piPackage.js";
31
33
  import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
32
34
  import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
@@ -43,6 +45,72 @@ async function confirmOnTty(question) {
43
45
  rl.close();
44
46
  }
45
47
  }
48
+ /**
49
+ * Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
50
+ * input is padded to match the chat/output area (`outputPad`) and the status
51
+ * bar. Fills the key in only when absent — an explicit user choice (including
52
+ * 0) is never overwritten. Best-effort: a missing/corrupt settings file or a
53
+ * write failure must never block the launch.
54
+ */
55
+ /**
56
+ * Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
57
+ * input aligns with the chat/output area (`outputPad`) and the status bar.
58
+ *
59
+ * This writes to a user-owned file, so it is deliberately conservative:
60
+ * - fills the key in ONLY when absent — an explicit user choice (including 0)
61
+ * is never overwritten;
62
+ * - backs off on a missing-dir, corrupt, non-object, or symlinked settings
63
+ * file rather than risk clobbering anything;
64
+ * - writes ATOMICALLY (temp file + rename) and preserves the existing file's
65
+ * permissions, so a crash mid-write can never truncate the user's settings.
66
+ * Any failure is swallowed: seeding must never block or break a launch.
67
+ */
68
+ export function seedEditorPadding(piAgentDir) {
69
+ try {
70
+ const settingsPath = join(piAgentDir, "settings.json");
71
+ let settings = {};
72
+ let existingMode;
73
+ if (existsSync(settingsPath)) {
74
+ // Refuse to follow a symlink: we must only ever write a regular, real
75
+ // settings file the user (or pi) owns.
76
+ if (lstatSync(settingsPath).isSymbolicLink())
77
+ return;
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
81
+ }
82
+ catch {
83
+ return; // corrupt file: back off rather than clobber the user's settings
84
+ }
85
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
86
+ return; // non-object settings: leave it alone
87
+ }
88
+ settings = parsed;
89
+ existingMode = lstatSync(settingsPath).mode & 0o777;
90
+ }
91
+ if (settings.editorPaddingX !== undefined)
92
+ return; // user already chose
93
+ settings.editorPaddingX = PAD_X;
94
+ // Atomic write: serialize to a temp file in the same directory, then
95
+ // rename over the target. A crash leaves either the old file or the temp
96
+ // file, never a half-written settings.json.
97
+ const tmpPath = join(piAgentDir, `.settings.json.yagni-${process.pid}-${Date.now()}.tmp`);
98
+ try {
99
+ writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
100
+ renameSync(tmpPath, settingsPath);
101
+ }
102
+ finally {
103
+ rmSync(tmpPath, { force: true }); // no-op once the rename succeeded
104
+ }
105
+ // Restore the original permissions if the file already had them (rename
106
+ // replaces the inode, which would otherwise reset to 0600).
107
+ if (existingMode !== undefined)
108
+ chmodSync(settingsPath, existingMode);
109
+ }
110
+ catch {
111
+ // never block launch on a settings-seed failure
112
+ }
113
+ }
46
114
  async function runDefault(passthroughArgs) {
47
115
  // Cache-backed update nudge (never a network wait), then a background cache
48
116
  // refresh that completes while the session runs. Both fail soft.
@@ -72,6 +140,11 @@ async function runDefault(passthroughArgs) {
72
140
  // instead of ~/.pi/agent, and prod state never bleeds into staging.
73
141
  const piAgentDir = agentDir(profile.name);
74
142
  mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
143
+ // Seed the editor's horizontal padding so the prompt input aligns with the
144
+ // chat/output area and the status bar. Only fills in when the user has not
145
+ // set it themselves — an explicit choice is never clobbered. Best-effort: a
146
+ // corrupt or unreadable settings.json must never block the launch.
147
+ seedEditorPadding(piAgentDir);
75
148
  // Generate the shadow pi package so the terminal title/process name read
76
149
  // "YAGNI Code" instead of "pi"/"π". Best-effort: if it can't be built we
77
150
  // still launch (un-rebranded but hermetic), never blocking the agent.
@@ -40,6 +40,14 @@
40
40
  */
41
41
  import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
42
42
  export declare const BRANCH_MAX_WIDTH = 60;
43
+ /**
44
+ * Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
45
+ * aligns with the editor input and the chat/output area on one shared column.
46
+ * The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
47
+ * read pi settings, so the value crosses over env. Falls back to the output
48
+ * area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
49
+ */
50
+ export declare function resolveFooterPadX(raw?: string | undefined): number;
43
51
  /** Format token counts for compact footer display (mirrors pi's formatTokens). */
44
52
  export declare function formatTokens(count: number): string;
45
53
  /** Shorten cwd relative to home, like pi's built-in footer. */
@@ -85,7 +93,7 @@ export declare function renderFooterLines(input: {
85
93
  usage: UsageTotals;
86
94
  contextPercent: number | null;
87
95
  statuses: string[];
88
- }, theme: Pick<Theme, "fg">, width: number): string[];
96
+ }, theme: Pick<Theme, "fg">, width: number, padX?: number): string[];
89
97
  /**
90
98
  * Create a footer factory that captures the session `ctx` (for session data)
91
99
  * and returns the component `setFooter` expects. Called from the
@@ -46,6 +46,19 @@ export const BRANCH_MAX_WIDTH = 60;
46
46
  const WORKTREE_MAX_WIDTH = 30;
47
47
  /** Section separator: single space + middle dot + single space. */
48
48
  const SEP = " · ";
49
+ /** Default horizontal pad when the launcher didn't forward one (matches outputPad=1). */
50
+ const DEFAULT_PAD_X = 1;
51
+ /**
52
+ * Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
53
+ * aligns with the editor input and the chat/output area on one shared column.
54
+ * The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
55
+ * read pi settings, so the value crosses over env. Falls back to the output
56
+ * area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
57
+ */
58
+ export function resolveFooterPadX(raw = process.env.YAGNI_PAD_X) {
59
+ const n = raw === undefined ? NaN : Number.parseInt(raw, 10);
60
+ return Number.isFinite(n) && n >= 0 && n <= 3 ? n : DEFAULT_PAD_X;
61
+ }
49
62
  /** Format token counts for compact footer display (mirrors pi's formatTokens). */
50
63
  export function formatTokens(count) {
51
64
  if (count < 1000)
@@ -191,9 +204,14 @@ function contextColor(percent) {
191
204
  return "dim";
192
205
  }
193
206
  /** Pure line-builder, exported for tests. All data injected; colors via theme. */
194
- export function renderFooterLines(input, theme, width) {
207
+ export function renderFooterLines(input, theme, width, padX = 0) {
195
208
  const dim = (s) => theme.fg("dim", s);
196
209
  const sep = dim(SEP);
210
+ // Reserve the left pad so the status bar's text starts on the same column as
211
+ // the (padded) editor input and the chat/output area, instead of hugging the
212
+ // terminal edge. Truncation runs against the reduced content width.
213
+ const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
214
+ const contentWidth = Math.max(1, width - pad.length);
197
215
  // Line 1: folder · [worktree] · branch
198
216
  const line1Parts = [theme.fg("accent", input.git.folder)];
199
217
  if (input.git.inRepo) {
@@ -202,7 +220,7 @@ export function renderFooterLines(input, theme, width) {
202
220
  if (input.git.branch)
203
221
  line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH)));
204
222
  }
205
- const line1 = truncateToWidth(line1Parts.join(sep), width, dim("…"));
223
+ const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
206
224
  // Line 2: model · ↑in ↓out $cost · ctx%
207
225
  const statParts = [];
208
226
  if (input.usage.input)
@@ -217,12 +235,12 @@ export function renderFooterLines(input, theme, width) {
217
235
  if (stats)
218
236
  line2Parts.push(dim(stats));
219
237
  line2Parts.push(theme.fg(contextColor(input.contextPercent), percentText));
220
- const line2 = truncateToWidth(line2Parts.join(sep), width, dim("…"));
238
+ const line2 = pad + truncateToWidth(line2Parts.join(sep), contentWidth, dim("…"));
221
239
  // Line 3: extension statuses (branding, todo counter, mode), joined by " · ".
222
240
  const statuses = input.statuses.map((s) => s.replace(/[\r\n\t]/g, " ").trim()).filter(Boolean);
223
241
  const lines = [line1, line2];
224
242
  if (statuses.length > 0) {
225
- lines.push(truncateToWidth(dim(statuses.join(SEP)), width, dim("…")));
243
+ lines.push(pad + truncateToWidth(dim(statuses.join(SEP)), contentWidth, dim("…")));
226
244
  }
227
245
  return lines;
228
246
  }
@@ -258,7 +276,7 @@ export function createYagniFooterFactory(ctx) {
258
276
  usage: collectUsage(ctx.sessionManager),
259
277
  contextPercent: ctx.getContextUsage()?.percent ?? null,
260
278
  statuses,
261
- }, theme, width);
279
+ }, theme, width, resolveFooterPadX());
262
280
  },
263
281
  invalidate() {
264
282
  gitCache = undefined;
package/dist/launch.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { agentDirEnvVar } from "./branding.js";
10
+ import { PAD_X_ENV, resolvePadX } from "./padding.js";
10
11
  /**
11
12
  * How close to expiry the token can be before launch warns. A coding session
12
13
  * easily outlives a token, so warn early enough that re-logging in before a long
@@ -95,6 +96,10 @@ export function buildLaunch(creds, passthroughArgs, opts) {
95
96
  // Match the e2b harness defaults: skip pi's update check + telemetry.
96
97
  PI_SKIP_VERSION_CHECK: "1",
97
98
  PI_TELEMETRY: "0",
99
+ // Forward the chosen horizontal pad so the extension's footer aligns with
100
+ // the editor (which the launcher pads by seeding editorPaddingX). The
101
+ // footer can't read pi's settings, so the value crosses over env.
102
+ [PAD_X_ENV]: String(resolvePadX()),
98
103
  };
99
104
  // Always load our extension. Default the provider to `yagni` unless the user
100
105
  // explicitly chose one (so power users can still point pi elsewhere).
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Single source of truth for the CLI's horizontal padding, shared by the
3
+ * launcher (which seeds the editor setting) and — via the `YAGNI_PAD_X` env the
4
+ * launcher forwards — pi-extension-yagni's footer.
5
+ *
6
+ * The value must match pi's `outputPad` (the chat/output area's left+right
7
+ * padding, which defaults to 1) so the input editor, the output stream, and
8
+ * the status bar all align on the same column. Keeping it in one module makes
9
+ * the coherence structural rather than eyeballed: change this one number and
10
+ * the editor, footer, and seeded default move together.
11
+ *
12
+ * Separators (the ─ rules above/below the input) intentionally stay full-bleed
13
+ * and are NOT padded — matching Claude Code's frame.
14
+ */
15
+ export declare const PAD_X = 1;
16
+ /** Left (and right) padding string applied to the editor and footer. */
17
+ export declare const PAD_STR: string;
18
+ /** Env var the launcher sets so the extension's footer can match the editor pad. */
19
+ export declare const PAD_X_ENV = "YAGNI_PAD_X";
20
+ /** Parse a pad width from the env override, falling back to {@link PAD_X}. */
21
+ export declare function resolvePadX(raw?: string | undefined): number;
22
+ //# sourceMappingURL=padding.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Single source of truth for the CLI's horizontal padding, shared by the
3
+ * launcher (which seeds the editor setting) and — via the `YAGNI_PAD_X` env the
4
+ * launcher forwards — pi-extension-yagni's footer.
5
+ *
6
+ * The value must match pi's `outputPad` (the chat/output area's left+right
7
+ * padding, which defaults to 1) so the input editor, the output stream, and
8
+ * the status bar all align on the same column. Keeping it in one module makes
9
+ * the coherence structural rather than eyeballed: change this one number and
10
+ * the editor, footer, and seeded default move together.
11
+ *
12
+ * Separators (the ─ rules above/below the input) intentionally stay full-bleed
13
+ * and are NOT padded — matching Claude Code's frame.
14
+ */
15
+ export const PAD_X = 1;
16
+ /** Left (and right) padding string applied to the editor and footer. */
17
+ export const PAD_STR = " ".repeat(PAD_X);
18
+ /** Env var the launcher sets so the extension's footer can match the editor pad. */
19
+ export const PAD_X_ENV = "YAGNI_PAD_X";
20
+ /** Parse a pad width from the env override, falling back to {@link PAD_X}. */
21
+ export function resolvePadX(raw = process.env[PAD_X_ENV]) {
22
+ const n = raw === undefined ? NaN : Number.parseInt(raw, 10);
23
+ return Number.isFinite(n) && n >= 0 && n <= 3 ? n : PAD_X;
24
+ }
25
+ //# sourceMappingURL=padding.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "0.2.1-staging.1038.1",
3
+ "version": "0.2.1-staging.1039.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -38,5 +38,5 @@
38
38
  "@earendil-works/pi-tui": "0.83.0",
39
39
  "typebox": "^1.1.38"
40
40
  },
41
- "yagniSourceSha": "4493e421b21ba86ed37645beecef7c9a74799f4b"
41
+ "yagniSourceSha": "82c8e819af017a1a2c5c93aea5b4f96690f267d3"
42
42
  }