refactor(ts): one compactNumber and one reasoning-effort value set in @hermes/shared
Three hand-rolled compact-number formatters and two mirrored copies of the
reasoning-effort value set collapse into apps/shared/src/format.ts and
apps/shared/src/reasoning-effort.ts, exported from the package root and as
the subpaths `@hermes/shared/format` / `@hermes/shared/reasoning-effort`
(the TUI compiles with lib ES2023 and imports subpaths only). Surfaces keep
their own label maps and UI helpers. No re-export shims remain.
Convention for compactNumber (desktop's implementation, moved verbatim):
lowercase 'k', uppercase 'M', promotion-guarded thresholds (>= 999.5 -> k,
>= 999_950 -> M) so rounding can never print "1000k", trailing ".0"
stripped, non-finite / <= 0 -> "0".
Sites (path::symbol -> canonical):
apps/desktop/src/lib/format.ts::compactNumber -> apps/shared/src/format.ts::compactNumber (moved; file deleted)
web/src/lib/format.ts::formatTokenCount -> deleted
ui-tui/src/lib/text.ts::fmtK -> deleted (text.ts's own callers use compactNumber)
apps/desktop/src/app/agents/index.tsx -> @hermes/shared
apps/desktop/src/app/chat/sidebar/chrome.tsx -> @hermes/shared
apps/desktop/src/app/chat/sidebar/session-row.tsx -> @hermes/shared
apps/desktop/src/app/command-center/index.tsx -> @hermes/shared
apps/desktop/src/app/shell/context-usage-panel.tsx -> @hermes/shared
apps/desktop/src/app/shell/titlebar-controls.tsx -> @hermes/shared
apps/desktop/src/app/skills/index.tsx -> @hermes/shared
apps/desktop/src/app/skills/mcp-tab.tsx -> @hermes/shared
apps/desktop/src/components/ui/tab-dropdown.tsx -> @hermes/shared
apps/desktop/src/lib/statusbar.tsx -> @hermes/shared
apps/desktop/src/sdk/index.ts::compactNumber -> re-exported from @hermes/shared (plugin SDK surface unchanged)
apps/desktop/src/plugins/kanban/{board,drawer}.tsx -> unchanged (import via @hermes/plugin-sdk)
web/src/components/ModelInfoCard.tsx::formatTokenCount -> @hermes/shared::compactNumber
web/src/pages/ModelsPage.tsx::formatTokenCount -> @hermes/shared::compactNumber
ui-tui/src/components/appChrome.tsx::fmtK -> @hermes/shared/format::compactNumber
ui-tui/src/components/thinking.tsx::fmtK -> @hermes/shared/format::compactNumber
ui-tui/src/app/slash/commands/session.ts::fmtK -> @hermes/shared/format::compactNumber
ui-tui/src/__tests__/text.test.ts::fmtK suite -> apps/shared/src/format.test.ts (table incl. promotion guard)
apps/desktop/src/lib/reasoning-effort.ts::REASONING_EFFORTS/REASONING_EFFORT_VALUES/
DEFAULT_REASONING_EFFORT/ReasoningEffort/isReasoningEffort -> apps/shared/src/reasoning-effort.ts
(SHORT_LABELS, reasoningEffortLabel, isThinkingEnabled, resolveReasoningEffort stay local)
apps/desktop/src/app/settings/constants.ts -> @hermes/shared
apps/desktop/src/app/settings/model-settings.tsx -> @hermes/shared
apps/desktop/src/app/shell/model-catalog-menu.tsx -> @hermes/shared (+ local reasoningEffortLabel)
apps/desktop/src/app/shell/model-edit-submenu.tsx -> @hermes/shared (+ local UI helpers)
apps/desktop/src/app/shell/model-menu-panel.tsx -> @hermes/shared
apps/desktop/src/lib/model-status-label.ts -> @hermes/shared (+ local reasoningEffortLabel)
apps/desktop/src/sdk/index.ts -> value set re-exported from @hermes/shared; label helper stays from '@/lib/reasoning-effort'
apps/desktop/src/lib/reasoning-effort.test.ts -> value-set + isReasoningEffort cases moved to apps/shared/src/reasoning-effort.test.ts
web/src/lib/reasoning-effort.ts::EFFORT_OPTIONS -> labels mapped over shared REASONING_EFFORT_VALUES (same order: none, then 7 levels)
web/src/lib/reasoning-effort.ts::VALID_EFFORTS -> Set(REASONING_EFFORT_VALUES); normalizeEffort falls back to DEFAULT_REASONING_EFFORT
Semantics kept: web `none` is selectable; desktop `none` resolves to ''
(thinking off); desktop isReasoningEffort still trims + lowercases.
Behavior change:
- web: token counts on the Models page and ModelInfoCard now print a
lowercase 'k' and are promotion-guarded: 128_000 "128K" -> "128k",
999_999 "1000.0K" -> "1M", 1_500 "1.5K" -> "1.5k". 'M' is unchanged.
- TUI: fmtK used Intl compact notation; compactNumber differs only in
suffix case and the guard: 1_000_000 "1m" -> "1M", and billions no
longer get a 'b' suffix (1_000_000_000 "1b" -> "1000M"). Sub-million
values are identical ("999", "1k", "1.5k"). Non-positive values now
print "0" instead of "-1k".
- desktop: none (its formatter moved verbatim).
Tests: apps/shared/src/format.test.ts::"compactNumber" (table incl.
999_999 -> "1M", 999_949 -> "999.9k"; fails when the promotion guard is
removed) and apps/shared/src/reasoning-effort.test.ts::"reasoning-effort"
(no duplicate values, `none` is the only non-level, default is a member;
fails on a duplicated level or a `none`-accepting isReasoningEffort).
This commit is contained in:
@@ -3,7 +3,7 @@ import { Brain, Eye, Gauge, Lightbulb, Wrench } from "lucide-react";
|
||||
import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
||||
import { api } from "@/lib/api";
|
||||
import type { ModelInfoResponse } from "@/lib/api";
|
||||
import { formatTokenCount } from "@/lib/format";
|
||||
import { compactNumber } from "@hermes/shared";
|
||||
|
||||
interface ModelInfoCardProps {
|
||||
/** Current model string from config state — used to detect changes */
|
||||
@@ -57,11 +57,11 @@ export function ModelInfoCard({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono font-semibold text-foreground">
|
||||
{formatTokenCount(info.effective_context_length)}
|
||||
{compactNumber(info.effective_context_length)}
|
||||
</span>
|
||||
{info.config_context_length > 0 ? (
|
||||
<span className="text-amber-500 text-xs">
|
||||
(override — auto: {formatTokenCount(info.auto_context_length)})
|
||||
(override — auto: {compactNumber(info.auto_context_length)})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-text-tertiary text-xs">
|
||||
@@ -78,7 +78,7 @@ export function ModelInfoCard({
|
||||
<span className="font-medium">Max Output</span>
|
||||
</div>
|
||||
<span className="font-mono font-semibold text-foreground">
|
||||
{formatTokenCount(caps.max_output_tokens)}
|
||||
{compactNumber(caps.max_output_tokens)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Format a token count as a human-readable string (e.g. 1M, 128K, 4096).
|
||||
* Strips trailing ".0" for clean round numbers.
|
||||
*/
|
||||
export function formatTokenCount(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;
|
||||
return String(n);
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
DEFAULT_REASONING_EFFORT,
|
||||
REASONING_EFFORT_VALUES,
|
||||
type ReasoningEffortValue,
|
||||
} from "@hermes/shared";
|
||||
|
||||
/**
|
||||
* Pure reasoning-effort helpers shared by the dashboard ReasoningPicker.
|
||||
*
|
||||
* Kept DOM-free so the node-environment vitest harness can cover the
|
||||
* resolution logic without loading React or the UI kit.
|
||||
*
|
||||
* Values mirror hermes_constants.VALID_REASONING_EFFORTS plus `none`
|
||||
* (thinking-off). An empty/unset config value means the Hermes default,
|
||||
* which is `medium`.
|
||||
* Values come from @hermes/shared (hermes_constants.VALID_REASONING_EFFORTS
|
||||
* plus `none`, thinking-off). An empty/unset config value means the Hermes
|
||||
* default.
|
||||
*/
|
||||
|
||||
export interface EffortOption {
|
||||
@@ -14,25 +20,28 @@ export interface EffortOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const EFFORT_OPTIONS: ReadonlyArray<EffortOption> = [
|
||||
{ value: "none", label: "Off (no thinking)" },
|
||||
{ value: "minimal", label: "Minimal" },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
{ value: "xhigh", label: "Extra High" },
|
||||
{ value: "max", label: "Max" },
|
||||
{ value: "ultra", label: "Ultra" },
|
||||
];
|
||||
const EFFORT_LABELS: Record<ReasoningEffortValue, string> = {
|
||||
none: "Off (no thinking)",
|
||||
minimal: "Minimal",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
xhigh: "Extra High",
|
||||
max: "Max",
|
||||
ultra: "Ultra",
|
||||
};
|
||||
|
||||
export const VALID_EFFORTS: ReadonlySet<string> = new Set(
|
||||
EFFORT_OPTIONS.map((o) => o.value),
|
||||
/** `none` first, then the seven levels ascending — the shared value order. */
|
||||
export const EFFORT_OPTIONS: ReadonlyArray<EffortOption> = REASONING_EFFORT_VALUES.map(
|
||||
(value) => ({ value, label: EFFORT_LABELS[value] }),
|
||||
);
|
||||
|
||||
export const VALID_EFFORTS: ReadonlySet<string> = new Set(REASONING_EFFORT_VALUES);
|
||||
|
||||
/** Normalize a raw `agent.reasoning_effort` config value to a selectable
|
||||
* option. Empty/unknown → `medium` (Hermes' default when unset). */
|
||||
export function normalizeEffort(raw: unknown): string {
|
||||
const value = String(raw ?? "").trim().toLowerCase();
|
||||
if (!value) return "medium";
|
||||
return VALID_EFFORTS.has(value) ? value : "medium";
|
||||
if (!value) return DEFAULT_REASONING_EFFORT;
|
||||
return VALID_EFFORTS.has(value) ? value : DEFAULT_REASONING_EFFORT;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
DASHBOARD_MODAL_PANEL,
|
||||
shouldCloseOuterModalOnEscape,
|
||||
} from "@/lib/dashboard-modal-shell";
|
||||
import { formatTokenCount } from "@/lib/format";
|
||||
import { compactNumber } from "@hermes/shared";
|
||||
import { Button } from "@nous-research/ui/ui/components/button";
|
||||
import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
||||
import { Stats } from "@nous-research/ui/ui/components/stats";
|
||||
@@ -430,12 +430,12 @@ function ModelCard({
|
||||
)}
|
||||
{caps.context_window && caps.context_window > 0 && (
|
||||
<span className="text-xs text-text-secondary">
|
||||
{formatTokenCount(caps.context_window)} ctx
|
||||
{compactNumber(caps.context_window)} ctx
|
||||
</span>
|
||||
)}
|
||||
{caps.max_output_tokens && caps.max_output_tokens > 0 && (
|
||||
<span className="text-xs text-text-secondary">
|
||||
{formatTokenCount(caps.max_output_tokens)} out
|
||||
{compactNumber(caps.max_output_tokens)} out
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user