osdy-pi 0.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +209 -161
  2. package/bin/osdy-pi.mjs +127 -0
  3. package/extensions/osdy-pi/account-profiles.test.ts +517 -0
  4. package/extensions/osdy-pi/account-profiles.ts +581 -0
  5. package/extensions/osdy-pi/animation.ts +47 -5
  6. package/extensions/osdy-pi/constants.ts +282 -103
  7. package/extensions/osdy-pi/diff-panel.ts +78 -24
  8. package/extensions/osdy-pi/editor-settings.test.ts +163 -0
  9. package/extensions/osdy-pi/editor-settings.ts +112 -0
  10. package/extensions/osdy-pi/metrics.ts +8 -135
  11. package/extensions/osdy-pi/plugin-events.ts +26 -0
  12. package/extensions/osdy-pi/profile-label.ts +29 -0
  13. package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
  14. package/extensions/osdy-pi/runtime-helpers.ts +87 -44
  15. package/extensions/osdy-pi/runtime.test.ts +62 -0
  16. package/extensions/osdy-pi/runtime.ts +256 -64
  17. package/extensions/osdy-pi/types.ts +33 -10
  18. package/extensions/osdy-pi/ui.test.ts +51 -0
  19. package/extensions/osdy-pi/ui.ts +298 -114
  20. package/extensions/osdy-pi/utils.ts +43 -1
  21. package/extensions/osdy-pi/working-tree.ts +25 -6
  22. package/package.json +22 -5
  23. package/scripts/osdy-pi-account-profiles.mjs +501 -0
  24. package/themes/osdy-pi-catppuccin-frappe.json +94 -0
  25. package/themes/osdy-pi-catppuccin-latte.json +94 -0
  26. package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
  27. package/themes/osdy-pi-catppuccin-mocha.json +94 -0
  28. package/themes/osdy-pi-dark.json +0 -6
  29. package/themes/osdy-pi-dracula.json +83 -0
  30. package/themes/osdy-pi-kanagawa-dragon.json +91 -0
  31. package/themes/osdy-pi-kanagawa-lotus.json +92 -0
  32. package/themes/osdy-pi-kanagawa-wave.json +91 -0
  33. package/themes/osdy-pi-lucent-orange.json +80 -0
  34. package/themes/osdy-pi-matrix.json +83 -0
  35. package/themes/osdy-pi-new.json +91 -0
  36. package/themes/osdy-pi-sexy.json +84 -0
  37. package/themes/osdy-pi-tokyo-night.json +83 -0
  38. package/themes/osdy-pi-light.json +0 -84
  39. package/themes/osdy-pi-simple.json +0 -83
@@ -20,66 +20,329 @@ import {
20
20
  import { fitBorder } from "./border.js";
21
21
  import {
22
22
  ANIMATION_INTERVAL_MS,
23
+ COMPACT_HEADER_HIDDEN_COLUMNS,
23
24
  headerWidth,
24
- HEADER_FALLBACK,
25
25
  HEADER_VARIANTS,
26
26
  INTRO_ANIMATION_FRAMES,
27
27
  MASCOT_GAP,
28
- MASCOT_MIN_ROWS,
29
- mascotWidth,
28
+ mascotWidthForRows,
29
+ scaleHeader,
30
+ scaleMascot,
31
+ STACKED_CONTENT_MAX_ROWS,
32
+ STACKED_HEADER_MAX_ROWS,
33
+ STACKED_HEADER_MIN_ROWS,
34
+ type ScaledHeaderArt,
30
35
  WORKING_SPINNER_FRAMES,
31
36
  } from "./constants.js";
32
37
  import { formatPath } from "./format.js";
38
+ import { modelLabel, usageLabel } from "./metrics.js";
33
39
  import {
34
- modelLabel,
35
- renderHeaderMetadata,
36
- renderMetaRows,
37
- usageLabel,
38
- } from "./metrics.js";
39
- import type { OsdyState, SimpleTheme, WorkingWidgetState } from "./types.js";
40
+ formatModelMetadata,
41
+ resolveActiveProfileLabel,
42
+ resolveEditorTitleLabel,
43
+ } from "./profile-label.js";
40
44
  import {
45
+ shouldShowFooterMetadata,
46
+ type HeaderVariant,
47
+ type OsdyState,
48
+ type SimpleTheme,
49
+ type WorkingWidgetState,
50
+ } from "./types.js";
51
+ import {
52
+ compactMascotWidthBudget,
41
53
  composeSideBySide,
42
54
  fitCenterVisible,
43
55
  internalLineTarget,
44
56
  sanitizeStatusText,
45
57
  } from "./utils.js";
46
58
 
59
+ const THINKING_THEME_TOKENS = {
60
+ off: "thinkingOff",
61
+ minimal: "thinkingMinimal",
62
+ low: "thinkingLow",
63
+ medium: "thinkingMedium",
64
+ high: "thinkingHigh",
65
+ xhigh: "thinkingXhigh",
66
+ max: "thinkingXhigh",
67
+ } satisfies Record<ReturnType<ExtensionAPI["getThinkingLevel"]>, string>;
68
+
69
+ const PI_LENS_STATUS_KEY = "pi-lens-lsp";
70
+
71
+ function stackedHeaderRowBudget(terminalRows: number): number {
72
+ const contentRows = Math.min(terminalRows, STACKED_CONTENT_MAX_ROWS);
73
+ return Math.min(STACKED_HEADER_MAX_ROWS, Math.floor(contentRows / 3));
74
+ }
75
+
76
+ type HeaderLayout = {
77
+ canUseFullHeader: boolean;
78
+ hasMascot: boolean;
79
+ compactHeader: ScaledHeaderArt | undefined;
80
+ fullHeaderWidth: number;
81
+ mascotRowBudget: number;
82
+ mascotWidthBudget: number;
83
+ renderWidth: number;
84
+ };
85
+
86
+ type HeaderConfig = (typeof HEADER_VARIANTS)[HeaderVariant];
87
+
88
+ function calculateHeaderLayout(
89
+ variantName: HeaderVariant,
90
+ variant: HeaderConfig,
91
+ width: number,
92
+ terminalColumns: number,
93
+ terminalRows: number,
94
+ ): HeaderLayout {
95
+ const renderWidth = Math.max(1, width);
96
+ const fullHeaderWidth = headerWidth(variantName);
97
+ const mascotArt = {
98
+ mascot: variant.mascot ?? [],
99
+ toneMap: variant.mascotMap ?? [],
100
+ };
101
+ const hasMascot = mascotArt.mascot.length > 0 && mascotArt.toneMap.length > 0;
102
+ const heightLimitedMascotWidth = hasMascot
103
+ ? mascotWidthForRows(mascotArt, terminalRows)
104
+ : 0;
105
+ const canUseFullHeader =
106
+ hasMascot &&
107
+ renderWidth >= fullHeaderWidth + MASCOT_GAP + heightLimitedMascotWidth &&
108
+ terminalRows >= variant.header.length;
109
+ const canShowCompactHeader =
110
+ !canUseFullHeader &&
111
+ terminalColumns >= COMPACT_HEADER_HIDDEN_COLUMNS &&
112
+ renderWidth >= COMPACT_HEADER_HIDDEN_COLUMNS;
113
+ const compactHeaderRows = stackedHeaderRowBudget(terminalRows);
114
+ const compactHeader =
115
+ canShowCompactHeader && compactHeaderRows >= STACKED_HEADER_MIN_ROWS
116
+ ? scaleHeader(variantName, renderWidth, compactHeaderRows)
117
+ : undefined;
118
+ const contentRowBudget = Math.min(terminalRows, STACKED_CONTENT_MAX_ROWS);
119
+ return {
120
+ canUseFullHeader,
121
+ hasMascot,
122
+ compactHeader,
123
+ fullHeaderWidth,
124
+ mascotRowBudget: canUseFullHeader
125
+ ? terminalRows
126
+ : Math.max(1, contentRowBudget - (compactHeader?.header.length ?? 0)),
127
+ mascotWidthBudget: canUseFullHeader
128
+ ? Math.max(1, renderWidth - fullHeaderWidth - MASCOT_GAP)
129
+ : compactMascotWidthBudget(renderWidth),
130
+ renderWidth,
131
+ };
132
+ }
133
+
134
+ function renderMascotLines(
135
+ variant: HeaderConfig,
136
+ mascotArt: ReturnType<typeof scaleMascot>,
137
+ frame: number,
138
+ theme: SimpleTheme,
139
+ animationStyle: "animated" | "static",
140
+ ): string[] {
141
+ return mascotArt.mascot.map((line, index) => {
142
+ const toneLine = mascotArt.toneMap[index];
143
+ if (toneLine && variant.mascotTonePalette) {
144
+ return animateAsciiLineWithToneMap(
145
+ line,
146
+ toneLine,
147
+ index,
148
+ frame,
149
+ theme,
150
+ variant.mascotTonePalette,
151
+ animationStyle,
152
+ );
153
+ }
154
+ return animateAsciiLine(
155
+ line,
156
+ index,
157
+ frame,
158
+ theme,
159
+ variant.mascotPalette.baseColor,
160
+ variant.mascotPalette.highlightColor,
161
+ variant.mascotPalette.trailColor,
162
+ animationStyle,
163
+ );
164
+ });
165
+ }
166
+
167
+ function renderHeaderLines(
168
+ variant: HeaderConfig,
169
+ header: readonly string[],
170
+ toneMap: readonly string[] | undefined,
171
+ sourceIndexes: readonly number[],
172
+ frame: number,
173
+ theme: SimpleTheme,
174
+ animationStyle: "animated" | "static",
175
+ ): string[] {
176
+ return header.map((line, index) => {
177
+ const sourceIndex = sourceIndexes[index] ?? index;
178
+ const toneLine = toneMap?.[index];
179
+ if (toneLine && variant.headerTonePalette) {
180
+ return animateAsciiLineWithToneMap(
181
+ line,
182
+ toneLine,
183
+ index,
184
+ frame,
185
+ theme,
186
+ variant.headerTonePalette,
187
+ animationStyle,
188
+ );
189
+ }
190
+ const palette = variant.linePalette(sourceIndex);
191
+ return animateAsciiLine(
192
+ line,
193
+ index,
194
+ frame,
195
+ theme,
196
+ palette.baseColor,
197
+ palette.highlightColor,
198
+ palette.trailColor,
199
+ animationStyle,
200
+ );
201
+ });
202
+ }
203
+
204
+ function renderResponsiveHeader(
205
+ state: OsdyState,
206
+ width: number,
207
+ terminalColumns: number,
208
+ terminalRows: number,
209
+ frame: number,
210
+ theme: SimpleTheme,
211
+ animationStyle: "animated" | "static",
212
+ ): string[] {
213
+ const variant = HEADER_VARIANTS[state.headerVariant];
214
+ const layout = calculateHeaderLayout(
215
+ state.headerVariant,
216
+ variant,
217
+ width,
218
+ terminalColumns,
219
+ terminalRows,
220
+ );
221
+ const mascotArt = layout.hasMascot
222
+ ? scaleMascot(
223
+ { mascot: variant.mascot ?? [], toneMap: variant.mascotMap ?? [] },
224
+ layout.mascotWidthBudget,
225
+ layout.mascotRowBudget,
226
+ )
227
+ : { mascot: [], toneMap: [] };
228
+ const mascotLines = renderMascotLines(
229
+ variant,
230
+ mascotArt,
231
+ frame,
232
+ theme,
233
+ animationStyle,
234
+ );
235
+ if (!layout.canUseFullHeader && !layout.compactHeader) {
236
+ return mascotLines.map((line) => fitCenterVisible(line, layout.renderWidth));
237
+ }
238
+ const headerArt = layout.compactHeader;
239
+ const logoLines = layout.canUseFullHeader
240
+ ? renderHeaderLines(
241
+ variant,
242
+ variant.header,
243
+ variant.headerMap,
244
+ variant.header.map((_line, index) => index),
245
+ frame,
246
+ theme,
247
+ animationStyle,
248
+ )
249
+ : renderHeaderLines(
250
+ variant,
251
+ headerArt?.header ?? [],
252
+ headerArt?.toneMap,
253
+ headerArt?.sourceIndexes ?? [],
254
+ frame,
255
+ theme,
256
+ animationStyle,
257
+ );
258
+ if (!layout.canUseFullHeader) {
259
+ return [...mascotLines, ...logoLines].map((line) =>
260
+ fitCenterVisible(line, layout.renderWidth),
261
+ );
262
+ }
263
+ const scaledMascotWidth = mascotArt.mascot.reduce(
264
+ (maximum, line) => Math.max(maximum, visibleWidth(line)),
265
+ 0,
266
+ );
267
+ return composeSideBySide(
268
+ mascotLines,
269
+ scaledMascotWidth,
270
+ logoLines,
271
+ layout.renderWidth,
272
+ layout.fullHeaderWidth,
273
+ );
274
+ }
275
+
47
276
  class OsdyFooter implements Component {
277
+ private readonly pi: ExtensionAPI;
48
278
  private readonly ctx: ExtensionContext;
279
+ private readonly state: OsdyState;
49
280
  private readonly footerData: ReadonlyFooterDataProvider;
50
281
  private readonly theme: SimpleTheme;
51
282
 
52
283
  constructor(
284
+ pi: ExtensionAPI,
53
285
  ctx: ExtensionContext,
286
+ state: OsdyState,
54
287
  footerData: ReadonlyFooterDataProvider,
55
288
  theme: SimpleTheme,
56
289
  ) {
290
+ this.pi = pi;
57
291
  this.ctx = ctx;
292
+ this.state = state;
58
293
  this.footerData = footerData;
59
294
  this.theme = theme;
60
295
  }
61
296
 
62
297
  render(width: number): string[] {
298
+ const ellipsis = this.theme.fg("dim", "...");
63
299
  let location = formatPath(this.ctx.cwd);
64
300
  const branch = this.footerData.getGitBranch?.();
65
301
  if (branch) location = `${location} (${branch})`;
66
302
  const locationLine = truncateToWidth(
67
303
  this.theme.fg("dim", location),
68
304
  width,
69
- this.theme.fg("dim", "..."),
305
+ ellipsis,
70
306
  );
71
307
  const statuses = this.footerData.getExtensionStatuses?.() ?? new Map();
72
- const statusEntries = Array.from(statuses.entries());
308
+ const statusEntries = Array.from(statuses.entries()).filter(
309
+ ([key]) => !this.state.smallMode || key !== PI_LENS_STATUS_KEY,
310
+ );
73
311
  const statusLine = statusEntries
74
312
  .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
75
313
  .map(([, text]) => sanitizeStatusText(text))
76
314
  .join(" ");
315
+ if (!shouldShowFooterMetadata(this.state.editorEffective)) {
316
+ return statusLine
317
+ ? [locationLine, truncateToWidth(statusLine, width, ellipsis)]
318
+ : [locationLine];
319
+ }
320
+ const thinkingLevel = this.pi.getThinkingLevel();
321
+ const thinkingLine = truncateToWidth(
322
+ formatModelMetadata(
323
+ modelLabel(this.ctx),
324
+ this.theme.fg(
325
+ THINKING_THEME_TOKENS[thinkingLevel],
326
+ `\u001B[1m${thinkingLevel}\u001B[22m`,
327
+ ),
328
+ resolveActiveProfileLabel(),
329
+ ),
330
+ width,
331
+ ellipsis,
332
+ );
333
+ const usageLine = truncateToWidth(
334
+ usageLabel(this.ctx).trim(),
335
+ width,
336
+ ellipsis,
337
+ );
77
338
  return statusLine
78
339
  ? [
340
+ thinkingLine,
341
+ usageLine,
79
342
  locationLine,
80
- truncateToWidth(statusLine, width, this.theme.fg("dim", "...")),
343
+ truncateToWidth(statusLine, width, ellipsis),
81
344
  ]
82
- : [locationLine];
345
+ : [thinkingLine, usageLine, locationLine];
83
346
  }
84
347
 
85
348
  invalidate(): void {}
@@ -97,9 +360,7 @@ class OsdyWorkingWidget implements Component {
97
360
  render(width: number): string[] {
98
361
  if (!this.state.active) return [];
99
362
  const frame =
100
- WORKING_SPINNER_FRAMES[
101
- this.state.frame % WORKING_SPINNER_FRAMES.length
102
- ] ??
363
+ WORKING_SPINNER_FRAMES[this.state.frame % WORKING_SPINNER_FRAMES.length] ??
103
364
  WORKING_SPINNER_FRAMES[0] ??
104
365
  "⠋";
105
366
  const line = `${this.theme.fg("accent", frame)} ${this.theme.fg("muted", this.state.label)}`;
@@ -110,11 +371,12 @@ class OsdyWorkingWidget implements Component {
110
371
  }
111
372
 
112
373
  export function createHeaderComponent(
113
- pi: ExtensionAPI,
114
- ctx: ExtensionContext,
374
+ _pi: ExtensionAPI,
375
+ _ctx: ExtensionContext,
115
376
  state: OsdyState,
116
377
  ) {
117
378
  return (_tui: TUI, theme: SimpleTheme) => {
379
+ state.tui = _tui;
118
380
  let frame = 0;
119
381
  let animationComplete = false;
120
382
  const animationMode = asciiAnimationMode();
@@ -133,93 +395,15 @@ export function createHeaderComponent(
133
395
  render(width: number): string[] {
134
396
  const animationStyle =
135
397
  animateAscii && !animationComplete ? "animated" : "static";
136
- const variant = HEADER_VARIANTS[state.headerVariant];
137
- const fullHeaderWidth = headerWidth(state.headerVariant);
138
- const canUseFullHeader =
139
- width >= fullHeaderWidth &&
140
- (variant.minRowsForFull === undefined ||
141
- _tui.terminal.rows >= variant.minRowsForFull);
142
- const mascotLinesSource = variant.mascot ?? [];
143
- const mascotToneMap = variant.mascotMap ?? [];
144
- const fullMascotWidth = mascotWidth(state.headerVariant);
145
- const showMascot =
146
- canUseFullHeader &&
147
- mascotLinesSource.length > 0 &&
148
- _tui.terminal.rows >= MASCOT_MIN_ROWS &&
149
- width >= fullMascotWidth + MASCOT_GAP + fullHeaderWidth;
150
- const headerLines = canUseFullHeader
151
- ? [...variant.header]
152
- : [...(variant.fallbackHeader ?? HEADER_FALLBACK)];
153
- const headerToneMap = canUseFullHeader ? (variant.headerMap ?? []) : [];
154
- const metadataRows = renderHeaderMetadata(pi, ctx, state, width);
155
- const logoLines = headerLines.map((line, index) => {
156
- const toneLine = headerToneMap[index];
157
- if (toneLine && variant.headerTonePalette) {
158
- return animateAsciiLineWithToneMap(
159
- line,
160
- toneLine,
161
- index,
162
- frame,
163
- theme,
164
- variant.headerTonePalette,
165
- animationStyle,
166
- );
167
- }
168
- const palette = canUseFullHeader
169
- ? variant.linePalette(index)
170
- : (variant.fallbackLinePalette?.(index) ??
171
- variant.linePalette(index));
172
- return animateAsciiLine(
173
- line,
174
- index,
175
- frame,
176
- theme,
177
- palette.baseColor,
178
- palette.highlightColor,
179
- palette.trailColor,
180
- animationStyle,
181
- );
182
- });
183
- const mascotLines = mascotLinesSource.map((line, index) => {
184
- const toneLine = mascotToneMap[index];
185
- if (toneLine && variant.mascotTonePalette) {
186
- return animateAsciiLineWithToneMap(
187
- line,
188
- toneLine,
189
- index,
190
- frame,
191
- theme,
192
- variant.mascotTonePalette,
193
- animationStyle,
194
- );
195
- }
196
- return animateAsciiLine(
197
- line,
198
- index,
199
- frame,
200
- theme,
201
- variant.mascotPalette.baseColor,
202
- variant.mascotPalette.highlightColor,
203
- variant.mascotPalette.trailColor,
204
- animationStyle,
205
- );
206
- });
207
- const headerBlock = showMascot
208
- ? composeSideBySide(
209
- mascotLines,
210
- fullMascotWidth,
211
- logoLines,
212
- width,
213
- fullHeaderWidth,
214
- )
215
- : logoLines.map((line) => fitCenterVisible(line, width));
216
- return [
217
- "",
218
- ...headerBlock,
219
- "",
220
- ...renderMetaRows(metadataRows, width, theme),
221
- "",
222
- ];
398
+ return renderResponsiveHeader(
399
+ state,
400
+ width,
401
+ Math.max(1, _tui.terminal.columns),
402
+ Math.max(1, _tui.terminal.rows),
403
+ frame,
404
+ theme,
405
+ animationStyle,
406
+ );
223
407
  },
224
408
  invalidate() {},
225
409
  dispose() {
@@ -230,11 +414,13 @@ export function createHeaderComponent(
230
414
  }
231
415
 
232
416
  export function createFooterComponent(
417
+ pi: ExtensionAPI,
233
418
  ctx: ExtensionContext,
419
+ state: OsdyState,
234
420
  footerData: ReadonlyFooterDataProvider,
235
421
  theme: SimpleTheme,
236
422
  ): Component {
237
- return new OsdyFooter(ctx, footerData, theme);
423
+ return new OsdyFooter(pi, ctx, state, footerData, theme);
238
424
  }
239
425
 
240
426
  export function createWorkingWidgetFactory(workingState: WorkingWidgetState) {
@@ -275,10 +461,7 @@ export function createEditorComponent(
275
461
  const internalLines = bottomIndex - 1;
276
462
  if (internalLines < target) {
277
463
  const blank = " ".repeat(innerWidth);
278
- const added = Array.from(
279
- { length: target - internalLines },
280
- () => blank,
281
- );
464
+ const added = Array.from({ length: target - internalLines }, () => blank);
282
465
  lines.splice(bottomIndex, 0, ...added);
283
466
  bottomIndex += added.length;
284
467
  }
@@ -286,12 +469,13 @@ export function createEditorComponent(
286
469
  const side = borderColor("│");
287
470
  for (let index = 1; index < bottomIndex; index += 1) {
288
471
  const content = truncateToWidth(lines[index] ?? "", innerWidth, "");
289
- const padding = " ".repeat(
290
- Math.max(0, innerWidth - visibleWidth(content)),
291
- );
472
+ const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
292
473
  lines[index] = `${side}${content}${padding}${side}`;
293
474
  }
294
- const topLeft = ctx.ui.theme.fg("mdLink", " Osdy-Pi ");
475
+ const topLeft = ctx.ui.theme.fg(
476
+ "mdLink",
477
+ ` ${resolveEditorTitleLabel(resolveActiveProfileLabel())} `,
478
+ );
295
479
  const topRight = ctx.ui.theme.fg(
296
480
  "muted",
297
481
  ` ${modelLabel(ctx)} · think ${pi.getThinkingLevel()} `,
@@ -1,6 +1,7 @@
1
1
  import type { TUI } from "@earendil-works/pi-tui";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { MASCOT_GAP } from "./constants.js";
3
+ import { headerWidth, HEADER_VARIANTS, MASCOT_GAP } from "./constants.js";
4
+ import type { HeaderVariant } from "./types.js";
4
5
 
5
6
  export function sanitizeStatusText(text: string): string {
6
7
  return text
@@ -49,6 +50,47 @@ export function composeSideBySide(
49
50
  return composedRows;
50
51
  }
51
52
 
53
+ export function isSmallResponsiveMode(
54
+ variant: HeaderVariant,
55
+ columns: number,
56
+ rows: number,
57
+ ): boolean {
58
+ const header = HEADER_VARIANTS[variant];
59
+ const hasMascot =
60
+ (header.mascot?.length ?? 0) > 0 && (header.mascotMap?.length ?? 0) > 0;
61
+ return (
62
+ !hasMascot ||
63
+ columns < headerWidth(variant) + MASCOT_GAP + 1 ||
64
+ rows < header.header.length
65
+ );
66
+ }
67
+
68
+ // In mascot-only mode, phone-sized terminals use roughly four-fifths of the
69
+ // available width while wider compact terminals gradually recover more detail.
70
+ const COMPACT_MASCOT_BASE_RATIO = 0.7938;
71
+ const COMPACT_MASCOT_MAX_RATIO = 0.87318;
72
+ const COMPACT_MASCOT_RATIO_RAMP_START_COLUMNS = 60;
73
+ const COMPACT_MASCOT_RATIO_RAMP_COLUMNS = 40;
74
+
75
+ export function compactMascotWidthBudget(availableWidth: number): number {
76
+ const normalizedWidth = Math.max(1, Math.floor(availableWidth));
77
+ const ratioProgress = Math.max(
78
+ 0,
79
+ Math.min(
80
+ 1,
81
+ (normalizedWidth - COMPACT_MASCOT_RATIO_RAMP_START_COLUMNS) /
82
+ COMPACT_MASCOT_RATIO_RAMP_COLUMNS,
83
+ ),
84
+ );
85
+ const widthRatio =
86
+ COMPACT_MASCOT_BASE_RATIO +
87
+ (COMPACT_MASCOT_MAX_RATIO - COMPACT_MASCOT_BASE_RATIO) * ratioProgress;
88
+ return Math.min(
89
+ normalizedWidth,
90
+ Math.max(1, Math.round(normalizedWidth * widthRatio)),
91
+ );
92
+ }
93
+
52
94
  export function internalLineTarget(tui: TUI): number {
53
95
  return tui.terminal.rows < 18 ? 3 : 4;
54
96
  }
@@ -11,6 +11,8 @@ import type {
11
11
  WorkingTreeFileSummary,
12
12
  WorkingTreeSnapshot,
13
13
  WorkingTreeState,
14
+ WorkingWidgetState,
15
+ WorkingTreePlacement,
14
16
  } from "./types.js";
15
17
  import { fitCenterVisible } from "./utils.js";
16
18
 
@@ -128,13 +130,16 @@ async function hydrateUntrackedStats(
128
130
  ): Promise<void> {
129
131
  await Promise.all(
130
132
  Array.from(fileMap.values()).map(async (file) => {
131
- if (!file.untracked || file.additions > 0 || file.removals > 0) return;
133
+ if (!file.untracked || file.additions > 0 || file.removals > 0) {
134
+ return undefined;
135
+ }
132
136
  try {
133
137
  const content = await readFile(resolve(ctx.cwd, file.path), "utf8");
134
138
  file.additions = content.length === 0 ? 0 : content.split("\n").length;
135
139
  } catch {
136
140
  file.additions = 1;
137
141
  }
142
+ return undefined;
138
143
  }),
139
144
  );
140
145
  }
@@ -231,8 +236,8 @@ function renderSummaryLine(
231
236
  }
232
237
  const parts = [
233
238
  theme.fg("mdLink", `Git changes · ${snapshot.totalFiles} files`),
234
- theme.fg("success", `+${snapshot.additions}`),
235
- theme.fg("error", `-${snapshot.removals}`),
239
+ theme.fg("toolDiffAdded", `+${snapshot.additions}`),
240
+ theme.fg("toolDiffRemoved", `-${snapshot.removals}`),
236
241
  theme.fg("mdLink", `staged ${snapshot.stagedFiles}`),
237
242
  theme.fg("mdLink", `unstaged ${snapshot.unstagedFiles}`),
238
243
  ];
@@ -249,7 +254,7 @@ class WorkingTreeWidget implements Component {
249
254
  ) {}
250
255
 
251
256
  render(width: number): string[] {
252
- if (!this.state.enabled) return [];
257
+ if (!this.state.enabled || !this.state.visible) return [];
253
258
  if (this.state.loading) {
254
259
  return [
255
260
  fitCenterVisible(
@@ -282,9 +287,23 @@ class WorkingTreeWidget implements Component {
282
287
  invalidate(): void {}
283
288
  }
284
289
 
285
- export function createWorkingTreeWidgetFactory(state: WorkingTreeState) {
290
+ export function createWorkingTreeWidgetFactory(
291
+ state: WorkingTreeState,
292
+ workingState: WorkingWidgetState,
293
+ placement: WorkingTreePlacement,
294
+ ) {
286
295
  return (tui: TUI, theme: SimpleTheme): Component => {
287
296
  state.tui = tui;
288
- return new WorkingTreeWidget(state, theme);
297
+ const summary = new WorkingTreeWidget(state, theme);
298
+ return {
299
+ render(width: number): string[] {
300
+ const lines = summary.render(width);
301
+ if (lines.length === 0) return [];
302
+ const needsLeadingBlank =
303
+ placement === "belowEditor" || workingState.active;
304
+ return [...(needsLeadingBlank ? [""] : []), ...lines, ""];
305
+ },
306
+ invalidate(): void {},
307
+ };
289
308
  };
290
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "0.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,23 +10,40 @@
10
10
  "theme",
11
11
  "extension"
12
12
  ],
13
+ "bin": {
14
+ "osdy-pi": "bin/osdy-pi.mjs"
15
+ },
13
16
  "scripts": {
14
17
  "typecheck": "tsc --noEmit",
15
18
  "lint": "eslint .",
19
+ "test": "node --test --experimental-strip-types extensions/osdy-pi/*.test.ts && node --test scripts/*.test.mjs",
16
20
  "pi:dev": "node ./scripts/pi-dev.mjs"
17
21
  },
18
22
  "files": [
19
23
  "README.md",
20
24
  "LICENSE",
25
+ "bin",
26
+ "scripts/osdy-pi-account-profiles.mjs",
21
27
  "themes",
22
28
  "extensions",
23
29
  "mapche1.png"
24
30
  ],
25
31
  "pi": {
26
32
  "themes": [
33
+ "./themes/osdy-pi-new.json",
27
34
  "./themes/osdy-pi-dark.json",
28
- "./themes/osdy-pi-light.json",
29
- "./themes/osdy-pi-simple.json"
35
+ "./themes/osdy-pi-sexy.json",
36
+ "./themes/osdy-pi-tokyo-night.json",
37
+ "./themes/osdy-pi-kanagawa-wave.json",
38
+ "./themes/osdy-pi-kanagawa-dragon.json",
39
+ "./themes/osdy-pi-kanagawa-lotus.json",
40
+ "./themes/osdy-pi-dracula.json",
41
+ "./themes/osdy-pi-catppuccin-latte.json",
42
+ "./themes/osdy-pi-catppuccin-frappe.json",
43
+ "./themes/osdy-pi-catppuccin-macchiato.json",
44
+ "./themes/osdy-pi-catppuccin-mocha.json",
45
+ "./themes/osdy-pi-matrix.json",
46
+ "./themes/osdy-pi-lucent-orange.json"
30
47
  ],
31
48
  "extensions": [
32
49
  "./extensions/osdy-pi.ts"
@@ -38,8 +55,8 @@
38
55
  "@earendil-works/pi-tui": "*"
39
56
  },
40
57
  "devDependencies": {
41
- "@earendil-works/pi-coding-agent": "^0.74.1",
42
- "@earendil-works/pi-tui": "^0.74.1",
58
+ "@earendil-works/pi-coding-agent": "^0.84.4",
59
+ "@earendil-works/pi-tui": "^0.84.4",
43
60
  "@eslint/js": "^9.39.1",
44
61
  "@types/node": "^25.8.0",
45
62
  "eslint": "^9.39.1",