everybuddy 0.1.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/dist/atlas/bundled.d.ts +15 -0
  4. package/dist/atlas/bundled.js +438 -0
  5. package/dist/atlas/bundled.js.map +1 -0
  6. package/dist/bones/rarity.d.ts +2 -0
  7. package/dist/bones/rarity.js +43 -0
  8. package/dist/bones/rarity.js.map +1 -0
  9. package/dist/bones/roll.d.ts +10 -0
  10. package/dist/bones/roll.js +71 -0
  11. package/dist/bones/roll.js.map +1 -0
  12. package/dist/bones/species.d.ts +1 -0
  13. package/dist/bones/species.js +3 -0
  14. package/dist/bones/species.js.map +1 -0
  15. package/dist/bones/stats.d.ts +6 -0
  16. package/dist/bones/stats.js +55 -0
  17. package/dist/bones/stats.js.map +1 -0
  18. package/dist/cli/attach.d.ts +5 -0
  19. package/dist/cli/attach.js +64 -0
  20. package/dist/cli/attach.js.map +1 -0
  21. package/dist/cli/card.d.ts +3 -0
  22. package/dist/cli/card.js +19 -0
  23. package/dist/cli/card.js.map +1 -0
  24. package/dist/cli/detach.d.ts +4 -0
  25. package/dist/cli/detach.js +36 -0
  26. package/dist/cli/detach.js.map +1 -0
  27. package/dist/cli/event.d.ts +7 -0
  28. package/dist/cli/event.js +35 -0
  29. package/dist/cli/event.js.map +1 -0
  30. package/dist/cli/hatch.d.ts +6 -0
  31. package/dist/cli/hatch.js +20 -0
  32. package/dist/cli/hatch.js.map +1 -0
  33. package/dist/cli/init.d.ts +2 -0
  34. package/dist/cli/init.js +76 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/install.d.ts +32 -0
  37. package/dist/cli/install.js +121 -0
  38. package/dist/cli/install.js.map +1 -0
  39. package/dist/cli/io.d.ts +9 -0
  40. package/dist/cli/io.js +35 -0
  41. package/dist/cli/io.js.map +1 -0
  42. package/dist/cli/setup.d.ts +26 -0
  43. package/dist/cli/setup.js +297 -0
  44. package/dist/cli/setup.js.map +1 -0
  45. package/dist/cli/sidecar.d.ts +5 -0
  46. package/dist/cli/sidecar.js +12 -0
  47. package/dist/cli/sidecar.js.map +1 -0
  48. package/dist/i18n/companion.d.ts +9 -0
  49. package/dist/i18n/companion.js +117 -0
  50. package/dist/i18n/companion.js.map +1 -0
  51. package/dist/i18n/ui.d.ts +67 -0
  52. package/dist/i18n/ui.js +270 -0
  53. package/dist/i18n/ui.js.map +1 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +116 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/render/card.d.ts +10 -0
  58. package/dist/render/card.js +188 -0
  59. package/dist/render/card.js.map +1 -0
  60. package/dist/render/color.d.ts +5 -0
  61. package/dist/render/color.js +100 -0
  62. package/dist/render/color.js.map +1 -0
  63. package/dist/render/compose.d.ts +2 -0
  64. package/dist/render/compose.js +10 -0
  65. package/dist/render/compose.js.map +1 -0
  66. package/dist/render/gacha.d.ts +9 -0
  67. package/dist/render/gacha.js +322 -0
  68. package/dist/render/gacha.js.map +1 -0
  69. package/dist/render/sprites.d.ts +5 -0
  70. package/dist/render/sprites.js +316 -0
  71. package/dist/render/sprites.js.map +1 -0
  72. package/dist/runtime/observer.d.ts +73 -0
  73. package/dist/runtime/observer.js +448 -0
  74. package/dist/runtime/observer.js.map +1 -0
  75. package/dist/runtime/sidecar.d.ts +18 -0
  76. package/dist/runtime/sidecar.js +670 -0
  77. package/dist/runtime/sidecar.js.map +1 -0
  78. package/dist/runtime/socket.d.ts +6 -0
  79. package/dist/runtime/socket.js +47 -0
  80. package/dist/runtime/socket.js.map +1 -0
  81. package/dist/runtime/tmux.d.ts +41 -0
  82. package/dist/runtime/tmux.js +186 -0
  83. package/dist/runtime/tmux.js.map +1 -0
  84. package/dist/runtime/types.d.ts +96 -0
  85. package/dist/runtime/types.js +2 -0
  86. package/dist/runtime/types.js.map +1 -0
  87. package/dist/soul/hatch.d.ts +6 -0
  88. package/dist/soul/hatch.js +90 -0
  89. package/dist/soul/hatch.js.map +1 -0
  90. package/dist/soul/profile.d.ts +6 -0
  91. package/dist/soul/profile.js +48 -0
  92. package/dist/soul/profile.js.map +1 -0
  93. package/dist/soul/providers/anthropic.d.ts +17 -0
  94. package/dist/soul/providers/anthropic.js +105 -0
  95. package/dist/soul/providers/anthropic.js.map +1 -0
  96. package/dist/soul/providers/index.d.ts +10 -0
  97. package/dist/soul/providers/index.js +23 -0
  98. package/dist/soul/providers/index.js.map +1 -0
  99. package/dist/soul/providers/openai.d.ts +20 -0
  100. package/dist/soul/providers/openai.js +120 -0
  101. package/dist/soul/providers/openai.js.map +1 -0
  102. package/dist/soul/providers/types.d.ts +4 -0
  103. package/dist/soul/providers/types.js +2 -0
  104. package/dist/soul/providers/types.js.map +1 -0
  105. package/dist/storage/companion.d.ts +3 -0
  106. package/dist/storage/companion.js +155 -0
  107. package/dist/storage/companion.js.map +1 -0
  108. package/dist/storage/config.d.ts +36 -0
  109. package/dist/storage/config.js +220 -0
  110. package/dist/storage/config.js.map +1 -0
  111. package/dist/storage/paths.d.ts +3 -0
  112. package/dist/storage/paths.js +12 -0
  113. package/dist/storage/paths.js.map +1 -0
  114. package/dist/types/companion.d.ts +84 -0
  115. package/dist/types/companion.js +2 -0
  116. package/dist/types/companion.js.map +1 -0
  117. package/dist/types/onboarding.d.ts +12 -0
  118. package/dist/types/onboarding.js +2 -0
  119. package/dist/types/onboarding.js.map +1 -0
  120. package/package.json +52 -0
@@ -0,0 +1,670 @@
1
+ import net from "node:net";
2
+ import path from "node:path";
3
+ import { getDumpStat, getPeakStat } from "../bones/stats.js";
4
+ import { getLocalizedSoulCopy } from "../i18n/companion.js";
5
+ import { localizeRarityName, localizeSpeciesName, localizeStatName, localizeVoiceLabel, uiText, } from "../i18n/ui.js";
6
+ import { readCompanionRecord } from "../storage/companion.js";
7
+ import { resolveBuddyConfig } from "../storage/config.js";
8
+ import { composeFrame } from "../render/compose.js";
9
+ import { bold, colorize, dim, italic } from "../render/color.js";
10
+ import { EYES, SPECIES, renderCompactFace } from "../render/sprites.js";
11
+ import { buildCommandTracker, buildCommandWindowEntry, buildMemoryEntry, createCompanionObserver, rememberCommandWindowEntry, rememberMemoryEntry, } from "./observer.js";
12
+ import { ensureSocketDirectory, removeSocketIfExists, socketPathForWindow } from "./socket.js";
13
+ import { SIDECAR_OPTION, TARGET_OPTION, TmuxClient } from "./tmux.js";
14
+ const FRAME_TICK_MS = 500;
15
+ const HEALTH_TICK_MS = 1000;
16
+ const FULL_MODE_MIN_WIDTH = 26;
17
+ const DEFAULT_PANE_WIDTH = 30;
18
+ const DEFAULT_PANE_HEIGHT = 24;
19
+ const BUBBLE_SHOW_MS = 10_000;
20
+ const FADE_WINDOW_MS = 3_000;
21
+ const BUBBLE_MAX_WIDTH = 24;
22
+ const BUBBLE_MAX_LINES = 5;
23
+ const IDLE_SUMMARY_WIDTH = 24;
24
+ const IDLE_SUMMARY_MAX_LINES = 3;
25
+ const NARROW_REACTION_MAX_LINES = 2;
26
+ const PULSE_TICKS = 4;
27
+ const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0];
28
+ const PET_BURST_FRAMES = [
29
+ [" ♥ ♥ "],
30
+ [" ♥ ♥ ♥ "],
31
+ [" ♥ ♥ ♥ "],
32
+ [" · · · "],
33
+ ];
34
+ export async function runSidecarCommand(options) {
35
+ const tmux = new TmuxClient();
36
+ const config = await resolveBuddyConfig();
37
+ const sidecarPaneId = process.env.TMUX_PANE?.trim();
38
+ if (!sidecarPaneId) {
39
+ throw new Error("EveryBuddy sidecar requires a tmux pane.");
40
+ }
41
+ const socketPath = socketPathForWindow(options.windowId);
42
+ await ensureSocketDirectory();
43
+ await removeSocketIfExists(socketPath);
44
+ const state = {
45
+ windowId: options.windowId,
46
+ sidecarPaneId,
47
+ targetPaneId: options.targetPane,
48
+ language: config.language,
49
+ companion: await readCompanionRecord(),
50
+ frameTick: 0,
51
+ renderMode: resolveRenderMode(currentPaneWidth()),
52
+ status: "idle",
53
+ reactionText: undefined,
54
+ reactionExpiresAt: undefined,
55
+ lastCommand: undefined,
56
+ lastExitCode: undefined,
57
+ cwd: undefined,
58
+ lastUpdatedAt: Date.now(),
59
+ memory: [],
60
+ commandTrackers: {},
61
+ commandWindows: {},
62
+ recentReaction: undefined,
63
+ recentNotableReactionAt: undefined,
64
+ directAddressActive: false,
65
+ pulseUntilTick: undefined,
66
+ };
67
+ const observer = await createCompanionObserver(state.companion);
68
+ let observerEventVersion = 0;
69
+ let renderedLineCount = 0;
70
+ const rerender = () => {
71
+ state.renderMode = resolveRenderMode(currentPaneWidth());
72
+ renderedLineCount = renderSidecar(state, renderedLineCount);
73
+ };
74
+ const server = net.createServer((connection) => {
75
+ let buffer = "";
76
+ connection.on("data", (chunk) => {
77
+ buffer += chunk.toString("utf8");
78
+ let newlineIndex = buffer.indexOf("\n");
79
+ while (newlineIndex >= 0) {
80
+ const line = buffer.slice(0, newlineIndex).trim();
81
+ buffer = buffer.slice(newlineIndex + 1);
82
+ newlineIndex = buffer.indexOf("\n");
83
+ if (line.length > 0) {
84
+ const eventVersion = ++observerEventVersion;
85
+ void handleShellEvent(state, line, observer, rerender, eventVersion, () => {
86
+ return observerEventVersion === eventVersion;
87
+ });
88
+ }
89
+ }
90
+ });
91
+ });
92
+ let closed = false;
93
+ const cleanup = async () => {
94
+ if (closed) {
95
+ return;
96
+ }
97
+ closed = true;
98
+ clearInterval(frameTimer);
99
+ clearInterval(healthTimer);
100
+ server.close();
101
+ const registeredSidecarPaneId = await tmux
102
+ .getWindowOption(options.windowId, SIDECAR_OPTION)
103
+ .catch(() => undefined);
104
+ if (registeredSidecarPaneId === sidecarPaneId) {
105
+ await removeSocketIfExists(socketPath);
106
+ await tmux.unsetWindowOption(options.windowId, SIDECAR_OPTION);
107
+ await tmux.unsetWindowOption(options.windowId, TARGET_OPTION);
108
+ }
109
+ showCursor();
110
+ };
111
+ const frameTimer = setInterval(() => {
112
+ state.frameTick += 1;
113
+ state.renderMode = resolveRenderMode(currentPaneWidth());
114
+ expireReactionIfNeeded(state, Date.now());
115
+ rerender();
116
+ }, FRAME_TICK_MS);
117
+ const healthTimer = setInterval(async () => {
118
+ const panes = await tmux.listWindowPanes(options.windowId).catch(() => []);
119
+ const nextTarget = resolveTargetPane(panes, sidecarPaneId, state.targetPaneId);
120
+ if (!nextTarget) {
121
+ await cleanup();
122
+ process.exit(0);
123
+ return;
124
+ }
125
+ if (nextTarget !== state.targetPaneId) {
126
+ state.targetPaneId = nextTarget;
127
+ state.directAddressActive = false;
128
+ await tmux.setWindowOption(options.windowId, TARGET_OPTION, nextTarget).catch(() => { });
129
+ state.status = "idle";
130
+ state.lastUpdatedAt = Date.now();
131
+ rerender();
132
+ }
133
+ }, HEALTH_TICK_MS);
134
+ process.on("SIGINT", () => {
135
+ void cleanup().finally(() => process.exit(0));
136
+ });
137
+ process.on("SIGTERM", () => {
138
+ void cleanup().finally(() => process.exit(0));
139
+ });
140
+ hideCursor();
141
+ await new Promise((resolve, reject) => {
142
+ server.on("error", reject);
143
+ server.listen(socketPath, () => resolve());
144
+ });
145
+ rerender();
146
+ }
147
+ export function resolveTargetPane(paneIds, sidecarPaneId, currentTargetPaneId) {
148
+ const workPaneIds = paneIds.filter((paneId) => paneId !== sidecarPaneId);
149
+ if (currentTargetPaneId && workPaneIds.includes(currentTargetPaneId)) {
150
+ return currentTargetPaneId;
151
+ }
152
+ return workPaneIds[0];
153
+ }
154
+ export function resolveRenderMode(columns) {
155
+ return columns >= FULL_MODE_MIN_WIDTH ? "full" : "narrow";
156
+ }
157
+ async function handleShellEvent(state, payload, observer, rerender, eventVersion, isCurrent) {
158
+ try {
159
+ const event = JSON.parse(payload);
160
+ if (event.windowId !== state.windowId) {
161
+ return;
162
+ }
163
+ state.targetPaneId = event.paneId;
164
+ state.cwd = event.cwd;
165
+ state.lastUpdatedAt = event.timestamp;
166
+ state.renderMode = resolveRenderMode(currentPaneWidth());
167
+ if (event.type === "input_update" || event.type === "command_start" || event.type === "command_end") {
168
+ state.lastCommand = event.command;
169
+ }
170
+ if (event.type === "command_start") {
171
+ state.lastExitCode = undefined;
172
+ }
173
+ if (event.type === "command_end") {
174
+ state.lastExitCode = event.exitCode;
175
+ }
176
+ const plan = observer.observe(event, state);
177
+ state.directAddressActive =
178
+ (event.type === "input_update" || event.type === "command_start") && plan.addressedToBuddy;
179
+ if (event.type === "command_start" && plan.command) {
180
+ state.commandTrackers[event.paneId] = buildCommandTracker(event, plan.command, plan.importance);
181
+ }
182
+ if (event.type === "command_end") {
183
+ if (plan.command) {
184
+ state.commandWindows[event.paneId] = rememberCommandWindowEntry(state.commandWindows[event.paneId] ?? [], buildCommandWindowEntry({
185
+ event,
186
+ command: plan.command,
187
+ durationMs: plan.durationMs,
188
+ }));
189
+ }
190
+ delete state.commandTrackers[event.paneId];
191
+ state.directAddressActive = false;
192
+ }
193
+ applyObserverPlan(state, plan, event.timestamp);
194
+ if (plan.pulse) {
195
+ state.pulseUntilTick = state.frameTick + PULSE_TICKS;
196
+ }
197
+ rerender();
198
+ if (event.type !== "command_end") {
199
+ return;
200
+ }
201
+ const modelDecision = await observer.maybeGenerateDecision(plan, event, state);
202
+ if (!isCurrent()) {
203
+ return;
204
+ }
205
+ const modelReaction = modelDecision?.reaction;
206
+ const finalReaction = modelReaction ?? plan.reactionText;
207
+ if (modelReaction) {
208
+ setTransientReaction(state, modelReaction, Date.now());
209
+ rerender();
210
+ }
211
+ if (finalReaction && plan.command) {
212
+ state.recentReaction = {
213
+ text: finalReaction,
214
+ at: Date.now(),
215
+ topic: modelDecision?.topic,
216
+ mood: modelDecision?.mood,
217
+ };
218
+ state.recentNotableReactionAt = Date.now();
219
+ }
220
+ if (finalReaction && plan.command) {
221
+ state.memory = rememberMemoryEntry(state.memory, buildMemoryEntry({
222
+ event,
223
+ command: plan.command,
224
+ importance: plan.importance,
225
+ durationMs: plan.durationMs,
226
+ reactionText: finalReaction,
227
+ topic: modelDecision?.topic,
228
+ mood: modelDecision?.mood,
229
+ }));
230
+ }
231
+ }
232
+ catch {
233
+ // Ignore malformed events from shell hooks.
234
+ void eventVersion;
235
+ }
236
+ }
237
+ function applyObserverPlan(state, plan, observedAt) {
238
+ state.status = plan.status;
239
+ if (plan.reactionMode === "preserve") {
240
+ return;
241
+ }
242
+ if (plan.reactionMode === "clear") {
243
+ state.reactionText = undefined;
244
+ state.reactionExpiresAt = undefined;
245
+ return;
246
+ }
247
+ if (!plan.reactionText) {
248
+ state.reactionText = undefined;
249
+ state.reactionExpiresAt = undefined;
250
+ return;
251
+ }
252
+ if (plan.reactionMode === "persistent") {
253
+ state.reactionText = plan.reactionText;
254
+ state.reactionExpiresAt = undefined;
255
+ return;
256
+ }
257
+ setTransientReaction(state, plan.reactionText, observedAt);
258
+ }
259
+ function renderSidecar(state, previousLineCount) {
260
+ void previousLineCount;
261
+ const bodyLines = state.companion
262
+ ? renderCompanionSidecar(state)
263
+ : renderEmptySidecar(state);
264
+ const nextLineCount = currentPaneHeight();
265
+ const lines = bottomAlignLines(bodyLines, nextLineCount);
266
+ process.stdout.write(buildRenderBuffer(lines, nextLineCount));
267
+ return nextLineCount;
268
+ }
269
+ function renderCompanionSidecar(state) {
270
+ const companion = state.companion;
271
+ if (!companion) {
272
+ return renderEmptySidecar(state);
273
+ }
274
+ const species = SPECIES[companion.bones.species];
275
+ const eye = EYES[companion.bones.eye];
276
+ if (!species || !eye) {
277
+ return renderEmptySidecar(state);
278
+ }
279
+ const reaction = resolveReactionBubble(state, Date.now());
280
+ const spriteFrame = resolveSpriteFrameState(state.frameTick, state.status !== "idle" || reaction.text !== undefined, species.frames.length, isPulseActive(state));
281
+ const frame = species.frames[spriteFrame.frameIndex] ?? species.frames[0];
282
+ if (!frame) {
283
+ return renderEmptySidecar(state);
284
+ }
285
+ const sprite = maybeBlinkSprite(composeFrame(frame, eye.char, companion.bones.hat, companion.bones.color), eye.char, spriteFrame.blink);
286
+ const burst = renderPetBurst(state, companion.bones.color.accent);
287
+ if (state.renderMode === "narrow") {
288
+ return renderNarrowCompanion(state, reaction, eye.char, companion.bones.color.primary);
289
+ }
290
+ const paneWidth = currentPaneWidth();
291
+ const paneHeight = currentPaneHeight();
292
+ const statusHint = statusHintText(state.status, state.lastExitCode, state.language);
293
+ const dockLines = renderCompanionDock(state, sprite, burst, paneWidth, companion.bones.color.primary, companion.bones.color.accent, resolveIdleSummaryWidth(paneWidth));
294
+ const lines = [];
295
+ if (reaction.text) {
296
+ lines.push(...centerBlock(renderSpeechBubble(reaction.text, reaction.fading, companion.bones.color.primary, resolveBubbleContentWidth(paneWidth), resolveBubbleMaxLines(paneHeight, dockLines.length)), paneWidth));
297
+ lines.push("");
298
+ }
299
+ else if (statusHint) {
300
+ lines.push(dim(centerLine(statusHint, paneWidth)));
301
+ lines.push("");
302
+ }
303
+ lines.push(...dockLines);
304
+ return trimTrailingEmptyLines(lines);
305
+ }
306
+ function renderNarrowCompanion(state, reaction, eyeChar, color) {
307
+ const paneWidth = currentPaneWidth();
308
+ const face = styleCompactFace(renderCompactFace(state.companion?.bones.species ?? "tanuki", eyeChar), color, isPulseActive(state));
309
+ const rawLabel = reaction.text ?? state.companion?.soul.name ?? "EveryBuddy";
310
+ const labelLines = wrapText(rawLabel, Math.max(10, paneWidth - 2), reaction.text ? NARROW_REACTION_MAX_LINES : 1).map((line) => {
311
+ const styled = reaction.fading ? dim(line) : styleNarrowLabel(line, state, color);
312
+ return centerLine(styled, paneWidth);
313
+ });
314
+ const statusHint = !reaction.text
315
+ ? statusHintText(state.status, state.lastExitCode, state.language)
316
+ : undefined;
317
+ return trimTrailingEmptyLines([
318
+ centerLine(face, paneWidth),
319
+ ...labelLines,
320
+ ...(statusHint ? [dim(centerLine(statusHint, paneWidth))] : []),
321
+ ]);
322
+ }
323
+ function renderEmptySidecar(state) {
324
+ const paneWidth = currentPaneWidth();
325
+ const paneHeight = currentPaneHeight();
326
+ const reaction = resolveReactionBubble(state, Date.now());
327
+ const text = uiText(state.language);
328
+ const lines = [
329
+ centerLine(dim(text.noCompanionSidecar), paneWidth),
330
+ centerLine(text.runBuddyHint, paneWidth),
331
+ ];
332
+ if (reaction.text) {
333
+ lines.push("");
334
+ lines.push(...centerBlock(renderSpeechBubble(reaction.text, reaction.fading, "#9CA3AF", resolveBubbleContentWidth(paneWidth), resolveBubbleMaxLines(paneHeight, lines.length)), paneWidth));
335
+ }
336
+ return trimTrailingEmptyLines(lines);
337
+ }
338
+ function renderCompanionDock(state, sprite, burst, paneWidth, primaryColor, accentColor, summaryWidth) {
339
+ const companion = state.companion;
340
+ if (!companion) {
341
+ return [];
342
+ }
343
+ const text = uiText(state.language);
344
+ const [peakStat, peakValue] = getPeakStat(companion.bones.stats);
345
+ const [dumpStat, dumpValue] = getDumpStat(companion.bones.stats);
346
+ const localizedRarity = localizeRarityName(companion.bones.rarity.name, state.language);
347
+ const rarityLine = colorize(`◆ ${state.language === "zh" ? localizedRarity : localizedRarity.toUpperCase()} ${companion.bones.rarity.stars}`, companion.bones.rarity.color);
348
+ const projectName = state.cwd ? path.basename(state.cwd) || state.cwd : undefined;
349
+ const infoLine = dim([
350
+ localizeSpeciesName(companion.bones.species, SPECIES[companion.bones.species]?.name ?? companion.bones.species, state.language),
351
+ projectName ? text.inProject(projectName) : undefined,
352
+ ]
353
+ .filter(Boolean)
354
+ .join(" · "));
355
+ const statLine = dim(`${localizeStatName(peakStat, state.language)} ${String(peakValue).padStart(2, "0")} · ${localizeStatName(dumpStat, state.language)} ${String(dumpValue).padStart(2, "0")}`);
356
+ const summary = buildIdleSoulSummary(companion, state.language);
357
+ const personalityLines = state.reactionText === undefined
358
+ ? wrapText(summary, summaryWidth, IDLE_SUMMARY_MAX_LINES).map((line) => dim(centerLine(line, paneWidth)))
359
+ : [];
360
+ return trimTrailingEmptyLines([
361
+ centerLine(rarityLine, paneWidth),
362
+ centerLine(infoLine, paneWidth),
363
+ centerLine(statLine, paneWidth),
364
+ ...(personalityLines.length > 0 ? ["", ...personalityLines, ""] : [""]),
365
+ ...(burst.length > 0 ? centerBlock(burst, paneWidth) : []),
366
+ ...centerBlock(sprite, paneWidth),
367
+ "",
368
+ centerLine(styleCompanionName(companion.soul.name, state, primaryColor), paneWidth),
369
+ centerLine(dim(colorize(localizedRarity, accentColor)), paneWidth),
370
+ ]);
371
+ }
372
+ function renderSpeechBubble(text, fading, color, contentWidth, maxLines) {
373
+ const wrapped = wrapText(text, contentWidth, maxLines);
374
+ const innerWidth = Math.max(...wrapped.map((line) => visibleLength(line)), 8);
375
+ const borderTop = styleBubbleBorder(`╭${"─".repeat(innerWidth + 2)}╮`, color, fading);
376
+ const borderBottom = styleBubbleBorder(`╰${"─".repeat(innerWidth + 2)}╯`, color, fading);
377
+ const tailPadding = " ".repeat(Math.max(0, Math.floor((innerWidth + 4) / 2)));
378
+ return [
379
+ borderTop,
380
+ ...wrapped.map((line) => {
381
+ const textLine = `${styleBubbleBorder("│", color, fading)} ${styleBubbleText(padDisplayWidth(line, innerWidth), fading)} ${styleBubbleBorder("│", color, fading)}`;
382
+ return textLine;
383
+ }),
384
+ borderBottom,
385
+ `${tailPadding}${styleBubbleBorder("╲", color, fading)}`,
386
+ ];
387
+ }
388
+ function resolveBubbleContentWidth(paneWidth) {
389
+ return Math.max(16, Math.min(BUBBLE_MAX_WIDTH, paneWidth - 6));
390
+ }
391
+ function resolveBubbleMaxLines(paneHeight, dockLineCount) {
392
+ const available = paneHeight - dockLineCount - 4;
393
+ return Math.max(2, Math.min(BUBBLE_MAX_LINES, available));
394
+ }
395
+ function resolveIdleSummaryWidth(paneWidth) {
396
+ return Math.max(16, Math.min(IDLE_SUMMARY_WIDTH, paneWidth - 6));
397
+ }
398
+ function styleBubbleBorder(text, color, fading) {
399
+ const colored = colorize(text, color);
400
+ return fading ? dim(colored) : colored;
401
+ }
402
+ function styleBubbleText(text, fading) {
403
+ return fading ? dim(text) : text;
404
+ }
405
+ function styleCompanionName(name, state, color) {
406
+ const styledName = italic(name);
407
+ if (state.directAddressActive || isPulseActive(state)) {
408
+ return bold(italic(colorize(` ${name} `, color)));
409
+ }
410
+ if (state.reactionText) {
411
+ return italic(colorize(name, color));
412
+ }
413
+ return dim(styledName);
414
+ }
415
+ function styleCompactFace(text, color, pulsing) {
416
+ const colored = colorize(text, color);
417
+ return pulsing ? bold(colored) : colored;
418
+ }
419
+ function styleNarrowLabel(text, state, color) {
420
+ if (state.directAddressActive || state.reactionText) {
421
+ return colorize(text, color);
422
+ }
423
+ return dim(text);
424
+ }
425
+ function statusHintText(status, exitCode, language) {
426
+ const text = uiText(language);
427
+ if (status === "thinking") {
428
+ return text.thinking;
429
+ }
430
+ if (status === "finished" && exitCode !== undefined && exitCode !== 0) {
431
+ return text.commandFailed;
432
+ }
433
+ return undefined;
434
+ }
435
+ export function resolveReactionBubble(state, now) {
436
+ if (!state.reactionText) {
437
+ return { text: undefined, fading: false };
438
+ }
439
+ if (state.reactionExpiresAt === undefined) {
440
+ return { text: state.reactionText, fading: false };
441
+ }
442
+ if (now >= state.reactionExpiresAt) {
443
+ return { text: undefined, fading: false };
444
+ }
445
+ return {
446
+ text: state.reactionText,
447
+ fading: now >= state.reactionExpiresAt - FADE_WINDOW_MS,
448
+ };
449
+ }
450
+ export function resolveSpriteFrameState(frameTick, animated, frameCount, pulsing = false) {
451
+ if (frameCount <= 1) {
452
+ return { frameIndex: 0, blink: false };
453
+ }
454
+ if (animated || pulsing) {
455
+ return { frameIndex: frameTick % frameCount, blink: false };
456
+ }
457
+ const sequenceStep = IDLE_SEQUENCE[frameTick % IDLE_SEQUENCE.length] ?? 0;
458
+ if (sequenceStep === -1) {
459
+ return { frameIndex: 0, blink: true };
460
+ }
461
+ return { frameIndex: sequenceStep % frameCount, blink: false };
462
+ }
463
+ function wrapText(text, width, maxLines) {
464
+ if (text.trim().length === 0) {
465
+ return [""];
466
+ }
467
+ const words = splitWrapTokens(text);
468
+ const joiner = /\s/.test(text.trim()) ? " " : "";
469
+ const lines = [];
470
+ let current = "";
471
+ for (const word of words) {
472
+ const candidate = current.length === 0 ? word : `${current}${joiner}${word}`;
473
+ if (visibleLength(candidate) <= width) {
474
+ current = candidate;
475
+ continue;
476
+ }
477
+ if (current.length === 0 && visibleLength(word) > width) {
478
+ const [head, tail] = splitTokenByDisplayWidth(word, width);
479
+ lines.push(head);
480
+ current = tail;
481
+ if (lines.length >= maxLines - 1) {
482
+ break;
483
+ }
484
+ continue;
485
+ }
486
+ if (current.length > 0) {
487
+ lines.push(current);
488
+ }
489
+ current = word;
490
+ if (lines.length >= maxLines - 1) {
491
+ break;
492
+ }
493
+ }
494
+ if (current.length > 0 && lines.length < maxLines) {
495
+ lines.push(current);
496
+ }
497
+ if (lines.length === 0) {
498
+ lines.push(splitTokenByDisplayWidth(text, width)[0]);
499
+ }
500
+ return lines.slice(0, maxLines).map((line, index, allLines) => {
501
+ if (index === allLines.length - 1 && visibleLength(line) > width) {
502
+ return truncateDisplayWidth(line, width);
503
+ }
504
+ if (index === maxLines - 1 && visibleLength(words.join(joiner)) > visibleLength(lines.join(joiner))) {
505
+ return truncateDisplayWidth(line, width);
506
+ }
507
+ return line;
508
+ });
509
+ }
510
+ function expireReactionIfNeeded(state, now) {
511
+ if (state.reactionExpiresAt === undefined || now < state.reactionExpiresAt) {
512
+ return;
513
+ }
514
+ state.reactionText = undefined;
515
+ state.reactionExpiresAt = undefined;
516
+ if (state.status === "finished") {
517
+ state.status = "idle";
518
+ state.lastExitCode = undefined;
519
+ }
520
+ }
521
+ function setTransientReaction(state, text, startedAt = Date.now()) {
522
+ state.reactionText = text;
523
+ state.reactionExpiresAt = startedAt + BUBBLE_SHOW_MS;
524
+ }
525
+ function maybeBlinkSprite(sprite, eyeChar, blink) {
526
+ if (!blink) {
527
+ return sprite;
528
+ }
529
+ return sprite.map((line) => line.replaceAll(eyeChar, "-"));
530
+ }
531
+ function buildRenderBuffer(lines, lineCount) {
532
+ const output = ["\u001B[H"];
533
+ for (let index = 0; index < lineCount; index += 1) {
534
+ output.push("\u001B[2K");
535
+ output.push(lines[index] ?? "");
536
+ if (index < lineCount - 1) {
537
+ output.push("\n");
538
+ }
539
+ }
540
+ return output.join("");
541
+ }
542
+ function centerLine(text, width) {
543
+ const visible = visibleLength(text);
544
+ const left = Math.max(0, Math.floor((width - visible) / 2));
545
+ return `${" ".repeat(left)}${text}`;
546
+ }
547
+ function visibleLength(value) {
548
+ const stripped = value.replace(/\u001B\[[0-9;]*m/g, "");
549
+ return Array.from(stripped).reduce((sum, char) => sum + charDisplayWidth(char), 0);
550
+ }
551
+ function currentPaneWidth() {
552
+ return process.stdout.columns ?? DEFAULT_PANE_WIDTH;
553
+ }
554
+ function currentPaneHeight() {
555
+ return process.stdout.rows ?? DEFAULT_PANE_HEIGHT;
556
+ }
557
+ function isPulseActive(state) {
558
+ return state.pulseUntilTick !== undefined && state.frameTick < state.pulseUntilTick;
559
+ }
560
+ function trimTrailingEmptyLines(lines) {
561
+ const output = [...lines];
562
+ while (output.length > 0 && output[output.length - 1]?.trim().length === 0) {
563
+ output.pop();
564
+ }
565
+ return output;
566
+ }
567
+ function bottomAlignLines(lines, height) {
568
+ if (height <= 0) {
569
+ return lines;
570
+ }
571
+ const trimmed = trimTrailingEmptyLines(lines);
572
+ if (trimmed.length >= height) {
573
+ return trimmed.slice(trimmed.length - height);
574
+ }
575
+ return [...Array.from({ length: height - trimmed.length }, () => ""), ...trimmed];
576
+ }
577
+ function renderPetBurst(state, color) {
578
+ if (!isPulseActive(state)) {
579
+ return [];
580
+ }
581
+ const frame = PET_BURST_FRAMES[state.frameTick % PET_BURST_FRAMES.length] ?? [" ♥ ♥ "];
582
+ return frame.map((line) => colorize(line, color));
583
+ }
584
+ function centerBlock(lines, width) {
585
+ const blockWidth = Math.max(0, ...lines.map((line) => visibleLength(line)));
586
+ const left = Math.max(0, Math.floor((width - blockWidth) / 2));
587
+ return lines.map((line) => `${" ".repeat(left)}${line}`);
588
+ }
589
+ function splitWrapTokens(text) {
590
+ const trimmed = text.trim();
591
+ if (!trimmed) {
592
+ return [""];
593
+ }
594
+ if (/\s/.test(trimmed)) {
595
+ return trimmed.split(/\s+/);
596
+ }
597
+ return Array.from(trimmed);
598
+ }
599
+ export function buildIdleSoulSummary(companion, language = inferCompanionLanguage(companion)) {
600
+ const tagline = getLocalizedSoulCopy(companion, language).tagline?.trim();
601
+ if (tagline) {
602
+ return tagline;
603
+ }
604
+ const profile = companion.soul.observerProfile;
605
+ const [peakStat] = getPeakStat(companion.bones.stats);
606
+ const [dumpStat] = getDumpStat(companion.bones.stats);
607
+ if (language === "zh") {
608
+ return `${localizeVoiceLabel(profile.voice, language)} · ${localizeStatName(peakStat, language)}强,${localizeStatName(dumpStat, language)}低`;
609
+ }
610
+ return `${localizeVoiceLabel(profile.voice, language)} · high ${localizeStatName(peakStat, language).toLowerCase()} · low ${localizeStatName(dumpStat, language).toLowerCase()}`;
611
+ }
612
+ function inferCompanionLanguage(companion) {
613
+ return /[\u3400-\u9fff]/u.test(companion.soul.personality) ? "zh" : "en";
614
+ }
615
+ function splitTokenByDisplayWidth(token, width) {
616
+ let head = "";
617
+ let used = 0;
618
+ const chars = Array.from(token);
619
+ for (const char of chars) {
620
+ const next = charDisplayWidth(char);
621
+ if (used + next > width && head.length > 0) {
622
+ break;
623
+ }
624
+ head += char;
625
+ used += next;
626
+ if (used >= width) {
627
+ break;
628
+ }
629
+ }
630
+ return [head, token.slice(head.length)];
631
+ }
632
+ function truncateDisplayWidth(value, width) {
633
+ const ellipsis = "…";
634
+ const target = Math.max(0, width - visibleLength(ellipsis));
635
+ let output = "";
636
+ let used = 0;
637
+ for (const char of Array.from(value)) {
638
+ const next = charDisplayWidth(char);
639
+ if (used + next > target) {
640
+ break;
641
+ }
642
+ output += char;
643
+ used += next;
644
+ }
645
+ return `${output}${ellipsis}`;
646
+ }
647
+ function padDisplayWidth(value, width) {
648
+ const padding = Math.max(0, width - visibleLength(value));
649
+ return `${value}${" ".repeat(padding)}`;
650
+ }
651
+ function charDisplayWidth(char) {
652
+ if (/[\u0300-\u036f]/u.test(char)) {
653
+ return 0;
654
+ }
655
+ if (/[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\u30ff\u3100-\u312f\u3130-\u318f\u3190-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/u.test(char)) {
656
+ return 2;
657
+ }
658
+ return 1;
659
+ }
660
+ function hideCursor() {
661
+ if (process.stdout.isTTY) {
662
+ process.stdout.write("\u001B[?25l");
663
+ }
664
+ }
665
+ function showCursor() {
666
+ if (process.stdout.isTTY) {
667
+ process.stdout.write("\u001B[?25h");
668
+ }
669
+ }
670
+ //# sourceMappingURL=sidecar.js.map