@duckmind/dm-darwin-x64 0.34.0 → 0.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dm CHANGED
Binary file
@@ -19,6 +19,14 @@ export type ContextUsage = {
19
19
  percent?: number;
20
20
  };
21
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
+
22
30
  export type BottomInputStatusState = {
23
31
  ctx: any;
24
32
  footerData?: any;
@@ -51,6 +59,7 @@ export type BottomInputStatusRender = {
51
59
  };
52
60
 
53
61
  export const CONTEXT_BAR_WIDTH = 10;
62
+ const CREDIT_BAR_WIDTH = 10;
54
63
  const CONTEXT_COLORS = {
55
64
  normal: "#00afaf",
56
65
  warning: "#febc38",
@@ -73,19 +82,26 @@ export function renderBottomInputStatus(input: BottomInputStatusState): BottomIn
73
82
  const safeWidth = Math.max(1, Math.floor(input.width));
74
83
  const enabled = input.beautifiedInputEnabled;
75
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);
76
87
  if (!enabled) {
77
88
  return {
78
89
  topLines: [],
79
90
  secondaryLines: renderExtensionStatusLines(extensionStatuses, safeWidth, input.theme),
80
- lastPromptLines: renderLastPromptLines(input.lastPrompt, safeWidth, input.theme),
91
+ lastPromptLines: [...lastPromptStatusLines, ...renderLastPromptLines(input.lastPrompt, safeWidth, input.theme)],
81
92
  frameStatus: emptyFrameStatus(),
82
- cacheKey: JSON.stringify({ width: safeWidth, enabled: false, lastPrompt: input.lastPrompt, extensionStatuses }),
93
+ cacheKey: JSON.stringify({
94
+ width: safeWidth,
95
+ enabled: false,
96
+ lastPrompt: input.lastPrompt,
97
+ extensionStatuses,
98
+ lastPromptStatusLines: lastPromptStatusLines.map(stripAnsi),
99
+ }),
83
100
  };
84
101
  }
85
102
 
86
103
  const icons = input.icons ?? getBottomInputIcons();
87
104
  const elapsedSeconds = Math.floor(Math.max(0, input.now - input.sessionStartTime) / 1000);
88
- const usage = readContextUsageSnapshot(input.ctx, input.isStreaming, input.liveUsage, input.latestAssistantUsage);
89
105
  const modelName = readModelName(input.ctx);
90
106
  const thinking = readThinkingLevel(input.ctx) ?? input.currentThinkingLevel ?? readThinkingLevelFromSession(input.ctx);
91
107
  const cacheKey = JSON.stringify({
@@ -97,13 +113,14 @@ export function renderBottomInputStatus(input: BottomInputStatusState): BottomIn
97
113
  elapsedSeconds,
98
114
  lastPrompt: input.lastPrompt,
99
115
  extensionStatuses,
116
+ lastPromptStatusLines: lastPromptStatusLines.map(stripAnsi),
100
117
  icons,
101
118
  });
102
119
 
103
120
  return {
104
121
  topLines: [],
105
122
  secondaryLines: renderExtensionStatusLines(extensionStatuses, safeWidth, input.theme),
106
- lastPromptLines: renderLastPromptLines(input.lastPrompt, safeWidth, input.theme),
123
+ lastPromptLines: [...lastPromptStatusLines, ...renderLastPromptLines(input.lastPrompt, safeWidth, input.theme)],
107
124
  frameStatus: renderFrameStatus({ ...input, width: safeWidth, icons }, modelName, thinking, usage),
108
125
  cacheKey,
109
126
  };
@@ -139,6 +156,16 @@ export function renderLastPromptLines(prompt: string, width: number, theme: Them
139
156
  return [truncateToWidth(`${prefix}${safeFg(theme, "muted", value)}`, safeWidth, "…", false)];
140
157
  }
141
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
+
142
169
  export function getVisibleExtensionStatuses(footerData: any): string[] {
143
170
  let statuses: unknown;
144
171
  try {
@@ -236,6 +263,66 @@ function renderContextSegment(usage: ContextUsage | null): string | null {
236
263
  return applyHexColor(CONTEXT_COLORS.normal, formatTokens(usage.tokens));
237
264
  }
238
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
+
239
326
  function renderElapsedSegment(theme: ThemeLike, startedAt: number, now: number, icons: BottomInputIconSet): string | null {
240
327
  const elapsed = Math.max(0, now - startedAt);
241
328
  if (elapsed < 1000) return null;
@@ -349,6 +436,135 @@ function readModelName(ctx: any): string | null {
349
436
  }
350
437
  }
351
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 filledCells = Math.round((percent / 100) * CREDIT_BAR_WIDTH);
520
+ const filled = "█".repeat(filledCells);
521
+ const empty = "░".repeat(Math.max(0, CREDIT_BAR_WIDTH - filledCells));
522
+ const color = percent <= 15 ? "error" : percent <= 35 ? "warning" : "success";
523
+ return `${safeFg(theme, "muted", "[")}${safeFg(theme, color, filled, "accent")}${safeFg(theme, "borderMuted", empty)}${safeFg(theme, "muted", "]")}`;
524
+ }
525
+
526
+ function isDuckmindProvider(ctx: any): boolean {
527
+ let values: unknown[] = [];
528
+ try {
529
+ const model = ctx?.model;
530
+ values = [
531
+ model?.provider,
532
+ model?.provider?.id,
533
+ model?.provider?.name,
534
+ model?.providerName,
535
+ model?.baseUrl,
536
+ model?.id,
537
+ ctx?.provider,
538
+ ctx?.provider?.id,
539
+ ctx?.provider?.name,
540
+ ctx?.provider?.baseUrl,
541
+ ];
542
+ } catch {
543
+ return false;
544
+ }
545
+ return values.some((value) => typeof value === "string" && /duckmind|openrouter|@preset\//i.test(value));
546
+ }
547
+
548
+ function renderEdgeAlignedLine(left: string, right: string, width: number): string {
549
+ const safeWidth = Math.max(1, Math.floor(width));
550
+ if (safeWidth <= 1) return truncateToWidth(left || right, safeWidth, "…", false);
551
+ const minGap = 2;
552
+ let rightPart = right;
553
+ let leftPart = left;
554
+ const originalLeftWidth = visibleWidth(leftPart);
555
+ const originalRightWidth = visibleWidth(rightPart);
556
+ if (originalLeftWidth + minGap + originalRightWidth > safeWidth) {
557
+ const rightBudget = Math.max(0, Math.min(originalRightWidth, Math.floor(safeWidth * 0.55)));
558
+ rightPart = rightBudget > 0 ? truncateToWidth(rightPart, rightBudget, "…", false) : "";
559
+ const leftBudget = Math.max(0, safeWidth - visibleWidth(rightPart) - (rightPart ? minGap : 0));
560
+ leftPart = leftBudget > 0 ? truncateToWidth(leftPart, leftBudget, "…", false) : "";
561
+ }
562
+ if (!leftPart) return truncateToWidth(rightPart, safeWidth, "…", false);
563
+ if (!rightPart) return truncateToWidth(leftPart, safeWidth, "…", false);
564
+ const gap = Math.max(1, safeWidth - visibleWidth(leftPart) - visibleWidth(rightPart));
565
+ return truncateToWidth(`${leftPart}${" ".repeat(gap)}${rightPart}`, safeWidth, "…", false);
566
+ }
567
+
352
568
  export function renderContextBar(percent: number, color = CONTEXT_COLORS.normal): string {
353
569
  const clamped = Math.max(0, Math.min(100, percent));
354
570
  const filledCells = Math.floor((clamped / 100) * CONTEXT_BAR_WIDTH);
@@ -382,6 +598,23 @@ function formatDuration(ms: number): string {
382
598
  return `${seconds}s`;
383
599
  }
384
600
 
601
+ function formatCreditNumber(value: number | null): string {
602
+ if (value === null || !Number.isFinite(value)) return "n/a";
603
+ const abs = Math.abs(value);
604
+ if (abs >= 1000) return value.toFixed(0);
605
+ if (abs >= 100) return value.toFixed(1).replace(/\.0$/, "");
606
+ return value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
607
+ }
608
+
609
+ function toFiniteNumber(value: unknown): number | null {
610
+ if (typeof value === "number" && Number.isFinite(value)) return value;
611
+ if (typeof value === "string" && value.trim() !== "") {
612
+ const parsed = Number(value);
613
+ return Number.isFinite(parsed) ? parsed : null;
614
+ }
615
+ return null;
616
+ }
617
+
385
618
  function safeFg(theme: ThemeLike, token: string, text: string, fallback = "text"): string {
386
619
  try {
387
620
  return theme.fg(token, text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-darwin-x64",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
4
4
  "description": "DuckMind (dm) binary payload for darwin x64",
5
5
  "license": "MIT",
6
6
  "os": [