opencode-go-usage-tui 1.3.2 → 1.3.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.
package/README.md CHANGED
@@ -122,7 +122,7 @@ Cookie 有效期约 1 年,过期后重新获取。
122
122
  - **`/go-lang`**:切换 中文 / English
123
123
  - 用量超过阈值(默认 80%,可用 `OPENCODE_GO_WARN_THRESHOLD` 调整)显示为红色
124
124
  - 点击面板标题可折叠/展开
125
- - 面板标题显示当前插件版本号(如 `v1.3.2`)
125
+ - 面板标题显示当前插件版本号(如 `v1.3.3`)
126
126
 
127
127
  ## 模型请求限额面板
128
128
 
package/README_EN.md CHANGED
@@ -122,7 +122,7 @@ The cookie is valid for about 1 year; get a new one after it expires.
122
122
  - **`/go-lang`**: switch between 中文 / English
123
123
  - Usage above the threshold (default 80%, adjustable via `OPENCODE_GO_WARN_THRESHOLD`) is shown in red
124
124
  - Click the panel title to collapse/expand
125
- - The panel title shows the current plugin version (e.g. `v1.3.2`)
125
+ - The panel title shows the current plugin version (e.g. `v1.3.3`)
126
126
 
127
127
  ## Model request limits panel
128
128
 
package/dist/tui.js CHANGED
@@ -488,7 +488,7 @@ function updatePricingStore(newData) {
488
488
  }
489
489
 
490
490
  // src/index.tsx
491
- var VERSION = "1.3.2";
491
+ var VERSION = "1.3.3";
492
492
  var CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 6e4);
493
493
  var WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8);
494
494
  var CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
@@ -1105,9 +1105,30 @@ function PricePanel(props) {
1105
1105
  const [priceError, setPriceError] = createSignal("");
1106
1106
  const [priceLoading, setPriceLoading] = createSignal(false);
1107
1107
  const [panelWidth, setPanelWidth] = createSignal(24);
1108
+ const [renderTick, setRenderTick] = createSignal(0);
1109
+ const [listReady, setListReady] = createSignal(false);
1110
+ createEffect(() => {
1111
+ const p = sharedPricing();
1112
+ if (p && p.models.length > 0) {
1113
+ const id = setTimeout(() => setListReady(true), 150);
1114
+ onCleanup(() => clearTimeout(id));
1115
+ } else {
1116
+ setListReady(false);
1117
+ }
1118
+ });
1119
+ createEffect(() => {
1120
+ if (!(listReady() && sharedPricing())) return;
1121
+ const t1 = setTimeout(() => setRenderTick((v) => v + 1), 250);
1122
+ const t2 = setTimeout(() => setRenderTick((v) => v + 1), 600);
1123
+ const t3 = setTimeout(() => setRenderTick((v) => v + 1), 1e3);
1124
+ onCleanup(() => {
1125
+ clearTimeout(t1);
1126
+ clearTimeout(t2);
1127
+ clearTimeout(t3);
1128
+ });
1129
+ });
1108
1130
  const [changedModels, setChangedModels] = createSignal(/* @__PURE__ */ new Map());
1109
1131
  const [expandedId, setExpandedId] = createSignal(null);
1110
- const [mounted, setMounted] = createSignal(false);
1111
1132
  const [cfg, setCfg] = createSignal(loadConfig());
1112
1133
  createEffect(() => {
1113
1134
  void configTick();
@@ -1115,14 +1136,23 @@ function PricePanel(props) {
1115
1136
  });
1116
1137
  let boxEl;
1117
1138
  let priceController = null;
1139
+ let widthSyncTimer = null;
1118
1140
  onMount(() => {
1119
- setTimeout(() => setMounted(true), 100);
1141
+ let tries = 0;
1142
+ const syncWidth = () => {
1143
+ tries++;
1144
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
1145
+ setPanelWidth(Math.max(20, boxEl.width));
1146
+ setRenderTick((v) => v + 1);
1147
+ widthSyncTimer = setTimeout(() => setRenderTick((v) => v + 1), 150);
1148
+ return;
1149
+ }
1150
+ if (tries < 50) widthSyncTimer = setTimeout(syncWidth, 80);
1151
+ };
1152
+ widthSyncTimer = setTimeout(syncWidth, 30);
1120
1153
  });
1121
- createEffect(() => {
1122
- mounted();
1123
- if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
1124
- setPanelWidth(Math.max(20, boxEl.width));
1125
- }
1154
+ onCleanup(() => {
1155
+ if (widthSyncTimer) clearTimeout(widthSyncTimer);
1126
1156
  });
1127
1157
  async function refreshPricing() {
1128
1158
  if (priceLoading()) return;
@@ -1188,6 +1218,7 @@ function PricePanel(props) {
1188
1218
  return out + "\u2026";
1189
1219
  }
1190
1220
  function justify(label, value) {
1221
+ void renderTick();
1191
1222
  const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth();
1192
1223
  const gauge = Math.max(10, outer - 4);
1193
1224
  const used = visualWidth(label) + visualWidth(value);
@@ -1204,7 +1235,6 @@ function PricePanel(props) {
1204
1235
  const row = (() => {
1205
1236
  var _el$30 = _$createElement("text");
1206
1237
  _$setProp(_el$30, "fg", fg);
1207
- _$setProp(_el$30, "selectable", false);
1208
1238
  _$setProp(_el$30, "onMouseUp", (e) => {
1209
1239
  e.preventDefault();
1210
1240
  setExpandedId(expanded ? null : m.id);
@@ -1298,6 +1328,7 @@ function PricePanel(props) {
1298
1328
  });
1299
1329
  const colors = () => pal();
1300
1330
  const sep = () => {
1331
+ void renderTick();
1301
1332
  const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth();
1302
1333
  return "\u2500".repeat(Math.max(1, outer - 4));
1303
1334
  };
@@ -1325,6 +1356,7 @@ function PricePanel(props) {
1325
1356
  resizeTimeout = setTimeout(() => {
1326
1357
  const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
1327
1358
  setPanelWidth(w);
1359
+ setRenderTick((v) => v + 1);
1328
1360
  }, 100);
1329
1361
  });
1330
1362
  _$insertNode(_el$37, _el$38);
@@ -1383,9 +1415,9 @@ function PricePanel(props) {
1383
1415
  })();
1384
1416
  },
1385
1417
  get children() {
1386
- return [_$memo(() => sharedPricing().models.filter((m) => {
1418
+ return [_$memo(() => _$memo(() => !!listReady())() ? sharedPricing().models.filter((m) => {
1387
1419
  return cfg().focusModels.length === 0 || cfg().focusModels.includes(m.id);
1388
- }).map((m) => renderLimitRow(m))), (() => {
1420
+ }).map((m) => renderLimitRow(m)) : null), (() => {
1389
1421
  var _el$48 = _$createElement("text");
1390
1422
  _$insert(_el$48, () => justify(t("priceUpdated"), new Date(sharedPricing().fetchTime).toLocaleTimeString()));
1391
1423
  _$effect((_$p) => _$setProp(_el$48, "fg", colors().muted, _$p));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-go-usage-tui",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "OpenCode TUI plugin displaying OpenCode Go usage in the sidebar",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.tsx CHANGED
@@ -445,25 +445,62 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
445
445
  const [priceError, setPriceError] = createSignal("")
446
446
  const [priceLoading, setPriceLoading] = createSignal(false)
447
447
  const [panelWidth, setPanelWidth] = createSignal(24)
448
+ // 渲染强制刷新计数:列表首帧常在数据到达使面板变高的瞬时布局中测量,留下
449
+ // "模型间空行";该残留不会随盒子变宽自愈,但会在一次额外重渲染(重新测量文本
450
+ // 高度)后消失。故列表就绪后级联触发若干次 renderTick,强制重新测量每行高度。
451
+ const [renderTick, setRenderTick] = createSignal(0)
452
+ // 列表就绪标记:sharedPricing 数据到达那帧盒子常在重布局(数据使面板内容变高),
453
+ // 直接渲染列表会命中"未稳定测量"。故数据到达后延迟一帧再创建列表文本
454
+ const [listReady, setListReady] = createSignal(false)
455
+ createEffect(() => {
456
+ const p = sharedPricing()
457
+ if (p && p.models.length > 0) {
458
+ const id = setTimeout(() => setListReady(true), 150)
459
+ onCleanup(() => clearTimeout(id))
460
+ } else {
461
+ setListReady(false)
462
+ }
463
+ })
464
+ // 列表首帧常在"数据到达使面板变高"的瞬时布局中测量,留下"模型间空行";
465
+ // 该残留不会随盒子变宽自愈,但会在一次额外重渲染(重新测量文本高度)后消失
466
+ // —— 即用户观察到的"展开任意行后空行消失"现象。故在列表就绪后再强制若干次
467
+ // 重渲染,让 @opentui 在稳定布局下重新测量每行高度,自愈空行。
468
+ createEffect(() => {
469
+ if (!(listReady() && sharedPricing())) return
470
+ const t1 = setTimeout(() => setRenderTick((v) => v + 1), 250)
471
+ const t2 = setTimeout(() => setRenderTick((v) => v + 1), 600)
472
+ const t3 = setTimeout(() => setRenderTick((v) => v + 1), 1000)
473
+ onCleanup(() => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3) })
474
+ })
448
475
  // 记录变化的模型:modelId -> 变化类别集合(added/pricing/limits),用于行高亮与颜色区分
449
476
  const [changedModels, setChangedModels] = createSignal<Map<string, Set<string>>>(new Map<string, Set<string>>())
450
477
  // 行内展开:当前展开的模型 id(单选 accordion)
451
478
  const [expandedId, setExpandedId] = createSignal<string | null>(null)
452
- // 挂载后延迟校正:首帧 boxEl.width 尚未测量,布局完成后再触发一次重渲染以拿到真实宽度
453
- const [mounted, setMounted] = createSignal(false)
454
479
  const [cfg, setCfg] = createSignal<LoadedConfig>(loadConfig())
455
480
  createEffect(() => { void configTick(); setCfg(loadConfig()) })
456
481
  let boxEl: any
457
482
  let priceController: AbortController | null = null
458
483
 
459
- onMount(() => { setTimeout(() => setMounted(true), 100) })
460
- // 强制同步 panelWidth 与 box 真实宽度(依赖 mounted,布局完成后重跑一次)
461
- createEffect(() => {
462
- mounted()
463
- if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
464
- setPanelWidth(Math.max(20, boxEl.width))
484
+ // 轮询等待布局就绪:首帧 boxEl.width 尚未测量(为 0)时文本会按临时窄宽度折行,
485
+ // 产生"模型间空行"。单次延时可能早于布局完成而浪费唯一机会,故循环重试,
486
+ // 宽度就绪那一刻强制重渲染自愈(上限 50 次 × 80ms 防泄漏)
487
+ let widthSyncTimer: ReturnType<typeof setTimeout> | null = null
488
+ onMount(() => {
489
+ let tries = 0
490
+ const syncWidth = () => {
491
+ tries++
492
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
493
+ setPanelWidth(Math.max(20, boxEl.width))
494
+ setRenderTick((v) => v + 1)
495
+ // 补一次延迟 tick:覆盖 onSizeChange 防抖窗口结束后的稳定态
496
+ widthSyncTimer = setTimeout(() => setRenderTick((v) => v + 1), 150)
497
+ return
498
+ }
499
+ if (tries < 50) widthSyncTimer = setTimeout(syncWidth, 80)
465
500
  }
501
+ widthSyncTimer = setTimeout(syncWidth, 30)
466
502
  })
503
+ onCleanup(() => { if (widthSyncTimer) clearTimeout(widthSyncTimer) })
467
504
 
468
505
  async function refreshPricing() {
469
506
  if (priceLoading()) return
@@ -534,6 +571,7 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
534
571
  // visual-cache 风格:左标签 + 自动填充 + 右值
535
572
  // 直接读取 boxEl 实时宽度(避免 panelWidth 信号滞后)
536
573
  function justify(label: string, value: string): string {
574
+ void renderTick()
537
575
  const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth()
538
576
  const gauge = Math.max(10, outer - 4)
539
577
  const used = visualWidth(label) + visualWidth(value)
@@ -555,7 +593,7 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
555
593
  // 行尾展开指示符(右对齐,不挤占名称列)
556
594
  const ind = expanded ? " \u25be" : " \u25b8"
557
595
  const row = (
558
- <text fg={fg} selectable={false} onMouseUp={(e) => { e.preventDefault(); setExpandedId(expanded ? null : m.id) }}>
596
+ <text fg={fg} onMouseUp={(e) => { e.preventDefault(); setExpandedId(expanded ? null : m.id) }}>
559
597
  {justify(name, req + ind)}
560
598
  </text>
561
599
  )
@@ -643,6 +681,7 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
643
681
  const colors = () => pal()
644
682
 
645
683
  const sep = () => {
684
+ void renderTick()
646
685
  const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth()
647
686
  return "\u2500".repeat(Math.max(1, outer - 4))
648
687
  }
@@ -671,6 +710,7 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
671
710
  resizeTimeout = setTimeout(() => {
672
711
  const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24
673
712
  setPanelWidth(w)
713
+ setRenderTick((v) => v + 1)
674
714
  }, 100)
675
715
  }}
676
716
  >
@@ -693,9 +733,9 @@ function PricePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX.E
693
733
  <Show when={sharedPricing()} fallback={
694
734
  <text fg={colors().muted}>{t("priceLoading")}</text>
695
735
  }>
696
- {sharedPricing()!.models.filter((m) => {
736
+ {listReady() ? sharedPricing()!.models.filter((m) => {
697
737
  return cfg().focusModels.length === 0 || cfg().focusModels.includes(m.id)
698
- }).map((m) => renderLimitRow(m))}
738
+ }).map((m) => renderLimitRow(m)) : null}
699
739
  <text fg={colors().muted}>
700
740
  {justify(t("priceUpdated"), new Date(sharedPricing()!.fetchTime).toLocaleTimeString())}
701
741
  </text>