pi-mega-compact 0.8.12 → 0.8.14

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.
Files changed (76) hide show
  1. package/extensions/dashboard-client/dist/assets/AchievementTiles-BH26e95w.js +2 -0
  2. package/extensions/dashboard-client/dist/assets/AchievementTiles-BH26e95w.js.map +1 -0
  3. package/extensions/dashboard-client/dist/assets/AchievementsTab-B94wvRV_.js +2 -0
  4. package/extensions/dashboard-client/dist/assets/AchievementsTab-B94wvRV_.js.map +1 -0
  5. package/extensions/dashboard-client/dist/assets/CacheTab-DPmw3NHR.js +2 -0
  6. package/extensions/dashboard-client/dist/assets/CacheTab-DPmw3NHR.js.map +1 -0
  7. package/extensions/dashboard-client/dist/assets/ConfigTab-C3g3smRL.js +2 -0
  8. package/extensions/dashboard-client/dist/assets/ConfigTab-C3g3smRL.js.map +1 -0
  9. package/extensions/dashboard-client/dist/assets/EventsTab-B5PicfFw.js +2 -0
  10. package/extensions/dashboard-client/dist/assets/EventsTab-B5PicfFw.js.map +1 -0
  11. package/extensions/dashboard-client/dist/assets/GameTab-CsVwp78a.js +2 -0
  12. package/extensions/dashboard-client/dist/assets/GameTab-CsVwp78a.js.map +1 -0
  13. package/extensions/dashboard-client/dist/assets/MetricsTab-Cf72UewX.js +2 -0
  14. package/extensions/dashboard-client/dist/assets/MetricsTab-Cf72UewX.js.map +1 -0
  15. package/extensions/dashboard-client/dist/assets/OverviewTab-Dh6aG3gy.js +2 -0
  16. package/extensions/dashboard-client/dist/assets/OverviewTab-Dh6aG3gy.js.map +1 -0
  17. package/extensions/dashboard-client/dist/assets/ReposTab-HXf2dorl.js +2 -0
  18. package/extensions/dashboard-client/dist/assets/ReposTab-HXf2dorl.js.map +1 -0
  19. package/extensions/dashboard-client/dist/assets/TimeSavedCard-kUnoospE.js +2 -0
  20. package/extensions/dashboard-client/dist/assets/TimeSavedCard-kUnoospE.js.map +1 -0
  21. package/extensions/dashboard-client/dist/assets/index-Cfxniu2R.css +1 -0
  22. package/extensions/dashboard-client/dist/assets/{index-XAIN8km6.js → index-DMntLu-8.js} +11 -10
  23. package/extensions/dashboard-client/dist/assets/index-DMntLu-8.js.map +1 -0
  24. package/extensions/dashboard-client/dist/index.html +2 -2
  25. package/extensions/dashboard-client/src/App.tsx +18 -4
  26. package/extensions/dashboard-client/src/components/AchievementTiles.tsx +105 -0
  27. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +116 -0
  28. package/extensions/dashboard-client/src/components/CacheHitsCard.tsx +73 -0
  29. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +78 -35
  30. package/extensions/dashboard-client/src/components/ConfigSummaryCard.tsx +101 -0
  31. package/extensions/dashboard-client/src/components/ContextGauge.tsx +11 -15
  32. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +42 -33
  33. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +46 -9
  34. package/extensions/dashboard-client/src/components/LegendCard.tsx +49 -0
  35. package/extensions/dashboard-client/src/components/ModelBadge.tsx +42 -11
  36. package/extensions/dashboard-client/src/components/PerfCards.tsx +91 -0
  37. package/extensions/dashboard-client/src/components/RepoAllSessionsCard.tsx +102 -0
  38. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +9 -2
  39. package/extensions/dashboard-client/src/components/RepoTable.tsx +30 -22
  40. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +226 -0
  41. package/extensions/dashboard-client/src/components/SessionInfo.tsx +25 -50
  42. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +20 -11
  43. package/extensions/dashboard-client/src/components/TimeSavedCard.tsx +61 -0
  44. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +31 -19
  45. package/extensions/dashboard-client/src/components/VectorStoreCard.tsx +134 -0
  46. package/extensions/dashboard-client/src/main.tsx +5 -0
  47. package/extensions/dashboard-client/src/styles/base.css +76 -9
  48. package/extensions/dashboard-client/src/styles/config.css +149 -0
  49. package/extensions/dashboard-client/src/styles/game-achievements.css +265 -0
  50. package/extensions/dashboard-client/src/styles/metrics-extra.css +69 -0
  51. package/extensions/dashboard-client/src/styles/overview-extra.css +170 -0
  52. package/extensions/dashboard-client/src/styles/repos-extra.css +99 -0
  53. package/extensions/dashboard-client/src/styles/repos-metrics.css +14 -23
  54. package/extensions/dashboard-client/src/tabs/AchievementsTab.tsx +55 -0
  55. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +50 -0
  56. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +240 -5
  57. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +55 -3
  58. package/extensions/dashboard-client/src/tabs/GameTab.tsx +287 -0
  59. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +7 -1
  60. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +103 -17
  61. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +57 -22
  62. package/extensions/dashboard-client/src/utils/format.ts +85 -0
  63. package/extensions/dashboard-client/src/utils/types.ts +27 -0
  64. package/package.json +1 -1
  65. package/extensions/dashboard-client/dist/assets/ConfigTab-RrjXWfvr.js +0 -2
  66. package/extensions/dashboard-client/dist/assets/ConfigTab-RrjXWfvr.js.map +0 -1
  67. package/extensions/dashboard-client/dist/assets/EventsTab-BH_FxJ08.js +0 -2
  68. package/extensions/dashboard-client/dist/assets/EventsTab-BH_FxJ08.js.map +0 -1
  69. package/extensions/dashboard-client/dist/assets/MetricsTab-C-PoFV4p.js +0 -2
  70. package/extensions/dashboard-client/dist/assets/MetricsTab-C-PoFV4p.js.map +0 -1
  71. package/extensions/dashboard-client/dist/assets/OverviewTab-Db-qBqFN.js +0 -2
  72. package/extensions/dashboard-client/dist/assets/OverviewTab-Db-qBqFN.js.map +0 -1
  73. package/extensions/dashboard-client/dist/assets/ReposTab-DhCjJvuj.js +0 -2
  74. package/extensions/dashboard-client/dist/assets/ReposTab-DhCjJvuj.js.map +0 -1
  75. package/extensions/dashboard-client/dist/assets/index-Buer5yxB.css +0 -1
  76. package/extensions/dashboard-client/dist/assets/index-XAIN8km6.js.map +0 -1
@@ -0,0 +1,170 @@
1
+ /**
2
+ * dashboard-client/src/styles/overview-extra.css — styles for new Overview cards.
3
+ *
4
+ * Covers: VectorStoreCard, RepoAllSessionsCard, DataSafetyCard,
5
+ * ConfigSummaryCard, CacheStatusPerModel (Model & Cost), SessionInfo (Crew),
6
+ * CacheHitsCard, TimeSavedCard, LegendCard, compression meters, bullet-na.
7
+ * Does NOT modify overview-events.css (integration phase handles that).
8
+ */
9
+
10
+ /* Overview card grid — allow cards to wrap naturally */
11
+ .overview-card-grid {
12
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
13
+ }
14
+
15
+ /* Stat rows (shared layout for all new cards) */
16
+ .ov-stat-row {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ align-items: baseline;
20
+ padding: 4px 0;
21
+ border-bottom: 1px solid var(--border);
22
+ font-size: clamp(13px, 1vw, 17px);
23
+ }
24
+ .ov-stat-label {
25
+ color: var(--muted);
26
+ font-size: 12px;
27
+ cursor: help;
28
+ }
29
+ .ov-stat-value {
30
+ font-weight: 600;
31
+ color: var(--text);
32
+ font-family: monospace;
33
+ }
34
+ .ov-value-ok {
35
+ color: var(--ok) !important;
36
+ }
37
+
38
+ /* Compression meter (Vector Store + Repo cards) */
39
+ .ov-compression-meter {
40
+ margin-top: 10px;
41
+ }
42
+ .meter-track {
43
+ height: clamp(6px, 0.6vw, 10px);
44
+ background: var(--bg);
45
+ border-radius: 5px;
46
+ overflow: hidden;
47
+ border: 1px solid var(--border);
48
+ }
49
+ .meter-fill {
50
+ height: 100%;
51
+ transition: width 0.4s ease;
52
+ border-radius: 4px;
53
+ }
54
+ .meter-green {
55
+ background: var(--ok);
56
+ }
57
+ .meter-yellow {
58
+ background: var(--warn);
59
+ }
60
+ .meter-red {
61
+ background: var(--err);
62
+ }
63
+ .meter-sub {
64
+ display: block;
65
+ margin-top: 4px;
66
+ font-size: 11px;
67
+ color: var(--muted);
68
+ }
69
+
70
+ /* Trigger status: na (yellow) bullet */
71
+ .bullet-na .bullet-dot {
72
+ background: var(--warn);
73
+ box-shadow: 0 0 6px var(--warn);
74
+ }
75
+ .bullet-na .bullet-label {
76
+ color: var(--warn);
77
+ }
78
+ .trigger-state-text {
79
+ margin: 6px 0 0;
80
+ font-size: 12px;
81
+ color: var(--muted);
82
+ }
83
+
84
+ /* Data safety card */
85
+ .data-safety-card .safe-note {
86
+ margin: 10px 0 0;
87
+ padding: 8px;
88
+ font-size: 11px;
89
+ color: var(--muted);
90
+ background: rgba(63, 185, 80, 0.06);
91
+ border-radius: 6px;
92
+ border: 1px solid rgba(63, 185, 80, 0.2);
93
+ }
94
+
95
+ /* Model & Cost Savings card */
96
+ .model-cost-card .ov-cost-usd {
97
+ font-size: clamp(16px, 1.4vw, 22px);
98
+ font-weight: 700;
99
+ color: #a371f7;
100
+ margin-bottom: 2px;
101
+ }
102
+ .model-cost-card .ov-cost-sub {
103
+ font-size: 12px;
104
+ color: var(--muted);
105
+ margin-bottom: 8px;
106
+ }
107
+
108
+ /* Crew / Agents card */
109
+ .crew-card .ov-stat-value {
110
+ font-family: inherit;
111
+ }
112
+
113
+ /* Legend card — full width */
114
+ .legend-card {
115
+ grid-column: 1 / -1;
116
+ }
117
+ .legend-card details {
118
+ cursor: pointer;
119
+ }
120
+ .legend-card summary {
121
+ font-size: 13px;
122
+ color: var(--muted);
123
+ font-weight: 600;
124
+ margin-bottom: 4px;
125
+ }
126
+ .legend-list {
127
+ margin: 8px 0;
128
+ padding-left: 20px;
129
+ font-size: 12px;
130
+ color: var(--muted);
131
+ line-height: 1.6;
132
+ }
133
+ .legend-list li {
134
+ margin-bottom: 4px;
135
+ }
136
+ .legend-list b {
137
+ color: var(--text);
138
+ }
139
+ .legend-note {
140
+ font-size: 11px;
141
+ color: var(--dim);
142
+ font-style: italic;
143
+ }
144
+
145
+ /* Cache tab layout */
146
+ .cache-tab {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 12px;
150
+ }
151
+
152
+ /* Responsive breakpoints */
153
+ @media (max-width: 640px) {
154
+ .overview-card-grid {
155
+ grid-template-columns: 1fr;
156
+ }
157
+ }
158
+
159
+ @media (min-width: 1025px) {
160
+ .overview-card-grid {
161
+ grid-template-columns: repeat(3, 1fr);
162
+ }
163
+ }
164
+
165
+ @media (min-width: 1601px) {
166
+ .overview-card-grid {
167
+ grid-template-columns: repeat(4, 1fr);
168
+ font-size: clamp(13px, 0.9vw, 16px);
169
+ }
170
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * dashboard-client/src/styles/repos-extra.css — Active repos + Savings by Model.
3
+ *
4
+ * Styles for ActiveReposTable and SavingsByModelTable components.
5
+ * Does NOT modify repos-metrics.css (per task constraint).
6
+ * Responsive: tables scroll horizontally on narrow screens.
7
+ */
8
+
9
+ /* Section headers within Repos tab */
10
+ .section-header {
11
+ font-size: 13px;
12
+ text-transform: uppercase;
13
+ letter-spacing: 0.5px;
14
+ color: var(--muted);
15
+ margin: 20px 0 10px;
16
+ font-weight: 600;
17
+ }
18
+
19
+ /* Horizontal scroll wrapper for wide tables */
20
+ .table-scroll {
21
+ overflow-x: auto;
22
+ -webkit-overflow-scrolling: touch;
23
+ }
24
+
25
+ /* Shared table styles for active-repos and savings tables */
26
+ .active-repos-table,
27
+ .savings-table {
28
+ width: 100%;
29
+ border-collapse: collapse;
30
+ font-size: 13px;
31
+ }
32
+
33
+ .active-repos-table th,
34
+ .savings-table th {
35
+ text-align: left;
36
+ padding: 6px 10px;
37
+ border-bottom: 1px solid var(--border);
38
+ white-space: nowrap;
39
+ }
40
+
41
+ .active-repos-table th.num,
42
+ .savings-table th.num {
43
+ text-align: right;
44
+ }
45
+
46
+ .active-repos-table td,
47
+ .savings-table td {
48
+ padding: 6px 10px;
49
+ border-bottom: 1px solid var(--border);
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .active-repos-table td.num,
54
+ .savings-table td.num {
55
+ text-align: right;
56
+ font-family: monospace;
57
+ }
58
+
59
+ .active-repos-table tbody tr:hover,
60
+ .savings-table tbody tr:hover {
61
+ background: var(--panel);
62
+ }
63
+
64
+ /* Model cell emphasis */
65
+ .savings-table .repo-model {
66
+ font-weight: 600;
67
+ }
68
+
69
+ /* Empty-state row */
70
+ .repo-empty {
71
+ text-align: center;
72
+ color: var(--muted);
73
+ padding: 24px;
74
+ }
75
+
76
+ /* Section spacing */
77
+ .active-repos-section {
78
+ margin-bottom: 8px;
79
+ }
80
+
81
+ .savings-by-model {
82
+ margin-bottom: 16px;
83
+ }
84
+
85
+ /* Responsive: ensure tables have min-width so columns don't crush */
86
+ @media (max-width: 640px) {
87
+ .active-repos-table {
88
+ min-width: 900px;
89
+ }
90
+ .savings-table {
91
+ min-width: 1200px;
92
+ }
93
+ }
94
+
95
+ @media (min-width: 641px) and (max-width: 1024px) {
96
+ .savings-table {
97
+ min-width: 1000px;
98
+ }
99
+ }
@@ -187,9 +187,8 @@
187
187
 
188
188
  /* Model badge */
189
189
  .model-badge {
190
- display: flex;
191
- flex-wrap: wrap;
192
- align-items: center;
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
193
192
  gap: 8px;
194
193
  padding: 12px 14px;
195
194
  background: var(--panel);
@@ -197,26 +196,19 @@
197
196
  border-radius: 8px;
198
197
  margin-bottom: 12px;
199
198
  }
200
- .model-name {
201
- font-size: 15px;
202
- font-weight: 600;
203
- }
204
- .model-provider {
205
- color: var(--accent);
206
- font-size: 13px;
207
- }
208
- .model-provider-id {
209
- color: var(--muted);
210
- font-size: 11px;
211
- }
212
- .model-rates {
213
- margin-left: auto;
199
+ .model-field {
214
200
  display: flex;
215
- gap: 12px;
201
+ flex-direction: column;
202
+ gap: 2px;
216
203
  }
217
- .model-rates .rate {
218
- font-size: 12px;
204
+ .model-field-label {
205
+ font-size: 11px;
219
206
  color: var(--muted);
207
+ text-transform: uppercase;
208
+ }
209
+ .model-field-value {
210
+ font-size: 14px;
211
+ font-weight: 600;
220
212
  }
221
213
 
222
214
  /* Perf chart */
@@ -300,8 +292,7 @@
300
292
  .modal-grid {
301
293
  grid-template-columns: 1fr 1fr;
302
294
  }
303
- .model-rates {
304
- margin-left: 0;
305
- width: 100%;
295
+ .model-badge {
296
+ grid-template-columns: 1fr 1fr;
306
297
  }
307
298
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).
3
+ *
4
+ * Renders the shared AchievementTiles component standalone, plus a toast area.
5
+ * Polls /api/achievements every 15s.
6
+ */
7
+
8
+ import type React from "react";
9
+ import { useState, useEffect, useCallback } from "react";
10
+ import type { AchievementRow } from "@contracts";
11
+ import { fetchAchievements } from "../api/client";
12
+ import { AchievementTiles } from "../components/AchievementTiles";
13
+
14
+ export default function AchievementsTab(): React.ReactElement {
15
+ const [achievements, setAchievements] = useState<AchievementRow[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState<Error | null>(null);
18
+
19
+ const doFetch = useCallback(async (): Promise<void> => {
20
+ try {
21
+ const rows = await fetchAchievements();
22
+ setAchievements(rows);
23
+ setError(null);
24
+ } catch (e) {
25
+ setError(e instanceof Error ? e : new Error(String(e)));
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ let active = true;
33
+ const run = async (): Promise<void> => {
34
+ if (active) await doFetch();
35
+ };
36
+ void run();
37
+ const timer = setInterval(() => void run(), 15000);
38
+ return () => {
39
+ active = false;
40
+ clearInterval(timer);
41
+ };
42
+ }, [doFetch]);
43
+
44
+ if (loading && achievements.length === 0)
45
+ return <div className="tab-stub">Loading achievements…</div>;
46
+ if (error && achievements.length === 0)
47
+ return <div className="tab-stub">Error: {error.message}</div>;
48
+
49
+ return (
50
+ <div className="achievements-tab">
51
+ <h2>Achievements</h2>
52
+ <AchievementTiles achievements={achievements} />
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * dashboard-client/src/tabs/CacheTab.tsx — Cache tab (NEW).
3
+ *
4
+ * Renders CacheHitsCard + TimeSavedCard from /api/snapshot
5
+ * via the useApi hook with 5s polling.
6
+ */
7
+
8
+ import type React from "react";
9
+ import { useCallback } from "react";
10
+ import type { SnapshotResponse } from "@contracts";
11
+ import { useApi } from "../hooks/useApi";
12
+ import { fetchSnapshot } from "../api/client";
13
+ import { CacheHitsCard } from "../components/CacheHitsCard";
14
+ import { TimeSavedCard } from "../components/TimeSavedCard";
15
+
16
+ export default function CacheTab(): React.ReactElement {
17
+ const { data: snapshot, loading, error } = useApi<SnapshotResponse>(
18
+ useCallback(() => fetchSnapshot(), []),
19
+ { pollInterval: 5000 },
20
+ );
21
+
22
+ if (loading && !snapshot)
23
+ return <div className="tab-stub">Loading snapshot…</div>;
24
+ if (error && !snapshot)
25
+ return <div className="tab-stub">Error: {error.message}</div>;
26
+ if (!snapshot) return <div className="tab-stub">No snapshot data.</div>;
27
+
28
+ const { cacheHits, compacts, timeSaved } = snapshot;
29
+
30
+ return (
31
+ <div className="cache-tab">
32
+ <div className="card-grid overview-card-grid">
33
+ <CacheHitsCard
34
+ cacheHitsSession={cacheHits.session}
35
+ cacheHitsTotal={cacheHits.total}
36
+ tokensSavedSession={cacheHits.sessionTokensSaved}
37
+ tokensSavedTotal={cacheHits.totalTokensSaved}
38
+ compactionsSession={compacts.session}
39
+ compactionsTotal={compacts.total}
40
+ />
41
+ <TimeSavedCard
42
+ compactSessionSec={timeSaved.compact.sessionSec}
43
+ compactTotalSec={timeSaved.compact.totalSec}
44
+ cacheHitSessionSec={timeSaved.cacheHit.sessionSec}
45
+ cacheHitTotalSec={timeSaved.cacheHit.totalSec}
46
+ />
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -1,16 +1,251 @@
1
1
  /**
2
- * dashboard-client/src/tabs/ConfigTab.tsx — Config tab (B1 stub).
2
+ * dashboard-client/src/tabs/ConfigTab.tsx — Config tab (C3).
3
3
  *
4
- * SPRINT-C3: config form, game-mode settings, theme picker.
4
+ * Full configuration editor:
5
+ * (1) Game mode toggle (checkbox)
6
+ * (2) Theme selector (dropdown) from src/config/themes.ts THEMES array
7
+ * (3) TUI mode selector (Full/Minimal dropdown)
8
+ * (4) Read-only config display section from snapshot (Tier, Preset, Pressure,
9
+ * Threshold, Fast Gate, Auto, Anchor) with verbatim tooltips from html.ts.
10
+ *
11
+ * Uses fetchGameState() to load current state, putGameState() to save changes.
12
+ * Polls gameState and snapshot every 5s. Applies theme by setting
13
+ * document.documentElement.dataset.theme.
5
14
  */
6
15
 
7
16
  import type React from "react";
17
+ import { useState, useEffect, useCallback } from "react";
18
+ import type { SnapshotResponse, GameStatePatch } from "@contracts";
19
+ import { fetchSnapshot, fetchGameState, putGameState } from "../api/client";
20
+ import { THEMES } from "../../../../src/config/themes";
21
+
22
+ /** Actual flat game state returned by the server (the server returns a flat
23
+ * { game_mode_on, theme, tui_display_mode } shape, NOT the contract's nested
24
+ * { config: { enabled, theme, displayMode }, activeRitual } shape). */
25
+ interface FlatGameState {
26
+ game_mode_on: boolean;
27
+ theme: string;
28
+ tui_display_mode: "full" | "minimal";
29
+ }
30
+ type FlatGameStatePatch = Partial<FlatGameState>;
31
+
32
+ /** Extended snapshot config — the runtime includes tierPct (not in contract). */
33
+ interface ConfigExt extends SnapshotResponse["config"] {
34
+ tierPct?: number | null;
35
+ }
36
+
37
+ /* Tooltips copied verbatim from extensions/dashboard-server/html.ts */
38
+ const TOOLTIPS = {
39
+ gameMode: "Turn game mode on/off (themes the widget + dashboard)",
40
+ theme: "Visual theme (applies instantly)",
41
+ tuiMode: "TUI widget display density",
42
+ tier: "Live pressure band — climbs low\u2192mega as context fills the window.",
43
+ preset:
44
+ "The env-resolved base compaction preset (low/medium/high/ultra/mega) that set the token threshold.",
45
+ pressure:
46
+ "Live pressure = currentTokens / threshold \u2014 % of the model context window (threshold fires at the tier's % of window).",
47
+ threshold:
48
+ "Compaction threshold = tierPct \u00d7 model context window \u2014 mega-compact trims BELOW pi's native ~80% auto-compact for any model size.",
49
+ fastGate:
50
+ "Fast-gate arming floor \u2014 the live trim arms once context passes this % of the window.",
51
+ } as const;
52
+
53
+ function formatThreshold(
54
+ tokens: number,
55
+ tierPct: number | null | undefined,
56
+ cw: number,
57
+ ): string {
58
+ let txt = tokens.toLocaleString();
59
+ if (tierPct != null && cw > 0) {
60
+ txt += ` (${Math.round(tierPct * 100)}% of ${cw.toLocaleString()})`;
61
+ }
62
+ return txt;
63
+ }
64
+
65
+ function formatPressure(pressure: number): string {
66
+ /* Runtime sends 0\u20131 fraction despite contract saying 0\u2013100. */
67
+ return `${Math.round((pressure || 0) * 100)}%`;
68
+ }
8
69
 
9
70
  export default function ConfigTab(): React.ReactElement {
71
+ const [snapshot, setSnapshot] = useState<SnapshotResponse | null>(null);
72
+ const [gameState, setGameState] = useState<FlatGameState | null>(null);
73
+ const [saving, setSaving] = useState(false);
74
+
75
+ /* Poll snapshot every 5s for config display section. */
76
+ useEffect(() => {
77
+ let active = true;
78
+ const doFetch = async (): Promise<void> => {
79
+ try {
80
+ const s = await fetchSnapshot();
81
+ if (active) setSnapshot(s);
82
+ } catch {
83
+ /* non-fatal */
84
+ }
85
+ };
86
+ void doFetch();
87
+ const timer = setInterval(() => void doFetch(), 5000);
88
+ return () => {
89
+ active = false;
90
+ clearInterval(timer);
91
+ };
92
+ }, []);
93
+
94
+ /* Poll game state every 5s. */
95
+ useEffect(() => {
96
+ let active = true;
97
+ const doFetch = async (): Promise<void> => {
98
+ try {
99
+ const gs = (await fetchGameState()) as unknown as FlatGameState;
100
+ if (active) {
101
+ setGameState(gs);
102
+ if (gs.theme)
103
+ document.documentElement.dataset.theme = gs.theme;
104
+ }
105
+ } catch {
106
+ /* non-fatal */
107
+ }
108
+ };
109
+ void doFetch();
110
+ const timer = setInterval(() => void doFetch(), 5000);
111
+ return () => {
112
+ active = false;
113
+ clearInterval(timer);
114
+ };
115
+ }, []);
116
+
117
+ /* PUT a patch to the server immediately on change. */
118
+ const handlePatch = useCallback(async (patch: FlatGameStatePatch) => {
119
+ setSaving(true);
120
+ try {
121
+ const result = (await putGameState(
122
+ patch as unknown as GameStatePatch,
123
+ )) as unknown as FlatGameState;
124
+ setGameState(result);
125
+ if (result.theme)
126
+ document.documentElement.dataset.theme = result.theme;
127
+ } catch {
128
+ /* non-fatal */
129
+ }
130
+ setSaving(false);
131
+ }, []);
132
+
133
+ const gmOn = gameState?.game_mode_on ?? false;
134
+ const themeVal = gameState?.theme ?? "transparent";
135
+ const tuiVal = gameState?.tui_display_mode ?? "full";
136
+
137
+ const cfg = snapshot?.config as ConfigExt | undefined;
138
+ const cw = snapshot?.context.contextWindow ?? 0;
139
+
10
140
  return (
11
- <div className="tab-stub">
12
- <strong>Config</strong> (B1 stub)
13
- {/* SPRINT-C3-REMAINING: /mega-compact-settings form, theme picker */}
141
+ <div className="config-tab">
142
+ <section className="config-settings">
143
+ <h3>Settings</h3>
144
+ <div className="settings-row">
145
+ <label title={TOOLTIPS.gameMode}>
146
+ <input
147
+ type="checkbox"
148
+ checked={gmOn}
149
+ onChange={(e) =>
150
+ handlePatch({ game_mode_on: e.currentTarget.checked })
151
+ }
152
+ disabled={saving}
153
+ />
154
+ Game Mode
155
+ </label>
156
+ </div>
157
+ <div className="settings-row">
158
+ <label title={TOOLTIPS.theme}>
159
+ Theme
160
+ <select
161
+ value={themeVal}
162
+ onChange={(e) => handlePatch({ theme: e.currentTarget.value })}
163
+ disabled={saving}
164
+ >
165
+ {THEMES.map((t) => (
166
+ <option key={t.id} value={t.id}>
167
+ {t.label}
168
+ </option>
169
+ ))}
170
+ </select>
171
+ </label>
172
+ </div>
173
+ <div className="settings-row">
174
+ <label title={TOOLTIPS.tuiMode}>
175
+ TUI Mode
176
+ <select
177
+ value={tuiVal}
178
+ onChange={(e) =>
179
+ handlePatch({
180
+ tui_display_mode: e.currentTarget.value as "full" | "minimal",
181
+ })
182
+ }
183
+ disabled={saving}
184
+ >
185
+ <option value="full">Full</option>
186
+ <option value="minimal">Minimal</option>
187
+ </select>
188
+ </label>
189
+ </div>
190
+ </section>
191
+
192
+ <section className="config-display">
193
+ <h3>Configuration</h3>
194
+ <div className="config-grid">
195
+ <div className="config-item">
196
+ <span className="config-label" title={TOOLTIPS.tier}>
197
+ Tier
198
+ </span>
199
+ <span className="config-value">
200
+ {snapshot ? `${snapshot.tier} (live)` : "\u2014"}
201
+ </span>
202
+ </div>
203
+ <div className="config-item">
204
+ <span className="config-label" title={TOOLTIPS.preset}>
205
+ Preset
206
+ </span>
207
+ <span className="config-value">
208
+ {snapshot?.presetTier ?? "\u2014"}
209
+ </span>
210
+ </div>
211
+ <div className="config-item">
212
+ <span className="config-label" title={TOOLTIPS.pressure}>
213
+ Pressure
214
+ </span>
215
+ <span className="config-value">
216
+ {snapshot ? formatPressure(snapshot.pressure) : "\u2014"}
217
+ </span>
218
+ </div>
219
+ <div className="config-item">
220
+ <span className="config-label" title={TOOLTIPS.threshold}>
221
+ Threshold
222
+ </span>
223
+ <span className="config-value">
224
+ {cfg ? formatThreshold(cfg.thresholdTokens, cfg.tierPct, cw) : "\u2014"}
225
+ </span>
226
+ </div>
227
+ <div className="config-item">
228
+ <span className="config-label" title={TOOLTIPS.fastGate}>
229
+ Fast Gate
230
+ </span>
231
+ <span className="config-value">
232
+ {cfg ? `${cfg.fastGatePct}%` : "\u2014"}
233
+ </span>
234
+ </div>
235
+ <div className="config-item">
236
+ <span className="config-label">Auto</span>
237
+ <span className="config-value">
238
+ {cfg ? (cfg.auto ? "enabled" : "disabled") : "\u2014"}
239
+ </span>
240
+ </div>
241
+ <div className="config-item">
242
+ <span className="config-label">Anchor</span>
243
+ <span className="config-value">
244
+ {cfg?.anchorUserMessages ?? "\u2014"}
245
+ </span>
246
+ </div>
247
+ </div>
248
+ </section>
14
249
  </div>
15
250
  );
16
251
  }