opencode-subagent-magazine 1.6.0-beta.2 → 1.6.0-beta.4

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/dist/v2.js CHANGED
@@ -307,7 +307,8 @@ var SETTING_KEYS = {
307
307
  order: `${KV_PREFIX}.order`,
308
308
  scrollMode: `${KV_PREFIX}.scroll_mode`,
309
309
  open: `${KV_PREFIX}.open`,
310
- ttlDays: `${KV_PREFIX}.ttl_days`
310
+ ttlDays: `${KV_PREFIX}.ttl_days`,
311
+ border: `${KV_PREFIX}.border`
311
312
  };
312
313
  function loadSessionData(kv) {
313
314
  try {
@@ -329,7 +330,7 @@ function readTTLDays(kv) {
329
330
  }
330
331
 
331
332
  // src/_version.ts
332
- var PLUGIN_VERSION = "1.6.0-beta.2";
333
+ var PLUGIN_VERSION = "1.6.0-beta.4";
333
334
 
334
335
  // src/i18n.ts
335
336
  var ZH_T = {
@@ -381,7 +382,9 @@ var ZH_T = {
381
382
  "cancel.already_ended": "\u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF0C\u65E0\u9700\u53D6\u6D88",
382
383
  "cancel.status_error": "\u65E0\u6CD5\u67E5\u8BE2\u4F1A\u8BDD\u72B6\u6001",
383
384
  "cancel.sent": "\u5DF2\u53D1\u9001\u53D6\u6D88\u6307\u4EE4",
384
- "cancel.failed": "\u53D6\u6D88\u5931\u8D25"
385
+ "cancel.failed": "\u53D6\u6D88\u5931\u8D25",
386
+ "borderShown": "\u9762\u677F\u8FB9\u6846 \u5DF2\u663E\u793A",
387
+ "borderHidden": "\u9762\u677F\u8FB9\u6846 \u5DF2\u9690\u85CF"
385
388
  };
386
389
  var EN_T = {
387
390
  "panel.title": "SubAgent",
@@ -432,7 +435,9 @@ var EN_T = {
432
435
  "cancel.already_ended": "Session already ended, no need to cancel",
433
436
  "cancel.status_error": "Cannot query session status",
434
437
  "cancel.sent": "Cancel instruction sent",
435
- "cancel.failed": "Cancellation failed"
438
+ "cancel.failed": "Cancellation failed",
439
+ "borderShown": "Panel border shown",
440
+ "borderHidden": "Panel border hidden"
436
441
  };
437
442
  var JA_T = {
438
443
  "panel.title": "\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8",
@@ -483,7 +488,9 @@ var JA_T = {
483
488
  "cancel.already_ended": "\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u65E2\u306B\u7D42\u4E86\u3057\u3066\u3044\u307E\u3059",
484
489
  "cancel.status_error": "\u30BB\u30C3\u30B7\u30E7\u30F3\u72B6\u614B\u3092\u7167\u4F1A\u3067\u304D\u307E\u305B\u3093",
485
490
  "cancel.sent": "\u30AD\u30E3\u30F3\u30BB\u30EB\u6307\u793A\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
486
- "cancel.failed": "\u30AD\u30E3\u30F3\u30BB\u30EB\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
491
+ "cancel.failed": "\u30AD\u30E3\u30F3\u30BB\u30EB\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
492
+ "borderShown": "\u30D1\u30CD\u30EB\u67A0\u7DDA \u8868\u793A",
493
+ "borderHidden": "\u30D1\u30CD\u30EB\u67A0\u7DDA \u975E\u8868\u793A"
487
494
  };
488
495
  var KO_T = {
489
496
  "panel.title": "\uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8",
@@ -534,7 +541,9 @@ var KO_T = {
534
541
  "cancel.already_ended": "\uC138\uC158\uC774 \uC774\uBBF8 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
535
542
  "cancel.status_error": "\uC138\uC158 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
536
543
  "cancel.sent": "\uCDE8\uC18C \uC9C0\uC2DC\uB97C \uBCF4\uB0C8\uC2B5\uB2C8\uB2E4",
537
- "cancel.failed": "\uCDE8\uC18C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"
544
+ "cancel.failed": "\uCDE8\uC18C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
545
+ "borderShown": "\uD328\uB110 \uD14C\uB450\uB9AC \uD45C\uC2DC",
546
+ "borderHidden": "\uD328\uB110 \uD14C\uB450\uB9AC \uC228\uAE40"
538
547
  };
539
548
  var LANGS = { zh: ZH_T, en: EN_T, ja: JA_T, ko: KO_T };
540
549
  var LANG_META = [
@@ -809,6 +818,19 @@ function makeCommands(context, api, signals) {
809
818
  } catch {
810
819
  }
811
820
  }
821
+ },
822
+ {
823
+ id: "opencode-subagent-magazine.subagent.border",
824
+ title: "SubAgent Magazine: Border",
825
+ description: "Show or hide the panel border",
826
+ slash: { name: "subagent-border" },
827
+ palette: true,
828
+ run: () => {
829
+ const cur = Boolean(kv.get(SETTING_KEYS.border, false));
830
+ kv.set(SETTING_KEYS.border, !cur);
831
+ signals.setBorderVisible(!cur);
832
+ api.ui.toast(!cur ? t("borderShown") : t("borderHidden"));
833
+ }
812
834
  }
813
835
  ];
814
836
  }
@@ -832,9 +854,11 @@ import { insertNode as _$insertNode } from "@opentui/solid";
832
854
  import { createComponent as _$createComponent } from "@opentui/solid";
833
855
  import { effect as _$effect } from "@opentui/solid";
834
856
  import { insert as _$insert } from "@opentui/solid";
835
- import { memo as _$memo } from "@opentui/solid";
836
- import { use as _$use } from "@opentui/solid";
837
857
  import { setProp as _$setProp } from "@opentui/solid";
858
+ import { use as _$use } from "@opentui/solid";
859
+ import { spread as _$spread } from "@opentui/solid";
860
+ import { mergeProps as _$mergeProps } from "@opentui/solid";
861
+ import { memo as _$memo } from "@opentui/solid";
838
862
  import { createElement as _$createElement } from "@opentui/solid";
839
863
  import { createMemo, createSignal as createSignal2, createEffect, onMount, onCleanup, untrack, Show, For } from "solid-js";
840
864
 
@@ -1057,6 +1081,31 @@ function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
1057
1081
  return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
1058
1082
  }
1059
1083
 
1084
+ // src/panel/entry-map.ts
1085
+ function findSubEntryKey(entries, id, sessionId) {
1086
+ if (sessionId) {
1087
+ for (const [key, entry] of entries) {
1088
+ if (entry.sessionId === sessionId) return key;
1089
+ }
1090
+ }
1091
+ return entries.has(id) ? id : void 0;
1092
+ }
1093
+ function upsertSubEntry(entries, partial, now = Date.now()) {
1094
+ const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
1095
+ const existing = entries.get(key);
1096
+ const next = new Map(entries);
1097
+ if (key !== partial.id) next.delete(partial.id);
1098
+ const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
1099
+ next.set(key, {
1100
+ ...existing ?? { startedAt: now },
1101
+ ...partial,
1102
+ id: existing?.id ?? partial.id,
1103
+ startedAt: existing?.startedAt || partial.startedAt || now,
1104
+ endedAt: ended ? existing?.endedAt || now : void 0
1105
+ });
1106
+ return next;
1107
+ }
1108
+
1060
1109
  // src/panel/SubAgentPanel.tsx
1061
1110
  var LEFT_PAD = 4;
1062
1111
  var INDENT = 2;
@@ -1096,7 +1145,7 @@ function SubAgentPanel(props) {
1096
1145
  };
1097
1146
  };
1098
1147
  const loadEntries = (sid) => {
1099
- const m = /* @__PURE__ */ new Map();
1148
+ let m = /* @__PURE__ */ new Map();
1100
1149
  try {
1101
1150
  const {
1102
1151
  parentSid,
@@ -1106,7 +1155,9 @@ function SubAgentPanel(props) {
1106
1155
  if (rec) {
1107
1156
  const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
1108
1157
  if (source) {
1109
- for (const e of source) m.set(e.id, e);
1158
+ for (const e of source) {
1159
+ m = upsertSubEntry(m, e);
1160
+ }
1110
1161
  }
1111
1162
  }
1112
1163
  } catch {
@@ -1344,22 +1395,7 @@ function SubAgentPanel(props) {
1344
1395
  let boxEl;
1345
1396
  let disposed = false;
1346
1397
  const upsertEntry = (partial) => {
1347
- setEntryMap((prev) => {
1348
- const existing = prev.get(partial.id);
1349
- const next = new Map(prev);
1350
- const nowTs = Date.now();
1351
- const e = partial.status;
1352
- const ended = e === "done" || e === "error" || e === "cancelled";
1353
- next.set(partial.id, {
1354
- ...existing ?? {
1355
- startedAt: nowTs
1356
- },
1357
- ...partial,
1358
- startedAt: existing?.startedAt || partial.startedAt || nowTs,
1359
- endedAt: ended ? existing?.endedAt || nowTs : void 0
1360
- });
1361
- return next;
1362
- });
1398
+ setEntryMap((prev) => upsertSubEntry(prev, partial));
1363
1399
  };
1364
1400
  const isDescendantOf = (childId, rootId) => {
1365
1401
  const visited = /* @__PURE__ */ new Set();
@@ -1503,13 +1539,17 @@ function SubAgentPanel(props) {
1503
1539
  if (rawStatus === "error") {
1504
1540
  const id2 = `tool:${String(part.id ?? "")}`;
1505
1541
  if (!part.id) return;
1506
- const existing = entryMap().get(id2);
1542
+ const stMeta2 = st?.metadata;
1543
+ const errorSid = stMeta2?.session_id !== void 0 ? String(stMeta2.session_id) : stMeta2?.sessionId !== void 0 ? String(stMeta2.sessionId) : void 0;
1544
+ const key = findSubEntryKey(entryMap(), id2, errorSid);
1545
+ const existing = key ? entryMap().get(key) : void 0;
1507
1546
  if (existing) {
1508
1547
  upsertEntry({
1509
- id: id2,
1548
+ id: existing.id,
1510
1549
  title: existing.title,
1511
1550
  agent: existing.agent,
1512
1551
  prompt: existing.prompt,
1552
+ sessionId: existing.sessionId ?? errorSid,
1513
1553
  status: "error"
1514
1554
  });
1515
1555
  }
@@ -1896,12 +1936,15 @@ function SubAgentPanel(props) {
1896
1936
  if (!part.id) continue;
1897
1937
  const st = part.state;
1898
1938
  const rawStatus = String(st?.status ?? "");
1899
- const exists = next.get(id);
1939
+ const scanStMeta = st?.metadata;
1940
+ const scanSubSid = scanStMeta?.session_id !== void 0 ? String(scanStMeta.session_id) : scanStMeta?.sessionId !== void 0 ? String(scanStMeta.sessionId) : void 0;
1941
+ const existingKey = findSubEntryKey(next, id, scanSubSid);
1942
+ const exists = existingKey ? next.get(existingKey) : void 0;
1900
1943
  if (!exists && clearedIds.has(id)) continue;
1901
1944
  if ((rawStatus === "pending" || rawStatus === "") && !exists) continue;
1902
1945
  if (rawStatus === "error") {
1903
1946
  if (exists && exists.status === "running") {
1904
- next.set(id, {
1947
+ next.set(existingKey ?? id, {
1905
1948
  ...exists,
1906
1949
  status: "error",
1907
1950
  endedAt: Date.now()
@@ -1912,11 +1955,11 @@ function SubAgentPanel(props) {
1912
1955
  let status = "running";
1913
1956
  if (rawStatus === "completed") status = "done";
1914
1957
  if ((st?.input?.run_in_background === true || st?.input?.background === true) && status === "done") {
1915
- const scanStMeta = st?.metadata;
1916
- const scanHasChild = scanStMeta?.session_id !== void 0 || scanStMeta?.sessionId !== void 0;
1958
+ const scanStMeta2 = st?.metadata;
1959
+ const scanHasChild = scanStMeta2?.session_id !== void 0 || scanStMeta2?.sessionId !== void 0;
1917
1960
  if (scanHasChild) status = "running";
1918
1961
  }
1919
- if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue;
1962
+ if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue;
1920
1963
  if (exists && status === "running") {
1921
1964
  if (!rawStatus) {
1922
1965
  const msgTokens = msg?.tokens;
@@ -1937,12 +1980,11 @@ function SubAgentPanel(props) {
1937
1980
  const desc = input?.description !== void 0 ? String(input.description) : "";
1938
1981
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
1939
1982
  let tokens;
1940
- const scanStMeta2 = st?.metadata;
1941
- const scanSubSid = scanStMeta2?.session_id !== void 0 ? String(scanStMeta2.session_id) : scanStMeta2?.sessionId !== void 0 ? String(scanStMeta2.sessionId) : void 0;
1942
1983
  if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid);
1943
1984
  const ended = status === "done";
1944
- next.set(id, {
1945
- id,
1985
+ const entryKey = existingKey ?? id;
1986
+ next.set(entryKey, {
1987
+ id: exists?.id ?? id,
1946
1988
  title,
1947
1989
  agent,
1948
1990
  prompt,
@@ -1953,6 +1995,7 @@ function SubAgentPanel(props) {
1953
1995
  startedAt: exists?.startedAt || Date.now(),
1954
1996
  endedAt: ended ? exists?.endedAt || Date.now() : void 0
1955
1997
  });
1998
+ if (entryKey !== id) next.delete(id);
1956
1999
  }
1957
2000
  }
1958
2001
  }
@@ -2098,13 +2141,21 @@ function SubAgentPanel(props) {
2098
2141
  return next;
2099
2142
  });
2100
2143
  };
2101
- const sep = () => "\u2500".repeat(Math.max(1, panelWidth()));
2144
+ const gutter = createMemo(() => props.borderVisible() ? 6 : 0);
2145
+ const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - gutter()));
2146
+ createEffect(() => {
2147
+ props.borderVisible();
2148
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
2149
+ const w = Math.max(20, boxEl.width);
2150
+ setPanelWidth((prev) => prev === w ? prev : w);
2151
+ }
2152
+ });
2102
2153
  const expandedMaxLabelW = createMemo(() => {
2103
2154
  const labels = [t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"), t("error.label"), t("cost.label"), t("model.label"), t("todo.label"), t("session.label")];
2104
2155
  return Math.max(...labels.map((l) => visualWidth(l + ": ")));
2105
2156
  });
2106
2157
  const expandedPad = (label) => Math.max(0, expandedMaxLabelW() - visualWidth(label + ": "));
2107
- const expandedValAvail = () => Math.max(6, panelWidth() - INDENT - expandedMaxLabelW());
2158
+ const expandedValAvail = () => Math.max(6, panelWidth() - gutter() - INDENT - expandedMaxLabelW());
2108
2159
  const summaryParts = createMemo(() => {
2109
2160
  if (!anyEntry()) return null;
2110
2161
  const dot = "\u25CF";
@@ -2133,7 +2184,7 @@ function SubAgentPanel(props) {
2133
2184
  if (!open()) return false;
2134
2185
  const icon = "\u25BC";
2135
2186
  const need = visualWidth(icon) + 1 + visualWidth(t("panel.title")) + versionW + summaryCols();
2136
- return need <= panelWidth();
2187
+ return need <= panelWidth() - gutter();
2137
2188
  });
2138
2189
  const leftCols = createMemo(() => {
2139
2190
  const icon = open() ? "\u25BC" : "\u25B6";
@@ -2143,25 +2194,36 @@ function SubAgentPanel(props) {
2143
2194
  });
2144
2195
  const spacerCols = createMemo(() => {
2145
2196
  if (!anyEntry()) return 0;
2146
- return Math.max(0, panelWidth() - leftCols() - summaryCols());
2197
+ return Math.max(0, panelWidth() - gutter() - leftCols() - summaryCols());
2147
2198
  });
2148
- const valueCols = (label) => Math.max(4, panelWidth() - INDENT - visualWidth(label + ": "));
2199
+ const valueCols = (label) => Math.max(4, panelWidth() - gutter() - INDENT - visualWidth(label + ": "));
2149
2200
  return (() => {
2150
2201
  var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("span"), _el$4 = _$createElement("span");
2151
2202
  _$insertNode(_el$, _el$2);
2152
2203
  var _ref$ = boxEl;
2153
2204
  typeof _ref$ === "function" ? _$use(_ref$, _el$) : boxEl = _el$;
2154
- _$setProp(_el$, "border", false);
2155
- _$setProp(_el$, "paddingTop", 0);
2156
- _$setProp(_el$, "paddingBottom", 0);
2157
- _$setProp(_el$, "paddingLeft", 0);
2158
- _$setProp(_el$, "paddingRight", 0);
2159
- _$setProp(_el$, "flexDirection", "column");
2160
- _$setProp(_el$, "gap", 0);
2161
- _$setProp(_el$, "onSizeChange", () => {
2162
- const w = boxEl ? Math.max(20, boxEl.width ?? 0) : 28;
2163
- setPanelWidth((prev) => prev === w ? prev : w);
2164
- });
2205
+ _$spread(_el$, _$mergeProps({
2206
+ get border() {
2207
+ return props.borderVisible();
2208
+ }
2209
+ }, () => props.borderVisible() ? {
2210
+ borderColor: pal().border
2211
+ } : {}, {
2212
+ "paddingTop": 0,
2213
+ "paddingBottom": 0,
2214
+ get paddingLeft() {
2215
+ return props.borderVisible() ? 2 : 0;
2216
+ },
2217
+ get paddingRight() {
2218
+ return props.borderVisible() ? 2 : 0;
2219
+ },
2220
+ "flexDirection": "column",
2221
+ "gap": 0,
2222
+ "onSizeChange": () => {
2223
+ const w = boxEl ? Math.max(20, boxEl.width ?? 0) : 28;
2224
+ setPanelWidth((prev) => prev === w ? prev : w);
2225
+ }
2226
+ }), true);
2165
2227
  _$insertNode(_el$2, _el$3);
2166
2228
  _$insertNode(_el$2, _el$4);
2167
2229
  _$setProp(_el$2, "onMouseUp", () => {
@@ -2358,7 +2420,7 @@ function SubAgentPanel(props) {
2358
2420
  if (tk) w += visualWidth(tk);
2359
2421
  return w;
2360
2422
  };
2361
- const labelAvail = () => Math.max(6, panelWidth() - LEFT_PAD - suffixW());
2423
+ const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW());
2362
2424
  const labelText = () => {
2363
2425
  const max2 = labelAvail();
2364
2426
  const text = entry.title || entry.agent;
@@ -2755,7 +2817,7 @@ ${t("session.toast.copy_failed")}`, {
2755
2817
  const cancelLabel = () => ` ${t("cancel.label")}`;
2756
2818
  const dismissLabel = () => ` ${t("dismiss.label")}`;
2757
2819
  const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
2758
- const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2);
2820
+ const spacerW = () => Math.max(1, panelWidth() - gutter() - openW() - rightW - 2);
2759
2821
  return (() => {
2760
2822
  var _el$91 = _$createElement("box"), _el$95 = _$createElement("text");
2761
2823
  _$insertNode(_el$91, _el$95);
@@ -2856,7 +2918,7 @@ ${t("session.toast.copy_failed")}`, {
2856
2918
  const showTop = props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max();
2857
2919
  const left = showMore ? ` \u2193 ${hiddenBelow()} ${t("scroll.more")}` : " ";
2858
2920
  const right = props.sortOrder() === "desc" ? `\u2191 ${t("scroll.top")}` : `\u2193 ${t("scroll.bottom")}`;
2859
- const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0;
2921
+ const pad = showTop ? Math.max(1, panelWidth() - gutter() - visualWidth(left) - visualWidth(right)) : 0;
2860
2922
  return (() => {
2861
2923
  var _el$100 = _$createElement("box"), _el$101 = _$createElement("text"), _el$102 = _$createElement("span");
2862
2924
  _$insertNode(_el$100, _el$101);
@@ -2960,6 +3022,9 @@ function PluginRoot(props) {
2960
3022
  get scrollMode() {
2961
3023
  return props.signals.scrollMode;
2962
3024
  },
3025
+ get borderVisible() {
3026
+ return props.signals.borderVisible;
3027
+ },
2963
3028
  get sessionId() {
2964
3029
  return props.sessionID;
2965
3030
  }
@@ -2984,6 +3049,7 @@ var mod = {
2984
3049
  const [maxSignal, setMaxEntries] = createSignal3(Number(api.kv.get(SETTING_KEYS.maxEntries, "10")) || 10);
2985
3050
  const [orderSignal, setSortOrder] = createSignal3(String(api.kv.get(SETTING_KEYS.order, "desc")) === "asc" ? "asc" : "desc");
2986
3051
  const [scrollSignal, setScrollMode] = createSignal3(String(api.kv.get(SETTING_KEYS.scrollMode, "wheel")) === "click" ? "click" : "wheel");
3052
+ const [borderSignal, setBorderVisible] = createSignal3(api.kv.get(SETTING_KEYS.border, false) === true);
2987
3053
  lang = langSignal;
2988
3054
  maxEntries = maxSignal;
2989
3055
  sortOrder = orderSignal;
@@ -2997,6 +3063,8 @@ var mod = {
2997
3063
  setSortOrder,
2998
3064
  scrollMode,
2999
3065
  setScrollMode,
3066
+ borderVisible: borderSignal,
3067
+ setBorderVisible,
3000
3068
  sessionId: ""
3001
3069
  };
3002
3070
  context.ui.slot({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-subagent-magazine",
3
- "version": "1.6.0-beta.2",
3
+ "version": "1.6.0-beta.4",
4
4
  "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.6.0-beta.2";
2
+ export const PLUGIN_VERSION="1.6.0-beta.4";
package/src/core/color.ts CHANGED
@@ -1,70 +1,70 @@
1
- /** Morandi palette helpers — desaturate any theme color toward a muted fallback. */
2
-
3
- export function rgb(raw: unknown): { r: number; g: number; b: number } | null {
4
- if (typeof raw === "string" && raw.startsWith("#")) {
5
- const h = raw.slice(1)
6
- return {
7
- r: parseInt(h.slice(0, 2), 16),
8
- g: parseInt(h.slice(2, 4), 16),
9
- b: parseInt(h.slice(4, 6), 16),
10
- }
11
- }
12
- if (raw && typeof raw === "object") {
13
- const o = raw as Record<string, unknown>
14
- if (typeof o.r === "number" && typeof o.g === "number" && typeof o.b === "number") {
15
- const scale = o.r > 1 || o.g > 1 || o.b > 1 ? 1 : 255
16
- return { r: Math.round(o.r * scale), g: Math.round(o.g * scale), b: Math.round(o.b * scale) }
17
- }
18
- }
19
- return null
20
- }
21
-
22
- export function saturation(r: number, g: number, b: number): number {
23
- const max = Math.max(r, g, b) / 255
24
- const min = Math.min(r, g, b) / 255
25
- const delta = max - min
26
- if (delta === 0) return 0
27
- const L = (max + min) / 2
28
- return L <= 0.5 ? delta / (max + min) : delta / (2 - max - min)
29
- }
30
-
31
- function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
32
- const c = rgb(raw)
33
- if (!c) return fallback
34
- const sat = saturation(c.r, c.g, c.b)
35
- if (sat <= maxSat) {
36
- return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
37
- }
38
- const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114
39
- let lo = 0, hi = 1
40
- for (let i = 0; i < 12; i++) {
41
- const mid = (lo + hi) / 2
42
- const nr = Math.round(c.r + (luma - c.r) * mid)
43
- const ng = Math.round(c.g + (luma - c.g) * mid)
44
- const nb = Math.round(c.b + (luma - c.b) * mid)
45
- if (saturation(nr, ng, nb) > maxSat) lo = mid
46
- else hi = mid
47
- }
48
- const nr = Math.round(c.r + (luma - c.r) * hi)
49
- const ng = Math.round(c.g + (luma - c.g) * hi)
50
- const nb = Math.round(c.b + (luma - c.b) * hi)
51
- return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
52
- }
53
-
54
- function dimColor(hex: string, factor = 0.5): string {
55
- const c = rgb(hex)
56
- if (!c) return hex
57
- const r = Math.round(c.r * factor)
58
- const g = Math.round(c.g * factor)
59
- const b = Math.round(c.b * factor)
60
- return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
61
- }
62
-
63
- export const FALLBACK = {
64
- primary: "#8B9DAF", text: "#C5C5BB", muted: "#7A7A72",
65
- success: "#9CAF8B", warning: "#C5B88D", error: "#B08A8A", border: "#6B6B63",
66
- } as const
67
-
68
- export const MAX_SAT = 0.28
69
-
70
- export { desaturateTo, dimColor }
1
+ /** Morandi palette helpers — desaturate any theme color toward a muted fallback. */
2
+
3
+ export function rgb(raw: unknown): { r: number; g: number; b: number } | null {
4
+ if (typeof raw === "string" && raw.startsWith("#")) {
5
+ const h = raw.slice(1)
6
+ return {
7
+ r: parseInt(h.slice(0, 2), 16),
8
+ g: parseInt(h.slice(2, 4), 16),
9
+ b: parseInt(h.slice(4, 6), 16),
10
+ }
11
+ }
12
+ if (raw && typeof raw === "object") {
13
+ const o = raw as Record<string, unknown>
14
+ if (typeof o.r === "number" && typeof o.g === "number" && typeof o.b === "number") {
15
+ const scale = o.r > 1 || o.g > 1 || o.b > 1 ? 1 : 255
16
+ return { r: Math.round(o.r * scale), g: Math.round(o.g * scale), b: Math.round(o.b * scale) }
17
+ }
18
+ }
19
+ return null
20
+ }
21
+
22
+ export function saturation(r: number, g: number, b: number): number {
23
+ const max = Math.max(r, g, b) / 255
24
+ const min = Math.min(r, g, b) / 255
25
+ const delta = max - min
26
+ if (delta === 0) return 0
27
+ const L = (max + min) / 2
28
+ return L <= 0.5 ? delta / (max + min) : delta / (2 - max - min)
29
+ }
30
+
31
+ function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
32
+ const c = rgb(raw)
33
+ if (!c) return fallback
34
+ const sat = saturation(c.r, c.g, c.b)
35
+ if (sat <= maxSat) {
36
+ return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
37
+ }
38
+ const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114
39
+ let lo = 0, hi = 1
40
+ for (let i = 0; i < 12; i++) {
41
+ const mid = (lo + hi) / 2
42
+ const nr = Math.round(c.r + (luma - c.r) * mid)
43
+ const ng = Math.round(c.g + (luma - c.g) * mid)
44
+ const nb = Math.round(c.b + (luma - c.b) * mid)
45
+ if (saturation(nr, ng, nb) > maxSat) lo = mid
46
+ else hi = mid
47
+ }
48
+ const nr = Math.round(c.r + (luma - c.r) * hi)
49
+ const ng = Math.round(c.g + (luma - c.g) * hi)
50
+ const nb = Math.round(c.b + (luma - c.b) * hi)
51
+ return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
52
+ }
53
+
54
+ function dimColor(hex: string, factor = 0.5): string {
55
+ const c = rgb(hex)
56
+ if (!c) return hex
57
+ const r = Math.round(c.r * factor)
58
+ const g = Math.round(c.g * factor)
59
+ const b = Math.round(c.b * factor)
60
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
61
+ }
62
+
63
+ export const FALLBACK = {
64
+ primary: "#8B9DAF", text: "#C5C5BB", muted: "#7A7A72",
65
+ success: "#9CAF8B", warning: "#C5B88D", error: "#B08A8A", border: "#6B6B63",
66
+ } as const
67
+
68
+ export const MAX_SAT = 0.28
69
+
70
+ export { desaturateTo, dimColor }
@@ -1,61 +1,61 @@
1
- /** Visual width of a single character (CJK/wide chars count as 2). */
2
- function charColumns(c: string): number {
3
- const code = c.codePointAt(0) ?? 0
4
- if (code < 0x20) return 0
5
- if (code < 0x7f) return 1
6
- if (code < 0xa0) return 0
7
- if (
8
- (code >= 0x1100 && code <= 0x115f) ||
9
- (code >= 0x2e80 && code <= 0xa4cf) ||
10
- (code >= 0xac00 && code <= 0xd7a3) ||
11
- (code >= 0xf900 && code <= 0xfaff) ||
12
- (code >= 0xfe10 && code <= 0xfe6f) ||
13
- (code >= 0xff01 && code <= 0xff60) ||
14
- (code >= 0xffe0 && code <= 0xffe6) ||
15
- (code >= 0x1f300 && code <= 0x1f64f) ||
16
- (code >= 0x20000 && code <= 0x3fffd)
17
- )
18
- return 2
19
- return 1
20
- }
21
-
22
- export function visualWidth(s: string): number {
23
- let w = 0
24
- for (const c of s) w += charColumns(c)
25
- return w
26
- }
27
-
28
- export function truncate(text: string, maxCols: number): string {
29
- if (visualWidth(text) <= maxCols) return text
30
- let cols = 0
31
- let i = 0
32
- for (const c of text) {
33
- const w = charColumns(c)
34
- if (cols + w > maxCols - 1) break
35
- cols += w
36
- i += c.length
37
- }
38
- return text.slice(0, i) + "\u2026"
39
- }
40
-
41
- export function fmtDurationShort(ms: number, running: boolean): string {
42
- if (running && ms < 2000) return ""
43
- if (ms < 1000) return (ms / 1000).toFixed(2) + "s"
44
- if (ms < 60000) return (ms / 1000).toFixed(2) + "s"
45
- const m = Math.floor(ms / 60000)
46
- const s = Math.round((ms % 60000) / 1000)
47
- return `${m}m${s}s`
48
- }
49
-
50
- export function fmtTokens(n: number): string {
51
- if (n < 1000) return `${n}`
52
- if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
53
- return `${(n / 1000000).toFixed(1)}M`
54
- }
55
-
56
- export function safeErrorMsg(err: unknown): string {
57
- if (!err) return ""
58
- if (typeof err === "string") return err
59
- if (typeof err === "object") return String((err as any).message || (err as any).code || "")
60
- return ""
61
- }
1
+ /** Visual width of a single character (CJK/wide chars count as 2). */
2
+ function charColumns(c: string): number {
3
+ const code = c.codePointAt(0) ?? 0
4
+ if (code < 0x20) return 0
5
+ if (code < 0x7f) return 1
6
+ if (code < 0xa0) return 0
7
+ if (
8
+ (code >= 0x1100 && code <= 0x115f) ||
9
+ (code >= 0x2e80 && code <= 0xa4cf) ||
10
+ (code >= 0xac00 && code <= 0xd7a3) ||
11
+ (code >= 0xf900 && code <= 0xfaff) ||
12
+ (code >= 0xfe10 && code <= 0xfe6f) ||
13
+ (code >= 0xff01 && code <= 0xff60) ||
14
+ (code >= 0xffe0 && code <= 0xffe6) ||
15
+ (code >= 0x1f300 && code <= 0x1f64f) ||
16
+ (code >= 0x20000 && code <= 0x3fffd)
17
+ )
18
+ return 2
19
+ return 1
20
+ }
21
+
22
+ export function visualWidth(s: string): number {
23
+ let w = 0
24
+ for (const c of s) w += charColumns(c)
25
+ return w
26
+ }
27
+
28
+ export function truncate(text: string, maxCols: number): string {
29
+ if (visualWidth(text) <= maxCols) return text
30
+ let cols = 0
31
+ let i = 0
32
+ for (const c of text) {
33
+ const w = charColumns(c)
34
+ if (cols + w > maxCols - 1) break
35
+ cols += w
36
+ i += c.length
37
+ }
38
+ return text.slice(0, i) + "\u2026"
39
+ }
40
+
41
+ export function fmtDurationShort(ms: number, running: boolean): string {
42
+ if (running && ms < 2000) return ""
43
+ if (ms < 1000) return (ms / 1000).toFixed(2) + "s"
44
+ if (ms < 60000) return (ms / 1000).toFixed(2) + "s"
45
+ const m = Math.floor(ms / 60000)
46
+ const s = Math.round((ms % 60000) / 1000)
47
+ return `${m}m${s}s`
48
+ }
49
+
50
+ export function fmtTokens(n: number): string {
51
+ if (n < 1000) return `${n}`
52
+ if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
53
+ return `${(n / 1000000).toFixed(1)}M`
54
+ }
55
+
56
+ export function safeErrorMsg(err: unknown): string {
57
+ if (!err) return ""
58
+ if (typeof err === "string") return err
59
+ if (typeof err === "object") return String((err as any).message || (err as any).code || "")
60
+ return ""
61
+ }