oc-go-usage-display 2.0.0 → 2.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.
@@ -51,6 +51,7 @@ import type { JSX } from "@opentui/solid/jsx-runtime";
51
51
  import type { TuiTheme } from "@opencode-ai/plugin/tui";
52
52
  import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
53
53
  import {
54
+ GO_SHARE_LABEL_WIDTH,
54
55
  PLAN_LABEL_WIDTH,
55
56
  buildPlanRows,
56
57
  formatPercentCell,
@@ -76,6 +77,9 @@ import {
76
77
  hostEnv,
77
78
  isRecord,
78
79
  mockSnapshot,
80
+ parseMockLimited,
81
+ parseMockPercents,
82
+ parseMockResets,
79
83
  readAuthJsonApiKey,
80
84
  toNonEmptyString,
81
85
  unavailableSnapshot,
@@ -85,7 +89,7 @@ import {
85
89
  SIDEBAR_COLLAPSED_GLYPH,
86
90
  SIDEBAR_EXPANDED_GLYPH,
87
91
  } from "./shared.js";
88
- import type { UsageHost, UsageSnapshot } from "./shared.js";
92
+ import type { MockSnapshotOverrides, UsageHost, UsageSnapshot } from "./shared.js";
89
93
 
90
94
  // ---------------------------------------------------------------------------
91
95
  // Constants
@@ -178,7 +182,16 @@ async function fetchJsonWithTimeout(url: string, apiKey: string): Promise<unknow
178
182
  }
179
183
 
180
184
  async function loadUsageSnapshot(host: UsageHost): Promise<UsageSnapshot | null> {
181
- if (hostEnv(host, "MOCK") === "1") return mockSnapshot();
185
+ if (hostEnv(host, "MOCK") === "1") {
186
+ const overrides: MockSnapshotOverrides = {};
187
+ const percents = parseMockPercents(hostEnv(host, "MOCK_PERCENTS"));
188
+ if (percents !== null) overrides.percents = percents;
189
+ const resets = parseMockResets(hostEnv(host, "MOCK_RESETS"));
190
+ if (resets !== null) overrides.resets = resets;
191
+ const limited = parseMockLimited(hostEnv(host, "MOCK_LIMITED"));
192
+ if (limited !== null) overrides.limited = limited;
193
+ return mockSnapshot(overrides);
194
+ }
182
195
 
183
196
  const apiKey = toNonEmptyString(hostEnv(host, "API_KEY")) ?? readAuthJsonApiKey(host);
184
197
  if (apiKey === null) return null;
@@ -457,20 +470,55 @@ export function LabeledValueRow(props: { theme: TuiTheme; row: UsageRow }) {
457
470
  // a filled-vs-empty split at a glance.
458
471
  export const MIN_METER_WIDTH = 6;
459
472
 
460
- // A meter as two boxes: the filled part at the window's percentage, the track for
461
- // the rest. Both fill whatever the row has, so the bar reaches the sidebar's
462
- // right edge on any host, at any sidebar width, with nothing to measure and
463
- // nothing to guess -- the failure modes a fixed-width glyph string has (too short
464
- // on a wide sidebar, clipped on a narrow one) cannot happen.
473
+ // The meter's two parts, as a SPLIT rather than as two flexible boxes.
474
+ //
475
+ // The FILL and the TRACK are flex WEIGHTS that sum to 100, on a zero basis, and a
476
+ // part that comes out at zero is not rendered at all. That is the whole point, and
477
+ // each of the two obvious alternatives moved the block:
478
+ //
479
+ // - two `flexGrow` parts on their default basis: the track keeps a one-cell
480
+ // basis that flex will not shrink away, so at 100% the fill wants the whole
481
+ // meter and the track still claims a cell. It paints itself into the padding
482
+ // beside the percent -- the cell of the wrong color to the right of a full bar
483
+ // -- and makes the meter a cell wider at 100% than anywhere else.
484
+ // - both parts as `width: "<n>%"`: no overflow, but two independent
485
+ // percentage-to-cell roundings that need not agree. At 50% of a 27-cell meter
486
+ // the fill took 13 and the track 13, so the pair was 26 and the bar sat a cell
487
+ // short. A percentage cannot express "the rest of this box".
488
+ //
489
+ // A zero basis makes each part's width purely its share of the space the row gave
490
+ // the meter, so the two add up to the meter at every reading.
491
+ //
492
+ // The meter is two BOXES rather than a string of block glyphs. A glyph string has a
493
+ // fixed cell count, and a plugin cannot measure the sidebar it renders into
494
+ // (opentui resolves a text node's `width` as a wrapping bound, and no layout
495
+ // callback reaches a plugin), so a glyph meter is either too short for a wide
496
+ // sidebar or clipped by a narrow one. Boxes fill whatever the row gives them,
497
+ // which is why the meter reaches the sidebar's right edge on any host at any width.
465
498
  //
466
499
  // The track is a surface tone rather than the severity colour, so an empty meter
467
500
  // reads as an empty track instead of blank space.
468
501
  export function MeterBar(props: { theme: TuiTheme; percent: number; severity: UsageMeterSeverity }) {
469
502
  const fill = createMemo(() => meterFillPercent(props.percent));
503
+ const track = createMemo(() => 100 - fill());
470
504
  return (
471
505
  <box flexDirection="row" flexGrow={1} flexShrink={1} minWidth={MIN_METER_WIDTH}>
472
- <box width={`${fill()}%`} backgroundColor={meterColor(props.theme, props.severity)} />
473
- <box flexGrow={1} backgroundColor={props.theme.current.backgroundElement} />
506
+ <Show when={fill() > 0}>
507
+ <box
508
+ flexGrow={fill()}
509
+ flexBasis={0}
510
+ flexShrink={0}
511
+ backgroundColor={meterColor(props.theme, props.severity)}
512
+ />
513
+ </Show>
514
+ <Show when={track() > 0}>
515
+ <box
516
+ flexGrow={track()}
517
+ flexBasis={0}
518
+ flexShrink={0}
519
+ backgroundColor={props.theme.current.backgroundElement}
520
+ />
521
+ </Show>
474
522
  </box>
475
523
  );
476
524
  }
@@ -585,25 +633,27 @@ export function CollapsibleSection(props: {
585
633
  );
586
634
  }
587
635
 
588
- // A model's share of the Go tokens spent in this session tree, drawn with the
589
- // same block bar and the same threshold coloring as the plan meters. It is a
590
- // share of tokens and nothing else: not of the plan, not a quota, not a price.
591
- // A model's Go share: the bar and the percent pinned to the RIGHT edge of the
592
- // row, so they line up with the Steps/Cost columns of the table above instead of
593
- // trailing the label wherever it happens to end. The label takes the slack
594
- // (`flexGrow`), the meter group never shrinks.
636
+ // A model's Go share: the bar and the percent pinned to the RIGHT edge of the row,
637
+ // so they line up with the Steps/Cost columns of the table above instead of
638
+ // trailing the label wherever it happens to end.
639
+ //
640
+ // The label sits in a FIXED-width cell and only the meter grows -- the same shape
641
+ // as `GoPlanRow`, and deliberately not a `flexGrow` label with a `gap`. Two
642
+ // flexible children divide the slack between them, and the share meter was doing
643
+ // exactly that: 18 cells wide at 50% and 20 at 100%, with its percent sliding a
644
+ // column each time. This row's number is a share of tokens, not of the plan, so
645
+ // its bar is not comparable with the plan meters' -- but its own geometry must
646
+ // still not depend on its own value.
595
647
  export function GoShareRow(props: { theme: TuiTheme; percent: number }) {
596
648
  const rounded = Math.round(props.percent);
597
649
  return (
598
- <box flexDirection="row" gap={1}>
599
- <text fg={props.theme.current.textMuted} wrapMode="none" flexShrink={0} flexGrow={1}>
600
- {INTEGRATED_GO_SHARE_LABEL}
601
- </text>
602
- <MeterBar
603
- theme={props.theme}
604
- percent={rounded}
605
- severity={meterSeverityForPercent(rounded)}
606
- />
650
+ <box flexDirection="row">
651
+ <box width={GO_SHARE_LABEL_WIDTH} marginRight={1} flexShrink={0} flexDirection="row">
652
+ <text fg={props.theme.current.textMuted} wrapMode="none">
653
+ {INTEGRATED_GO_SHARE_LABEL}
654
+ </text>
655
+ </box>
656
+ <MeterBar theme={props.theme} percent={rounded} severity={meterSeverityForPercent(rounded)} />
607
657
  <text fg={props.theme.current.textMuted} wrapMode="none" flexShrink={0}>
608
658
  {formatPercentCell(rounded)}
609
659
  </text>
package/src/tui.kilo.tsx CHANGED
@@ -107,6 +107,7 @@ import {
107
107
  KILO_SLOT_ORDER,
108
108
  KILO_STEPS_COLUMN_WIDTH,
109
109
  KILO_USAGE_PANEL_PLUGIN_ID,
110
+ mockGoShare,
110
111
  } from "./shared.js";
111
112
  import type { ModelUsage, SessionModelUsage } from "./shared.js";
112
113
  import {
@@ -376,7 +377,7 @@ async function initializeTui(api: TuiPluginApi, options: PluginOptions | undefin
376
377
  model={model}
377
378
  isGo={isGo}
378
379
  expanded={() => props.expandedModels().has(key)}
379
- goShare={() => goSharePercent(usageTokenCount(model.tokens), props.goTotal)}
380
+ goShare={() => mockGoShare(HOST) ?? goSharePercent(usageTokenCount(model.tokens), props.goTotal)}
380
381
  onToggle={() => props.onToggleModel(key)}
381
382
  />
382
383
  );