@duckmind/dm-darwin-arm64 0.35.4 → 0.35.5

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/dm +0 -0
  2. package/extensions/.dm-extensions.json +1 -27
  3. package/package.json +1 -1
  4. package/extensions/dm-alps/LICENSE +0 -21
  5. package/extensions/dm-alps/README.md +0 -22
  6. package/extensions/dm-alps/index.ts +0 -172
  7. package/extensions/dm-alps/package.json +0 -49
  8. package/extensions/dm-alps/src/commands.ts +0 -208
  9. package/extensions/dm-alps/src/features/animations/debug.ts +0 -160
  10. package/extensions/dm-alps/src/features/animations/index.ts +0 -33
  11. package/extensions/dm-alps/src/features/animations/patch.ts +0 -112
  12. package/extensions/dm-alps/src/features/animations/preview.ts +0 -117
  13. package/extensions/dm-alps/src/features/animations/registry.ts +0 -593
  14. package/extensions/dm-alps/src/features/animations/runtime.ts +0 -574
  15. package/extensions/dm-alps/src/features/animations/settings.ts +0 -69
  16. package/extensions/dm-alps/src/features/bottom-input/cluster.ts +0 -2
  17. package/extensions/dm-alps/src/features/bottom-input/compositor.ts +0 -2
  18. package/extensions/dm-alps/src/features/bottom-input/editor.ts +0 -148
  19. package/extensions/dm-alps/src/features/bottom-input/frame.ts +0 -224
  20. package/extensions/dm-alps/src/features/bottom-input/icons.ts +0 -36
  21. package/extensions/dm-alps/src/features/bottom-input/index.ts +0 -8
  22. package/extensions/dm-alps/src/features/bottom-input/runtime.ts +0 -1197
  23. package/extensions/dm-alps/src/features/bottom-input/shortcuts.ts +0 -286
  24. package/extensions/dm-alps/src/features/bottom-input/status.ts +0 -663
  25. package/extensions/dm-alps/src/features/bottom-status/index.ts +0 -2
  26. package/extensions/dm-alps/src/features/chrome-frame/chrome.ts +0 -222
  27. package/extensions/dm-alps/src/features/chrome-frame/debug.ts +0 -212
  28. package/extensions/dm-alps/src/features/chrome-frame/image.ts +0 -11
  29. package/extensions/dm-alps/src/features/chrome-frame/index.ts +0 -4
  30. package/extensions/dm-alps/src/features/chrome-frame/osc.ts +0 -111
  31. package/extensions/dm-alps/src/features/chrome-frame/patch.ts +0 -769
  32. package/extensions/dm-alps/src/features/chrome-frame/preview.ts +0 -67
  33. package/extensions/dm-alps/src/features/chrome-frame/styles.ts +0 -105
  34. package/extensions/dm-alps/src/features/fixed-bottom-editor/cluster.ts +0 -161
  35. package/extensions/dm-alps/src/features/fixed-bottom-editor/compositor.ts +0 -1149
  36. package/extensions/dm-alps/src/features/fixed-bottom-editor/index.ts +0 -3
  37. package/extensions/dm-alps/src/features/fixed-bottom-editor/runtime.ts +0 -3
  38. package/extensions/dm-alps/src/settings-store.ts +0 -194
  39. package/extensions/dm-alps/src/settings-ui.ts +0 -653
  40. package/extensions/dm-alps/src/settings.ts +0 -102
  41. package/extensions/dm-alps/src/terminal-sanitizer.ts +0 -91
  42. package/extensions/dm-alps/themes/LICENSE.synthwave-84 +0 -21
  43. package/extensions/dm-alps/themes/alps.json +0 -93
@@ -1,663 +0,0 @@
1
-
2
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
- import { sanitizeTerminalSingleLineText, sanitizeTerminalText } from "../../terminal-sanitizer.ts";
4
- import type { ThemeLike } from "../chrome-frame/styles.ts";
5
- import { getBottomInputIcons, type BottomInputIconSet } from "./icons.ts";
6
-
7
- export type AssistantUsage = {
8
- input: number;
9
- output: number;
10
- cacheRead: number;
11
- cacheWrite: number;
12
- totalTokens?: number;
13
- cost?: { total?: number };
14
- };
15
-
16
- export type ContextUsage = {
17
- tokens: number;
18
- contextWindow?: number;
19
- percent?: number;
20
- };
21
-
22
- export type DuckmindCreditsSnapshot = {
23
- usage: number | null;
24
- remaining: number | null;
25
- limit: number | null;
26
- isFreeTier?: boolean | null;
27
- fetchedAt?: number | null;
28
- };
29
-
30
- export type BottomInputStatusState = {
31
- ctx: any;
32
- footerData?: any;
33
- theme: ThemeLike;
34
- width: number;
35
- beautifiedInputEnabled: boolean;
36
- isStreaming: boolean;
37
- liveUsage: AssistantUsage | null;
38
- latestAssistantUsage: AssistantUsage | null;
39
- currentThinkingLevel: string | null;
40
- sessionStartTime: number;
41
- now: number;
42
- lastPrompt: string;
43
- icons?: BottomInputIconSet;
44
- };
45
-
46
- export type BottomInputFrameStatus = {
47
- model: string | null;
48
- thinking: string | null;
49
- context: string | null;
50
- elapsed: string | null;
51
- };
52
-
53
- export type BottomInputStatusRender = {
54
- topLines: string[];
55
- secondaryLines: string[];
56
- lastPromptLines: string[];
57
- frameStatus: BottomInputFrameStatus;
58
- cacheKey: string;
59
- };
60
-
61
- export const CONTEXT_BAR_WIDTH = 10;
62
- const CREDIT_BAR_WIDTH = 10;
63
- const CONTEXT_COLORS = {
64
- normal: "#00afaf",
65
- warning: "#febc38",
66
- error: "#ff5f5f",
67
- empty: "#444444",
68
- };
69
- const RAINBOW_COLORS = [
70
- "#b281d6",
71
- "#d787af",
72
- "#febc38",
73
- "#e4c00f",
74
- "#89d281",
75
- "#00afaf",
76
- "#178fb9",
77
- "#b281d6",
78
- ];
79
- const INTERNAL_STATUS_KEYS = new Set(["dm-alps-bottom-input", "dm-alps-bottom-status", "dm-alps-last-prompt"]);
80
-
81
- export function renderBottomInputStatus(input: BottomInputStatusState): BottomInputStatusRender {
82
- const safeWidth = Math.max(1, Math.floor(input.width));
83
- const enabled = input.beautifiedInputEnabled;
84
- const extensionStatuses = getVisibleExtensionStatuses(input.footerData);
85
- const usage = readContextUsageSnapshot(input.ctx, input.isStreaming, input.liveUsage, input.latestAssistantUsage);
86
- const lastPromptStatusLines = renderLastPromptStatusLines(input, usage, safeWidth);
87
- if (!enabled) {
88
- return {
89
- topLines: [],
90
- secondaryLines: renderExtensionStatusLines(extensionStatuses, safeWidth, input.theme),
91
- lastPromptLines: [...lastPromptStatusLines, ...renderLastPromptLines(input.lastPrompt, safeWidth, input.theme)],
92
- frameStatus: emptyFrameStatus(),
93
- cacheKey: JSON.stringify({
94
- width: safeWidth,
95
- enabled: false,
96
- lastPrompt: input.lastPrompt,
97
- extensionStatuses,
98
- lastPromptStatusLines: lastPromptStatusLines.map(stripAnsi),
99
- }),
100
- };
101
- }
102
-
103
- const icons = input.icons ?? getBottomInputIcons();
104
- const elapsedSeconds = Math.floor(Math.max(0, input.now - input.sessionStartTime) / 1000);
105
- const modelName = readModelName(input.ctx);
106
- const thinking = readThinkingLevel(input.ctx) ?? input.currentThinkingLevel ?? readThinkingLevelFromSession(input.ctx);
107
- const cacheKey = JSON.stringify({
108
- width: safeWidth,
109
- beautifiedInputEnabled: enabled,
110
- model: modelName,
111
- thinking,
112
- context: usage,
113
- elapsedSeconds,
114
- lastPrompt: input.lastPrompt,
115
- extensionStatuses,
116
- lastPromptStatusLines: lastPromptStatusLines.map(stripAnsi),
117
- icons,
118
- });
119
-
120
- return {
121
- topLines: [],
122
- secondaryLines: renderExtensionStatusLines(extensionStatuses, safeWidth, input.theme),
123
- lastPromptLines: [...lastPromptStatusLines, ...renderLastPromptLines(input.lastPrompt, safeWidth, input.theme)],
124
- frameStatus: renderFrameStatus({ ...input, width: safeWidth, icons }, modelName, thinking, usage),
125
- cacheKey,
126
- };
127
- }
128
-
129
- export function renderFrameStatus(input: BottomInputStatusState & { icons?: BottomInputIconSet }, modelName = readModelName(input.ctx), thinkingLevel = readThinkingLevel(input.ctx) ?? input.currentThinkingLevel ?? readThinkingLevelFromSession(input.ctx), usage = readContextUsageSnapshot(input.ctx, input.isStreaming, input.liveUsage, input.latestAssistantUsage)): BottomInputFrameStatus {
130
- return {
131
- model: renderModelSegment(modelName, input.theme, input.icons ?? getBottomInputIcons()),
132
- thinking: renderThinkingSegment(thinkingLevel, input.theme),
133
- context: null,
134
- elapsed: renderElapsedSegment(input.theme, input.sessionStartTime, input.now, input.icons ?? getBottomInputIcons()),
135
- };
136
- }
137
-
138
- export function renderExtensionStatusLines(statuses: readonly string[], width: number, theme: ThemeLike): string[] {
139
- const safeStatuses = statuses.map((status) => sanitizeTerminalSingleLineText(status, { preserveSgr: false })).filter(Boolean);
140
- if (safeStatuses.length === 0) return [];
141
- const separator = safeFg(theme, "borderMuted", " › ");
142
- const line = ` ${safeStatuses.join(separator)} `;
143
- return [truncateToWidth(line, Math.max(1, Math.floor(width)), "…", false)];
144
- }
145
-
146
- export function renderLastPromptLines(prompt: string, width: number, theme: ThemeLike): string[] {
147
- const safeWidth = Math.max(1, Math.floor(width));
148
- const safePrompt = sanitizeTerminalSingleLineText(prompt, { preserveSgr: false });
149
- if (!safePrompt) return [];
150
-
151
- const prefix = ` ${safeFg(theme, "borderMuted", "↳")} `;
152
- const availableWidth = safeWidth - visibleWidth(prefix);
153
- if (availableWidth < 4) return [];
154
-
155
- const value = truncateToWidth(safePrompt, availableWidth, "…", false);
156
- return [truncateToWidth(`${prefix}${safeFg(theme, "muted", value)}`, safeWidth, "…", false)];
157
- }
158
-
159
- export function renderLastPromptStatusLines(input: BottomInputStatusState, usage = readContextUsageSnapshot(input.ctx, input.isStreaming, input.liveUsage, input.latestAssistantUsage), width = input.width): string[] {
160
- const safeWidth = Math.max(1, Math.floor(width));
161
- if (!normalizePromptText(input.lastPrompt)) return [];
162
- if (safeWidth < 10) return [];
163
-
164
- const statusLine = renderContextCostLine(input, usage, safeWidth);
165
- const creditsLine = renderDuckmindCreditsLine(input, safeWidth);
166
- return [statusLine, creditsLine].filter((line): line is string => typeof line === "string" && visibleWidth(stripAnsi(line)) > 0);
167
- }
168
-
169
- export function getVisibleExtensionStatuses(footerData: any): string[] {
170
- let statuses: unknown;
171
- try {
172
- statuses = footerData?.getExtensionStatuses?.();
173
- } catch {
174
- return [];
175
- }
176
- const entries: Array<[unknown, unknown]> = statuses instanceof Map
177
- ? [...statuses.entries()]
178
- : isRecord(statuses)
179
- ? Object.entries(statuses)
180
- : [];
181
- const visible: string[] = [];
182
- for (const [key, value] of entries) {
183
- if (typeof key === "string" && INTERNAL_STATUS_KEYS.has(key)) continue;
184
- if (typeof value !== "string") continue;
185
- const normalized = sanitizeTerminalSingleLineText(value, { preserveSgr: false });
186
- if (!normalized) continue;
187
- if (normalized.trimStart().startsWith("[")) continue;
188
- if (visibleWidth(stripAnsi(normalized)) <= 0) continue;
189
- visible.push(normalized);
190
- }
191
- return visible;
192
- }
193
-
194
- export function normalizePromptText(value: unknown): string {
195
- return sanitizeTerminalSingleLineText(value, { preserveSgr: false });
196
- }
197
-
198
- export function isAssistantUsage(value: unknown): value is AssistantUsage {
199
- return isRecord(value)
200
- && typeof value.input === "number"
201
- && typeof value.output === "number"
202
- && typeof value.cacheRead === "number"
203
- && typeof value.cacheWrite === "number";
204
- }
205
-
206
- export function getUsageTokenTotal(usage: AssistantUsage): number {
207
- return typeof usage.totalTokens === "number" && usage.totalTokens > 0
208
- ? usage.totalTokens
209
- : usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
210
- }
211
-
212
- export function readContextUsageSnapshot(
213
- ctx: any,
214
- isStreaming: boolean,
215
- liveUsage: AssistantUsage | null,
216
- latestAssistantUsage: AssistantUsage | null,
217
- ): ContextUsage | null {
218
- const coreUsage = isStreaming && liveUsage ? null : readCoreContextUsage(ctx);
219
- const assistantUsage = liveUsage ?? latestAssistantUsage ?? readLatestAssistantUsage(ctx);
220
- const tokens = coreUsage?.tokens ?? (assistantUsage ? getUsageTokenTotal(assistantUsage) : 0);
221
- const contextWindow = coreUsage?.contextWindow ?? readModelContextWindow(ctx);
222
- const percent = coreUsage?.percent ?? (contextWindow > 0 ? (tokens / contextWindow) * 100 : undefined);
223
-
224
- if (!Number.isFinite(tokens) || tokens <= 0) return null;
225
- return {
226
- tokens,
227
- contextWindow: contextWindow > 0 ? contextWindow : undefined,
228
- percent: typeof percent === "number" && Number.isFinite(percent) ? percent : undefined,
229
- };
230
- }
231
-
232
- function emptyFrameStatus(): BottomInputFrameStatus {
233
- return { model: null, thinking: null, context: null, elapsed: null };
234
- }
235
-
236
- function renderModelSegment(modelName: string | null, theme: ThemeLike, _icons: BottomInputIconSet): string | null {
237
- if (!modelName) return null;
238
- return safeFg(theme, "accent", modelName);
239
- }
240
-
241
- function renderThinkingSegment(level: string | null, theme: ThemeLike): string | null {
242
- if (!level) return null;
243
- const label = normalizeThinkingLevel(level);
244
- if (!label) return null;
245
- if (level === "high" || level === "xhigh") {
246
- return rainbow(label);
247
- }
248
- return safeFg(theme, thinkingColorToken(level), label);
249
- }
250
-
251
- function renderContextSegment(usage: ContextUsage | null): string | null {
252
- if (!usage || usage.tokens <= 0) return null;
253
-
254
- if (usage.contextWindow && usage.contextWindow > 0) {
255
- const percent = typeof usage.percent === "number" && Number.isFinite(usage.percent)
256
- ? usage.percent
257
- : (usage.tokens / usage.contextWindow) * 100;
258
- const color = contextColor(percent);
259
- const value = applyHexColor(color, `${percent.toFixed(1)}%/${formatTokens(usage.contextWindow)}`);
260
- return `${renderContextBar(percent, color)} ${value}`;
261
- }
262
-
263
- return applyHexColor(CONTEXT_COLORS.normal, formatTokens(usage.tokens));
264
- }
265
-
266
- function renderContextCostLine(input: BottomInputStatusState, usage: ContextUsage | null, width: number): string {
267
- const contextText = formatContextStatus(input.ctx, usage);
268
- const costInfo = readModelCostInfo(input.ctx);
269
- const costText = formatModelCostInline(costInfo);
270
- const left = safeFg(input.theme, "muted", contextText);
271
- const right = safeFg(input.theme, costInfo.color, costText, costInfo.color === "success" ? "accent" : "text");
272
- return renderEdgeAlignedLine(left, right, width);
273
- }
274
-
275
- function renderDuckmindCreditsLine(input: BottomInputStatusState, width: number): string | null {
276
- if (!isDuckmindProvider(input.ctx)) return null;
277
- const credits = readDuckmindCredits(input.footerData);
278
- if (!credits) return null;
279
-
280
- const left = [
281
- safeFg(input.theme, "muted", "Credits Remaining"),
282
- renderCreditProgressBar(credits, input.theme),
283
- safeFg(input.theme, "accent", formatCreditNumber(credits.remaining)),
284
- ].join(" ");
285
- const right = `${safeFg(input.theme, "muted", "Credits Used")} ${safeFg(input.theme, "accent", formatCreditNumber(credits.usage))} ${safeFg(input.theme, "muted", "• Credit")}`;
286
- return renderEdgeAlignedLine(left, right, width);
287
- }
288
-
289
- function formatContextStatus(ctx: any, usage: ContextUsage | null): string {
290
- const tokens = usage?.tokens ?? 0;
291
- const contextWindow = usage?.contextWindow ?? readModelContextWindow(ctx);
292
- const mode = `(${readContextManagementMode(ctx)})`;
293
- if (contextWindow > 0) {
294
- const usagePercent = usage?.percent;
295
- const percent = typeof usagePercent === "number" && Number.isFinite(usagePercent)
296
- ? usagePercent
297
- : (tokens / contextWindow) * 100;
298
- return `${Math.max(0, percent).toFixed(1)}%/${formatTokens(contextWindow)} ${mode}`;
299
- }
300
- return `${formatTokens(tokens)} ${mode}`;
301
- }
302
-
303
- function readContextManagementMode(ctx: any): string {
304
- const candidates: unknown[] = [];
305
- try {
306
- candidates.push(ctx?.getAutoCompactionEnabled?.());
307
- } catch {
308
- // Ignore optional API probes; older runtimes do not expose this value.
309
- }
310
- try {
311
- candidates.push(ctx?.autoCompactionEnabled, ctx?.autoCompactEnabled, ctx?.session?.autoCompactionEnabled, ctx?.session?.autoCompactEnabled);
312
- } catch {
313
- // Keep the display stable if the host shape changes.
314
- }
315
- for (const candidate of candidates) {
316
- if (candidate === true) return "auto";
317
- if (candidate === false) return "manual";
318
- if (typeof candidate === "string") {
319
- const normalized = candidate.trim().toLowerCase();
320
- if (normalized === "auto" || normalized === "manual" || normalized === "off") return normalized;
321
- }
322
- }
323
- return "auto";
324
- }
325
-
326
- function renderElapsedSegment(theme: ThemeLike, startedAt: number, now: number, icons: BottomInputIconSet): string | null {
327
- const elapsed = Math.max(0, now - startedAt);
328
- if (elapsed < 1000) return null;
329
- return safeFg(theme, "muted", `◷ ${formatDuration(elapsed)}`);
330
- }
331
-
332
- function normalizeModelName(value: unknown): string | null {
333
- if (typeof value !== "string") return null;
334
- let modelName = value.trim();
335
- if (!modelName) return null;
336
- if (modelName.includes("/")) modelName = modelName.split("/").filter(Boolean).at(-1) ?? modelName;
337
- if (modelName.includes(":")) modelName = modelName.split(":").filter(Boolean).at(-1) ?? modelName;
338
- if (modelName.startsWith("Claude ")) modelName = modelName.slice("Claude ".length);
339
- return modelName.trim() || null;
340
- }
341
-
342
- function readThinkingLevel(ctx: any): string | null {
343
- try {
344
- const level = ctx?.getThinkingLevel?.();
345
- return typeof level === "string" && level ? level : null;
346
- } catch {
347
- return null;
348
- }
349
- }
350
-
351
- function readThinkingLevelFromSession(ctx: any): string | null {
352
- let latest: string | null = null;
353
- for (const entry of readBranchEntries(ctx)) {
354
- if (isRecord(entry) && entry.type === "thinking_level_change" && typeof entry.thinkingLevel === "string" && entry.thinkingLevel) {
355
- latest = entry.thinkingLevel;
356
- }
357
- }
358
- return latest;
359
- }
360
-
361
- function normalizeThinkingLevel(level: string): string {
362
- const labels: Record<string, string> = {
363
- off: "off",
364
- minimal: "min",
365
- low: "low",
366
- medium: "med",
367
- high: "high",
368
- xhigh: "xhigh",
369
- };
370
- return labels[level] ?? level;
371
- }
372
-
373
- function thinkingColorToken(level: string): string {
374
- const tokens: Record<string, string> = {
375
- off: "thinking",
376
- minimal: "thinkingMinimal",
377
- low: "thinkingLow",
378
- medium: "thinkingMedium",
379
- };
380
- return tokens[level] ?? "thinking";
381
- }
382
-
383
- function readCoreContextUsage(ctx: any): ContextUsage | null {
384
- try {
385
- if (typeof ctx?.getContextUsage !== "function") return null;
386
- const usage = ctx.getContextUsage();
387
- if (!isRecord(usage)) return null;
388
- const tokens = usage.tokens;
389
- if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0) return null;
390
- const contextWindow = usage.contextWindow;
391
- const percent = usage.percent;
392
- return {
393
- tokens,
394
- contextWindow: typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : undefined,
395
- percent: typeof percent === "number" && Number.isFinite(percent) ? percent : undefined,
396
- };
397
- } catch {
398
- return null;
399
- }
400
- }
401
-
402
- function readLatestAssistantUsage(ctx: any): AssistantUsage | null {
403
- let latestUsage: AssistantUsage | null = null;
404
- for (const entry of readBranchEntries(ctx)) {
405
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
406
- if (entry.message.role !== "assistant" || !isAssistantUsage(entry.message.usage)) continue;
407
- if (entry.message.stopReason === "error" || entry.message.stopReason === "aborted") continue;
408
- if (getUsageTokenTotal(entry.message.usage) > 0) latestUsage = entry.message.usage;
409
- }
410
- return latestUsage;
411
- }
412
-
413
- function readBranchEntries(ctx: any): any[] {
414
- try {
415
- const entries = ctx?.sessionManager?.getBranch?.();
416
- return Array.isArray(entries) ? entries : [];
417
- } catch {
418
- return [];
419
- }
420
- }
421
-
422
- function readModelContextWindow(ctx: any): number {
423
- try {
424
- const contextWindow = ctx?.model?.contextWindow;
425
- return typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 0;
426
- } catch {
427
- return 0;
428
- }
429
- }
430
-
431
- function readModelName(ctx: any): string | null {
432
- try {
433
- return normalizeModelName(ctx?.model?.name || ctx?.model?.id);
434
- } catch {
435
- return null;
436
- }
437
- }
438
-
439
- type ModelCostInfo = {
440
- label: string;
441
- color: "success" | "muted" | "warning" | "error";
442
- inputPerMillion: number | null;
443
- outputPerMillion: number | null;
444
- };
445
-
446
- function readModelCostInfo(ctx: any): ModelCostInfo {
447
- let model: any;
448
- try {
449
- model = ctx?.model;
450
- } catch {
451
- model = null;
452
- }
453
- if (model?.id === "@preset/free") return classifyModelCost(0, 0);
454
- const input = toFiniteNumber(model?.cost?.input);
455
- const output = toFiniteNumber(model?.cost?.output);
456
- return classifyModelCost(input, output);
457
- }
458
-
459
- function classifyModelCost(inputPerMillion: number | null, outputPerMillion: number | null): ModelCostInfo {
460
- if (inputPerMillion === null || outputPerMillion === null) {
461
- return { label: "Unknown Cost", color: "muted", inputPerMillion: null, outputPerMillion: null };
462
- }
463
- if (inputPerMillion === 0 && outputPerMillion === 0) {
464
- return { label: "Free", color: "success", inputPerMillion, outputPerMillion };
465
- }
466
- if (inputPerMillion > 20 || outputPerMillion > 100) {
467
- return { label: "Critical Cost", color: "error", inputPerMillion, outputPerMillion };
468
- }
469
- if (inputPerMillion > 10 || outputPerMillion > 30) {
470
- return { label: "High Cost", color: "error", inputPerMillion, outputPerMillion };
471
- }
472
- if ((inputPerMillion >= 1 && inputPerMillion <= 10) || (outputPerMillion >= 5 && outputPerMillion <= 30)) {
473
- return { label: "Moderate Cost", color: "warning", inputPerMillion, outputPerMillion };
474
- }
475
- if (inputPerMillion < 1 && outputPerMillion < 5) {
476
- return { label: "Low Cost", color: "success", inputPerMillion, outputPerMillion };
477
- }
478
- return { label: "Unknown Cost", color: "muted", inputPerMillion, outputPerMillion };
479
- }
480
-
481
- function formatModelCostInline(info: ModelCostInfo): string {
482
- return `${info.label} ${formatUsd(info.inputPerMillion)}/M in · ${formatUsd(info.outputPerMillion)}/M out`;
483
- }
484
-
485
- function formatUsd(value: number | null): string {
486
- if (value === null) return "n/a";
487
- if (value === 0) return "$0";
488
- if (value >= 100) return `$${value.toFixed(0)}`;
489
- if (value >= 10) return `$${value.toFixed(1).replace(/\.0$/, "")}`;
490
- if (value >= 1) return `$${value.toFixed(2).replace(/0$/, "").replace(/\.$/, "")}`;
491
- return `$${value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}`;
492
- }
493
-
494
- function readDuckmindCredits(footerData: any): DuckmindCreditsSnapshot | null {
495
- let raw: unknown;
496
- try {
497
- raw = footerData?.getDuckmindCredits?.();
498
- } catch {
499
- return null;
500
- }
501
- if (!isRecord(raw)) return null;
502
- const usage = toFiniteNumber(raw.usage);
503
- const remaining = toFiniteNumber(raw.remaining);
504
- const limit = toFiniteNumber(raw.limit);
505
- if (usage === null && remaining === null && limit === null) return null;
506
- return {
507
- usage,
508
- remaining,
509
- limit,
510
- isFreeTier: typeof raw.isFreeTier === "boolean" ? raw.isFreeTier : null,
511
- fetchedAt: toFiniteNumber(raw.fetchedAt),
512
- };
513
- }
514
-
515
- function renderCreditProgressBar(credits: DuckmindCreditsSnapshot, theme: ThemeLike): string {
516
- const remaining = credits.remaining ?? 0;
517
- const limit = credits.limit && credits.limit > 0 ? credits.limit : (remaining > 0 ? remaining : 0);
518
- const percent = limit > 0 ? Math.max(0, Math.min(100, (remaining / limit) * 100)) : 0;
519
- const rawFilledCells = Math.floor((percent / 100) * CREDIT_BAR_WIDTH);
520
- const filledCells = percent >= 100 ? CREDIT_BAR_WIDTH : (percent > 0 ? Math.max(1, rawFilledCells) : 0);
521
- const filled = "■".repeat(filledCells);
522
- const empty = "□".repeat(Math.max(0, CREDIT_BAR_WIDTH - filledCells));
523
- const color = percent <= 15 ? "error" : percent <= 35 ? "warning" : "success";
524
- return `${safeFg(theme, color, filled, "accent")}${safeFg(theme, "borderMuted", empty)}`;
525
- }
526
-
527
- function isDuckmindProvider(ctx: any): boolean {
528
- let values: unknown[] = [];
529
- try {
530
- const model = ctx?.model;
531
- values = [
532
- model?.provider,
533
- model?.provider?.id,
534
- model?.provider?.name,
535
- model?.providerName,
536
- model?.baseUrl,
537
- model?.id,
538
- ctx?.provider,
539
- ctx?.provider?.id,
540
- ctx?.provider?.name,
541
- ctx?.provider?.baseUrl,
542
- ];
543
- } catch {
544
- return false;
545
- }
546
- return values.some((value) => typeof value === "string" && /duckmind|openrouter|@preset\//i.test(value));
547
- }
548
-
549
- function renderEdgeAlignedLine(left: string, right: string, width: number): string {
550
- const safeWidth = Math.max(1, Math.floor(width));
551
- if (safeWidth <= 1) return truncateToWidth(left || right, safeWidth, "…", false);
552
- const minGap = 2;
553
- let rightPart = right;
554
- let leftPart = left;
555
- const originalLeftWidth = visibleWidth(leftPart);
556
- const originalRightWidth = visibleWidth(rightPart);
557
- if (originalLeftWidth + minGap + originalRightWidth > safeWidth) {
558
- const rightBudget = Math.max(0, Math.min(originalRightWidth, Math.floor(safeWidth * 0.55)));
559
- rightPart = rightBudget > 0 ? truncateToWidth(rightPart, rightBudget, "…", false) : "";
560
- const leftBudget = Math.max(0, safeWidth - visibleWidth(rightPart) - (rightPart ? minGap : 0));
561
- leftPart = leftBudget > 0 ? truncateToWidth(leftPart, leftBudget, "…", false) : "";
562
- }
563
- if (!leftPart) return truncateToWidth(rightPart, safeWidth, "…", false);
564
- if (!rightPart) return truncateToWidth(leftPart, safeWidth, "…", false);
565
- const gap = Math.max(1, safeWidth - visibleWidth(leftPart) - visibleWidth(rightPart));
566
- return truncateToWidth(`${leftPart}${" ".repeat(gap)}${rightPart}`, safeWidth, "…", false);
567
- }
568
-
569
- export function renderContextBar(percent: number, color = CONTEXT_COLORS.normal): string {
570
- const clamped = Math.max(0, Math.min(100, percent));
571
- const filledCells = Math.floor((clamped / 100) * CONTEXT_BAR_WIDTH);
572
- const hasPartial = clamped > 0 && filledCells < CONTEXT_BAR_WIDTH;
573
- const filled = "━".repeat(filledCells);
574
- const partial = hasPartial ? "╸" : "";
575
- const empty = "─".repeat(Math.max(0, CONTEXT_BAR_WIDTH - filledCells - (hasPartial ? 1 : 0)));
576
- return `${applyHexColor(color, `${filled}${partial}`)}${applyHexColor(CONTEXT_COLORS.empty, empty)}`;
577
- }
578
-
579
- function contextColor(percent: number): string {
580
- if (percent > 90) return CONTEXT_COLORS.error;
581
- if (percent > 70) return CONTEXT_COLORS.warning;
582
- return CONTEXT_COLORS.normal;
583
- }
584
-
585
- function formatTokens(n: number): string {
586
- if (n < 1000) return String(n);
587
- if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
588
- if (n < 1000000) return `${Math.round(n / 1000)}k`;
589
- if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
590
- return `${Math.round(n / 1000000)}M`;
591
- }
592
-
593
- function formatDuration(ms: number): string {
594
- const seconds = Math.floor(ms / 1000);
595
- const minutes = Math.floor(seconds / 60);
596
- const hours = Math.floor(minutes / 60);
597
- if (hours > 0) return `${hours}h${minutes % 60}m`;
598
- if (minutes > 0) return `${minutes}m${seconds % 60}s`;
599
- return `${seconds}s`;
600
- }
601
-
602
- function formatCreditNumber(value: number | null): string {
603
- if (value === null || !Number.isFinite(value)) return "n/a";
604
- const abs = Math.abs(value);
605
- if (abs >= 1000) return value.toFixed(0);
606
- if (abs >= 100) return value.toFixed(1).replace(/\.0$/, "");
607
- return value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
608
- }
609
-
610
- function toFiniteNumber(value: unknown): number | null {
611
- if (typeof value === "number" && Number.isFinite(value)) return value;
612
- if (typeof value === "string" && value.trim() !== "") {
613
- const parsed = Number(value);
614
- return Number.isFinite(parsed) ? parsed : null;
615
- }
616
- return null;
617
- }
618
-
619
- function safeFg(theme: ThemeLike, token: string, text: string, fallback = "text"): string {
620
- try {
621
- return theme.fg(token, text);
622
- } catch {
623
- try {
624
- return theme.fg(fallback, text);
625
- } catch {
626
- return text;
627
- }
628
- }
629
- }
630
-
631
- function rainbow(text: string): string {
632
- let result = "";
633
- let colorIndex = 0;
634
- for (const char of text) {
635
- if (char === " " || char === ":") {
636
- result += char;
637
- continue;
638
- }
639
- result += `${hexToAnsi(RAINBOW_COLORS[colorIndex % RAINBOW_COLORS.length]!)}${char}`;
640
- colorIndex += 1;
641
- }
642
- return `${result}\x1b[0m`;
643
- }
644
-
645
- function applyHexColor(hex: string, text: string): string {
646
- return `${hexToAnsi(hex)}${text}\x1b[0m`;
647
- }
648
-
649
- function hexToAnsi(hex: string): string {
650
- const value = hex.replace("#", "");
651
- const red = Number.parseInt(value.slice(0, 2), 16);
652
- const green = Number.parseInt(value.slice(2, 4), 16);
653
- const blue = Number.parseInt(value.slice(4, 6), 16);
654
- return `\x1b[38;2;${red};${green};${blue}m`;
655
- }
656
-
657
- function stripAnsi(input: string): string {
658
- return sanitizeTerminalText(input, { allowNewline: false, allowTab: false, preserveSgr: false });
659
- }
660
-
661
- function isRecord(value: unknown): value is Record<string, any> {
662
- return typeof value === "object" && value !== null && !Array.isArray(value);
663
- }
@@ -1,2 +0,0 @@
1
-
2
- export { isStashShortcutInput } from "../bottom-input/index.ts";