@code-yeongyu/senpi 2026.5.15-2 → 2026.5.15-3

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.
@@ -60,6 +60,7 @@ import { UserMessageComponent } from "./components/user-message.js";
60
60
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
61
61
  import { resolveStartupToolPaths } from "./startup-tools.js";
62
62
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
63
+ import { formatWorkingStatusMessageFrame } from "./working-status.js";
63
64
  function isExpandable(obj) {
64
65
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
65
66
  }
@@ -77,6 +78,58 @@ class ExpandableText extends Text {
77
78
  }
78
79
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
79
80
  const DEFAULT_WORKING_STATUS_REFRESH_INTERVAL_MS = 600;
81
+ const DEFAULT_WORKING_STATUS_MESSAGE_ANIMATION_INTERVAL_MS = 32;
82
+ const RGB_FOREGROUND_PATTERN = /\x1b\[38;2;(\d+);(\d+);(\d+)m/;
83
+ const DARK_DEFAULT_WORKING_TEXT_RGB = { r: 229, g: 229, b: 231 };
84
+ const LIGHT_DEFAULT_WORKING_TEXT_RGB = { r: 17, g: 17, b: 17 };
85
+ const DARK_DEFAULT_WORKING_BASE_RGB = { r: 102, g: 102, b: 102 };
86
+ const LIGHT_DEFAULT_WORKING_BASE_RGB = { r: 118, g: 118, b: 118 };
87
+ function parseAnsiRgbForeground(ansi) {
88
+ const match = RGB_FOREGROUND_PATTERN.exec(ansi);
89
+ const red = match?.[1];
90
+ const green = match?.[2];
91
+ const blue = match?.[3];
92
+ if (red === undefined || green === undefined || blue === undefined) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ r: Number.parseInt(red, 10),
97
+ g: Number.parseInt(green, 10),
98
+ b: Number.parseInt(blue, 10),
99
+ };
100
+ }
101
+ function isWorkingLightTheme() {
102
+ return theme.name?.toLowerCase().includes("light") ?? false;
103
+ }
104
+ function clampColorChannel(value) {
105
+ return Math.max(0, Math.min(255, Math.round(value)));
106
+ }
107
+ function mixRgbColor(base, highlight, amount) {
108
+ const clampedAmount = Math.max(0, Math.min(1, amount));
109
+ return {
110
+ r: clampColorChannel(base.r + (highlight.r - base.r) * clampedAmount),
111
+ g: clampColorChannel(base.g + (highlight.g - base.g) * clampedAmount),
112
+ b: clampColorChannel(base.b + (highlight.b - base.b) * clampedAmount),
113
+ };
114
+ }
115
+ function formatWorkingStatusShimmerText(text, intensity) {
116
+ if (theme.getColorMode() !== "truecolor") {
117
+ if (intensity < 0.2) {
118
+ return theme.fg("dim", text);
119
+ }
120
+ if (intensity < 0.6) {
121
+ return theme.fg("text", text);
122
+ }
123
+ return theme.bold(theme.fg("text", text));
124
+ }
125
+ const lightTheme = isWorkingLightTheme();
126
+ const base = parseAnsiRgbForeground(theme.getFgAnsi("dim")) ??
127
+ (lightTheme ? LIGHT_DEFAULT_WORKING_BASE_RGB : DARK_DEFAULT_WORKING_BASE_RGB);
128
+ const highlight = parseAnsiRgbForeground(theme.getFgAnsi("text")) ??
129
+ (lightTheme ? LIGHT_DEFAULT_WORKING_TEXT_RGB : DARK_DEFAULT_WORKING_TEXT_RGB);
130
+ const color = mixRgbColor(base, highlight, intensity * 0.9);
131
+ return `\x1b[1m\x1b[38;2;${color.r};${color.g};${color.b}m${text}\x1b[39m\x1b[22m`;
132
+ }
80
133
  function isDeadTerminalError(error) {
81
134
  if (!error || typeof error !== "object" || !("code" in error)) {
82
135
  return false;
@@ -84,23 +137,6 @@ function isDeadTerminalError(error) {
84
137
  const code = error.code;
85
138
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
86
139
  }
87
- export function formatWorkingElapsedSeconds(elapsedSeconds) {
88
- const totalSeconds = Math.max(0, Math.floor(elapsedSeconds));
89
- const seconds = totalSeconds % 60;
90
- const totalMinutes = Math.floor(totalSeconds / 60);
91
- if (totalSeconds < 60) {
92
- return `${totalSeconds}s`;
93
- }
94
- if (totalSeconds < 3600) {
95
- return `${totalMinutes}m ${seconds.toString().padStart(2, "0")}s`;
96
- }
97
- const hours = Math.floor(totalMinutes / 60);
98
- const minutes = totalMinutes % 60;
99
- return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
100
- }
101
- export function formatWorkingStatusMessage(message, elapsedSeconds, interruptKey) {
102
- return `${message} (${formatWorkingElapsedSeconds(elapsedSeconds)} • ${interruptKey} to interrupt)`;
103
- }
104
140
  const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
105
141
  function isAnthropicSubscriptionAuthKey(apiKey) {
106
142
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
@@ -1308,11 +1344,8 @@ export class InteractiveMode {
1308
1344
  }
1309
1345
  return Math.max(0, Math.floor((Date.now() - this.workingStartedAt) / 1000));
1310
1346
  }
1311
- getWorkingStatusMessage() {
1312
- return formatWorkingStatusMessage(this.getWorkingLoaderMessage(), this.getWorkingElapsedSeconds(), keyText("app.interrupt"));
1313
- }
1314
1347
  refreshWorkingLoaderMessage() {
1315
- this.loadingAnimation?.setMessage(this.getWorkingStatusMessage());
1348
+ this.loadingAnimation?.setMessage(this.getWorkingLoaderMessage());
1316
1349
  }
1317
1350
  startWorkingElapsedTimer() {
1318
1351
  this.stopWorkingElapsedTimer();
@@ -1329,12 +1362,20 @@ export class InteractiveMode {
1329
1362
  }
1330
1363
  getWorkingIndicatorOptions() {
1331
1364
  return (this.workingIndicatorOptions ?? {
1332
- frames: [theme.fg("accent", "•"), theme.fg("muted", "◦")],
1365
+ frames: [theme.fg("accent", "•")],
1333
1366
  intervalMs: DEFAULT_WORKING_STATUS_REFRESH_INTERVAL_MS,
1367
+ messageFormatter: (message, animationElapsedMs) => formatWorkingStatusMessageFrame(message, this.getWorkingElapsedSeconds(), keyText("app.interrupt"), animationElapsedMs, {
1368
+ base: (text) => theme.fg("dim", text),
1369
+ glow: (text) => theme.fg("text", text),
1370
+ highlight: (text) => theme.bold(theme.fg("text", text)),
1371
+ shimmer: formatWorkingStatusShimmerText,
1372
+ suffix: (text) => theme.fg("dim", text),
1373
+ }),
1374
+ messageIntervalMs: DEFAULT_WORKING_STATUS_MESSAGE_ANIMATION_INTERVAL_MS,
1334
1375
  });
1335
1376
  }
1336
1377
  createWorkingLoader() {
1337
- return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingStatusMessage(), this.getWorkingIndicatorOptions());
1378
+ return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.getWorkingIndicatorOptions());
1338
1379
  }
1339
1380
  stopWorkingLoader() {
1340
1381
  this.stopWorkingElapsedTimer();