@tokentop/ttop 0.3.0 → 0.5.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.
- package/package.json +9 -4
- package/src/agents/aggregator.ts +8 -1
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +2 -2
- package/src/demo/simulator.ts +1 -1
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +17 -13
- package/src/plugins/loader.test.ts +1 -1
- package/src/plugins/loader.ts +15 -7
- package/src/plugins/notification-bus.ts +5 -2
- package/src/plugins/notifications/terminal-bell.ts +9 -2
- package/src/plugins/notifications/visual-flash.ts +13 -3
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/providers/anthropic.ts +2 -2
- package/src/plugins/providers/antigravity.ts +1 -1
- package/src/plugins/providers/chutes.ts +189 -0
- package/src/plugins/providers/codex.ts +1 -1
- package/src/plugins/providers/gemini.ts +1 -1
- package/src/plugins/providers/github-copilot.ts +1 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +1 -1
- package/src/plugins/providers/openai-api.ts +1 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +1 -1
- package/src/plugins/providers/zai.ts +4 -2
- package/src/plugins/registry.ts +51 -46
- package/src/plugins/sandbox-guard.test.ts +1 -1
- package/src/plugins/sandbox-guard.ts +1 -1
- package/src/plugins/sandbox.ts +1 -1
- package/src/plugins/themes/catppuccin-latte.ts +1 -1
- package/src/plugins/themes/catppuccin-mocha.ts +1 -1
- package/src/plugins/themes/claude-code.ts +1 -1
- package/src/plugins/themes/dracula.ts +1 -1
- package/src/plugins/themes/github-light.ts +1 -1
- package/src/plugins/themes/gruvbox-dark.ts +1 -1
- package/src/plugins/themes/gruvbox-light.ts +1 -1
- package/src/plugins/themes/kanagawa.ts +1 -1
- package/src/plugins/themes/nord.ts +1 -1
- package/src/plugins/themes/one-dark.ts +1 -1
- package/src/plugins/themes/opencode.ts +1 -1
- package/src/plugins/themes/rose-pine-dawn.ts +1 -1
- package/src/plugins/themes/rose-pine.ts +1 -1
- package/src/plugins/themes/solarized-light.ts +1 -1
- package/src/plugins/themes/tokyo-night.ts +1 -1
- package/src/plugins/update-checker.ts +2 -2
- package/src/pricing/estimator.test.ts +1 -1
- package/src/pricing/estimator.ts +1 -1
- package/src/pricing/fallback.ts +1 -1
- package/src/pricing/index.ts +1 -1
- package/src/pricing/models-dev.ts +1 -1
- package/src/storage/index.ts +6 -0
- package/src/storage/migrations/index.ts +17 -0
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/storage/retention.ts +146 -0
- package/src/tui/App.tsx +1 -1
- package/src/tui/components/DebugPanel.tsx +23 -10
- package/src/tui/components/ProviderCard.tsx +1 -1
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +15 -4
- package/src/tui/components/SettingsModal.tsx +383 -76
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +44 -80
- package/src/tui/contexts/PluginContext.tsx +13 -3
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/StorageContext.tsx +42 -130
- package/src/tui/contexts/ThemeContext.tsx +3 -3
- package/src/tui/hooks/useExitAnimation.ts +4 -0
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/views/Dashboard.tsx +40 -3
- package/src/tui/views/ProjectsView.tsx +29 -6
- package/src/tui/views/RealTimeDashboard.tsx +29 -8
- package/src/tui/views/SettingsView.tsx +9 -4
- package/src/version.ts +1 -1
- package/src/plugins/types/agent.ts +0 -108
- package/src/plugins/types/base.ts +0 -278
- package/src/plugins/types/index.ts +0 -19
- package/src/plugins/types/notification.ts +0 -51
- package/src/plugins/types/provider.ts +0 -237
- package/src/plugins/types/theme.ts +0 -78
- package/src/storage/database.ts +0 -623
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import type { ConfigField, PluginType } from "@tokentop/plugin-sdk";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
4
|
import type { AppConfig, SparklineOrientation, SparklineStyle } from "@/config/schema.ts";
|
|
4
5
|
import { formatBudgetDisplay, parseCurrencyInput } from "@/utils/currency.ts";
|
|
@@ -11,12 +12,14 @@ import { useToastContext } from "../contexts/ToastContext.tsx";
|
|
|
11
12
|
import { ModalBackdrop, Z_INDEX } from "./ModalBackdrop.tsx";
|
|
12
13
|
import { ThemePicker } from "./ThemePicker.tsx";
|
|
13
14
|
|
|
14
|
-
type SettingCategory = "refresh" | "display" | "budgets" | "alerts" | "notifications";
|
|
15
|
+
type SettingCategory = "refresh" | "display" | "budgets" | "alerts" | "notifications" | "plugins";
|
|
15
16
|
|
|
16
17
|
interface SettingItem {
|
|
17
18
|
key: string;
|
|
18
19
|
label: string;
|
|
20
|
+
description?: string;
|
|
19
21
|
category: SettingCategory;
|
|
22
|
+
pluginId?: string;
|
|
20
23
|
type: "toggle" | "select" | "number";
|
|
21
24
|
options?: string[];
|
|
22
25
|
getValue: (config: AppConfig) => string | number | boolean | null;
|
|
@@ -210,8 +213,88 @@ const CATEGORIES: { id: SettingCategory; label: string }[] = [
|
|
|
210
213
|
{ id: "budgets", label: "Budgets" },
|
|
211
214
|
{ id: "alerts", label: "Alerts" },
|
|
212
215
|
{ id: "notifications", label: "Notifications" },
|
|
216
|
+
{ id: "plugins", label: "Plugins" },
|
|
213
217
|
];
|
|
214
218
|
|
|
219
|
+
function buildPluginSettings(
|
|
220
|
+
plugins: Array<{ id: string; name: string; configSchema?: Record<string, ConfigField> }>,
|
|
221
|
+
): SettingItem[] {
|
|
222
|
+
const items: SettingItem[] = [];
|
|
223
|
+
for (const plugin of plugins) {
|
|
224
|
+
if (!plugin.configSchema) continue;
|
|
225
|
+
for (const [fieldKey, field] of Object.entries(plugin.configSchema)) {
|
|
226
|
+
const settingKey = `${plugin.id}.${fieldKey}`;
|
|
227
|
+
// Short label for the setting row; long description shown in help area
|
|
228
|
+
const label = field.label ?? fieldKey;
|
|
229
|
+
const description = field.description;
|
|
230
|
+
|
|
231
|
+
const getPluginValue = (c: AppConfig): unknown => {
|
|
232
|
+
const pluginCfg = c.pluginConfig[plugin.id];
|
|
233
|
+
const raw = pluginCfg?.[fieldKey];
|
|
234
|
+
return raw ?? field.default;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const setPluginValue = (c: AppConfig, value: unknown): AppConfig => ({
|
|
238
|
+
...c,
|
|
239
|
+
pluginConfig: {
|
|
240
|
+
...c.pluginConfig,
|
|
241
|
+
[plugin.id]: {
|
|
242
|
+
...(c.pluginConfig[plugin.id] ?? {}),
|
|
243
|
+
[fieldKey]: value,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const baseItem: Omit<SettingItem, "type" | "getValue" | "setValue"> = {
|
|
249
|
+
key: settingKey,
|
|
250
|
+
label,
|
|
251
|
+
...(description != null ? { description } : {}),
|
|
252
|
+
category: "plugins" as SettingCategory,
|
|
253
|
+
pluginId: plugin.id,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (field.type === "boolean") {
|
|
257
|
+
items.push({
|
|
258
|
+
...baseItem,
|
|
259
|
+
type: "toggle",
|
|
260
|
+
getValue: (c) => getPluginValue(c) as boolean,
|
|
261
|
+
setValue: (c, v) => setPluginValue(c, v),
|
|
262
|
+
});
|
|
263
|
+
} else if (field.type === "select" && field.options) {
|
|
264
|
+
items.push({
|
|
265
|
+
...baseItem,
|
|
266
|
+
type: "select",
|
|
267
|
+
options: field.options.map((o) => o.label),
|
|
268
|
+
getValue: (c) => {
|
|
269
|
+
const val = getPluginValue(c) as string;
|
|
270
|
+
const opt = field.options?.find((o) => o.value === val);
|
|
271
|
+
return opt?.label ?? val ?? "";
|
|
272
|
+
},
|
|
273
|
+
setValue: (c, v) => {
|
|
274
|
+
const opt = field.options?.find((o) => o.label === v);
|
|
275
|
+
return setPluginValue(c, opt?.value ?? v);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
} else if (field.type === "number") {
|
|
279
|
+
items.push({
|
|
280
|
+
...baseItem,
|
|
281
|
+
type: "number",
|
|
282
|
+
getValue: (c) => (getPluginValue(c) as number) ?? 0,
|
|
283
|
+
setValue: (c, v) => setPluginValue(c, v),
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
items.push({
|
|
287
|
+
...baseItem,
|
|
288
|
+
type: "select",
|
|
289
|
+
getValue: (c) => String(getPluginValue(c) ?? ""),
|
|
290
|
+
setValue: (c, v) => setPluginValue(c, v),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return items;
|
|
296
|
+
}
|
|
297
|
+
|
|
215
298
|
interface SettingsModalProps {
|
|
216
299
|
onClose: () => void;
|
|
217
300
|
}
|
|
@@ -219,7 +302,7 @@ interface SettingsModalProps {
|
|
|
219
302
|
export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
220
303
|
const colors = useColors();
|
|
221
304
|
const { setPreviewTheme } = useTheme();
|
|
222
|
-
const { themes } = usePlugins();
|
|
305
|
+
const { providers, agents, themes, notifications } = usePlugins();
|
|
223
306
|
const { showToast } = useToastContext();
|
|
224
307
|
const { config, updateConfig, resetToDefaults, saveNow } = useConfig();
|
|
225
308
|
const { demoMode, seed, preset } = useDemoMode();
|
|
@@ -228,6 +311,8 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
228
311
|
|
|
229
312
|
const [selectedCategory, setSelectedCategory] = useState<SettingCategory>("refresh");
|
|
230
313
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
314
|
+
const [selectedPluginId, setSelectedPluginId] = useState<string | null>(null);
|
|
315
|
+
const [isPluginsExpanded, setIsPluginsExpanded] = useState(false);
|
|
231
316
|
const [focusedPane, setFocusedPane] = useState<"categories" | "settings">("categories");
|
|
232
317
|
const [showThemePicker, setShowThemePicker] = useState(false);
|
|
233
318
|
|
|
@@ -238,14 +323,82 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
238
323
|
const height = Math.min(termHeight - 4, 28);
|
|
239
324
|
|
|
240
325
|
// Calculate available height for settings list
|
|
241
|
-
// Modal height - header(1) - footer(1) - outer padding(2) - inner borders(4) - title row with margin(2)
|
|
242
|
-
const
|
|
326
|
+
// Modal height - header(1) - footer(1) - outer padding(2) - inner borders(4) - title row with margin(2) - help area(4)
|
|
327
|
+
const helpAreaHeight = 4; // separator(1) + 3 lines of description text
|
|
328
|
+
const settingsAreaHeight = height - 1 - 1 - 2 - 4 - 2 - helpAreaHeight - (demoMode ? 1 : 0);
|
|
243
329
|
// Each setting takes 2 rows (height=1 + marginBottom=1)
|
|
244
330
|
const visibleSettingsCount = Math.max(1, Math.floor(settingsAreaHeight / 2));
|
|
245
331
|
|
|
246
|
-
const
|
|
332
|
+
const pluginsByType = useMemo(() => {
|
|
333
|
+
const allPlugins = [
|
|
334
|
+
...Array.from(providers.values()).map((p) => p.plugin),
|
|
335
|
+
...agents,
|
|
336
|
+
...themes,
|
|
337
|
+
...notifications,
|
|
338
|
+
].filter((p) => p.configSchema && Object.keys(p.configSchema).length > 0);
|
|
339
|
+
|
|
340
|
+
const grouped = new Map<PluginType, typeof allPlugins>();
|
|
341
|
+
for (const p of allPlugins) {
|
|
342
|
+
const list = grouped.get(p.type) ?? [];
|
|
343
|
+
list.push(p);
|
|
344
|
+
grouped.set(p.type, list);
|
|
345
|
+
}
|
|
346
|
+
// Sort plugins within each group
|
|
347
|
+
for (const list of grouped.values()) {
|
|
348
|
+
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
349
|
+
}
|
|
350
|
+
return grouped;
|
|
351
|
+
}, [providers, agents, themes, notifications]);
|
|
352
|
+
|
|
353
|
+
const availablePlugins = useMemo(() => {
|
|
354
|
+
return [...pluginsByType.values()].flat();
|
|
355
|
+
}, [pluginsByType]);
|
|
356
|
+
|
|
357
|
+
const pluginSettings = useMemo(() => {
|
|
358
|
+
return buildPluginSettings(availablePlugins);
|
|
359
|
+
}, [availablePlugins]);
|
|
360
|
+
|
|
361
|
+
const TYPE_LABELS: Record<PluginType, string> = {
|
|
362
|
+
provider: "Providers",
|
|
363
|
+
agent: "Agents",
|
|
364
|
+
theme: "Themes",
|
|
365
|
+
notification: "Delivery",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Stable ordering for plugin type groups in sidebar
|
|
369
|
+
const TYPE_ORDER: PluginType[] = ["provider", "agent", "theme", "notification"];
|
|
370
|
+
|
|
371
|
+
type NavItem =
|
|
372
|
+
| { type: "category"; id: SettingCategory; label: string }
|
|
373
|
+
| { type: "pluginTypeHeader"; pluginType: PluginType; label: string }
|
|
374
|
+
| { type: "plugin"; id: string; label: string };
|
|
375
|
+
|
|
376
|
+
const navItems = useMemo<NavItem[]>(() => {
|
|
377
|
+
const items: NavItem[] = [];
|
|
378
|
+
for (const cat of CATEGORIES) {
|
|
379
|
+
items.push({ type: "category", id: cat.id, label: cat.label });
|
|
380
|
+
if (cat.id === "plugins" && isPluginsExpanded) {
|
|
381
|
+
for (const pt of TYPE_ORDER) {
|
|
382
|
+
const pluginsOfType = pluginsByType.get(pt);
|
|
383
|
+
if (!pluginsOfType || pluginsOfType.length === 0) continue;
|
|
384
|
+
items.push({ type: "pluginTypeHeader", pluginType: pt, label: TYPE_LABELS[pt] });
|
|
385
|
+
for (const p of pluginsOfType) {
|
|
386
|
+
items.push({ type: "plugin", id: p.id, label: p.name });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return items;
|
|
392
|
+
}, [isPluginsExpanded, pluginsByType]);
|
|
393
|
+
|
|
394
|
+
const settings = useMemo(() => [...BASE_SETTINGS, ...pluginSettings], [pluginSettings]);
|
|
247
395
|
|
|
248
|
-
const categorySettings =
|
|
396
|
+
const categorySettings = useMemo(() => {
|
|
397
|
+
if (selectedCategory === "plugins" && selectedPluginId) {
|
|
398
|
+
return settings.filter((s) => s.category === "plugins" && s.pluginId === selectedPluginId);
|
|
399
|
+
}
|
|
400
|
+
return settings.filter((s) => s.category === selectedCategory && !s.pluginId);
|
|
401
|
+
}, [settings, selectedCategory, selectedPluginId]);
|
|
249
402
|
|
|
250
403
|
// Calculate scroll offset to keep selected item visible
|
|
251
404
|
const scrollOffset = useMemo(() => {
|
|
@@ -258,6 +411,19 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
258
411
|
const visibleSettings = categorySettings.slice(scrollOffset, scrollOffset + visibleSettingsCount);
|
|
259
412
|
const hasMoreAbove = scrollOffset > 0;
|
|
260
413
|
const hasMoreBelow = scrollOffset + visibleSettingsCount < categorySettings.length;
|
|
414
|
+
const focusedDescription = useMemo(() => {
|
|
415
|
+
if (focusedPane !== "settings") return null;
|
|
416
|
+
const setting = categorySettings[selectedIndex];
|
|
417
|
+
const desc = setting?.description ?? null;
|
|
418
|
+
if (!desc) return null;
|
|
419
|
+
|
|
420
|
+
// Truncate to fit 3 lines with ellipsis if needed
|
|
421
|
+
// Help area usable width: total width - sidebar(20) - borders(2) - outer padding(2) - help paddingX(2) - inner padding(2)
|
|
422
|
+
const lineWidth = Math.max(20, width - 20 - 2 - 2 - 2 - 2);
|
|
423
|
+
const maxChars = lineWidth * 3;
|
|
424
|
+
if (desc.length <= maxChars) return desc;
|
|
425
|
+
return desc.slice(0, maxChars - 1) + "\u2026";
|
|
426
|
+
}, [focusedPane, categorySettings, selectedIndex, width]);
|
|
261
427
|
|
|
262
428
|
useEffect(() => {
|
|
263
429
|
setInputFocused(true);
|
|
@@ -376,17 +542,74 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
376
542
|
}
|
|
377
543
|
|
|
378
544
|
if (focusedPane === "categories") {
|
|
379
|
-
|
|
545
|
+
// Find current position — skip headers when matching
|
|
546
|
+
const currentIdx = navItems.findIndex((item) =>
|
|
547
|
+
item.type === "category"
|
|
548
|
+
? item.id === selectedCategory && (item.id !== "plugins" || !selectedPluginId)
|
|
549
|
+
: item.type === "plugin"
|
|
550
|
+
? item.id === selectedPluginId
|
|
551
|
+
: false,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// Helper: find next focusable item (skip pluginTypeHeader)
|
|
555
|
+
const findNext = (from: number, dir: 1 | -1): number => {
|
|
556
|
+
let idx = from + dir;
|
|
557
|
+
while (idx >= 0 && idx < navItems.length) {
|
|
558
|
+
if (navItems[idx]!.type !== "pluginTypeHeader") return idx;
|
|
559
|
+
idx += dir;
|
|
560
|
+
}
|
|
561
|
+
return from; // no focusable item found, stay put
|
|
562
|
+
};
|
|
563
|
+
|
|
380
564
|
if (key.name === "down" || key.name === "j") {
|
|
381
|
-
const nextIdx =
|
|
382
|
-
|
|
383
|
-
|
|
565
|
+
const nextIdx = findNext(currentIdx, 1);
|
|
566
|
+
const nextItem = navItems[nextIdx];
|
|
567
|
+
if (nextItem && nextIdx !== currentIdx) {
|
|
568
|
+
if (nextItem.type === "category") {
|
|
569
|
+
setSelectedCategory(nextItem.id);
|
|
570
|
+
setSelectedPluginId(null);
|
|
571
|
+
if (nextItem.id === "plugins") {
|
|
572
|
+
setIsPluginsExpanded(true);
|
|
573
|
+
}
|
|
574
|
+
} else if (nextItem.type === "plugin") {
|
|
575
|
+
setSelectedPluginId(nextItem.id);
|
|
576
|
+
}
|
|
577
|
+
setSelectedIndex(0);
|
|
578
|
+
}
|
|
384
579
|
} else if (key.name === "up" || key.name === "k") {
|
|
385
|
-
const prevIdx =
|
|
386
|
-
|
|
387
|
-
|
|
580
|
+
const prevIdx = findNext(currentIdx, -1);
|
|
581
|
+
const prevItem = navItems[prevIdx];
|
|
582
|
+
if (prevItem && prevIdx !== currentIdx) {
|
|
583
|
+
if (prevItem.type === "category") {
|
|
584
|
+
setSelectedCategory(prevItem.id);
|
|
585
|
+
setSelectedPluginId(null);
|
|
586
|
+
// Collapse plugins if navigating up from first plugin child to Plugins parent
|
|
587
|
+
if (prevItem.id === "plugins" && navItems[currentIdx]?.type === "plugin") {
|
|
588
|
+
setIsPluginsExpanded(false);
|
|
589
|
+
}
|
|
590
|
+
} else if (prevItem.type === "plugin") {
|
|
591
|
+
setSelectedPluginId(prevItem.id);
|
|
592
|
+
}
|
|
593
|
+
setSelectedIndex(0);
|
|
594
|
+
}
|
|
388
595
|
} else if (key.name === "return" || key.name === "right" || key.name === "l") {
|
|
389
|
-
|
|
596
|
+
const currentItem = navItems[currentIdx];
|
|
597
|
+
if (currentItem?.type === "category" && currentItem.id === "plugins") {
|
|
598
|
+
if (!isPluginsExpanded) {
|
|
599
|
+
setIsPluginsExpanded(true);
|
|
600
|
+
} else if (key.name === "return") {
|
|
601
|
+
setIsPluginsExpanded((p) => !p);
|
|
602
|
+
}
|
|
603
|
+
} else if (selectedPluginId || selectedCategory !== "plugins") {
|
|
604
|
+
setFocusedPane("settings");
|
|
605
|
+
}
|
|
606
|
+
} else if (key.name === "left" || key.name === "h") {
|
|
607
|
+
if (selectedPluginId) {
|
|
608
|
+
setSelectedPluginId(null);
|
|
609
|
+
setSelectedIndex(0);
|
|
610
|
+
} else if (selectedCategory === "plugins" && isPluginsExpanded) {
|
|
611
|
+
setIsPluginsExpanded(false);
|
|
612
|
+
}
|
|
390
613
|
}
|
|
391
614
|
return;
|
|
392
615
|
}
|
|
@@ -471,19 +694,67 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
471
694
|
borderColor={focusedPane === "categories" ? colors.primary : colors.border}
|
|
472
695
|
padding={1}
|
|
473
696
|
>
|
|
474
|
-
{
|
|
475
|
-
|
|
697
|
+
{navItems.map((item) => {
|
|
698
|
+
if (item.type === "pluginTypeHeader") {
|
|
699
|
+
// Non-interactive type group header with · prefix
|
|
700
|
+
return (
|
|
701
|
+
<box key={`type-${item.pluginType}`} height={1}>
|
|
702
|
+
<text fg={colors.textSubtle}>{` · ${item.label}`}</text>
|
|
703
|
+
</box>
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const isCategory = item.type === "category";
|
|
708
|
+
const isPlugin = item.type === "plugin";
|
|
709
|
+
const isActive = isCategory
|
|
710
|
+
? item.id === selectedCategory && (!selectedPluginId || item.id !== "plugins")
|
|
711
|
+
: isPlugin
|
|
712
|
+
? item.id === selectedPluginId
|
|
713
|
+
: false;
|
|
476
714
|
const isFocusedActive = isActive && focusedPane === "categories";
|
|
715
|
+
|
|
716
|
+
let prefix = " ";
|
|
717
|
+
if (isCategory) {
|
|
718
|
+
if (item.id === "plugins") {
|
|
719
|
+
prefix = isPluginsExpanded ? "▾ " : isActive ? "▸ " : " ";
|
|
720
|
+
} else {
|
|
721
|
+
prefix = isActive ? "> " : " ";
|
|
722
|
+
}
|
|
723
|
+
} else if (isPlugin) {
|
|
724
|
+
prefix = isActive ? " > " : " ";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Smart truncation: preserve last word when possible
|
|
728
|
+
const maxLabelWidth = 18 - prefix.length;
|
|
729
|
+
let label = item.label;
|
|
730
|
+
if (label.length > maxLabelWidth) {
|
|
731
|
+
const words = label.split(" ");
|
|
732
|
+
if (words.length > 1) {
|
|
733
|
+
const lastWord = words[words.length - 1];
|
|
734
|
+
// Try to fit "X… LastWord" format
|
|
735
|
+
const ellipsisAndLast = `… ${lastWord}`;
|
|
736
|
+
if (ellipsisAndLast.length < maxLabelWidth) {
|
|
737
|
+
const firstPartLen = maxLabelWidth - ellipsisAndLast.length;
|
|
738
|
+
label = label.slice(0, firstPartLen) + ellipsisAndLast;
|
|
739
|
+
} else {
|
|
740
|
+
// Last word alone is too long, just truncate
|
|
741
|
+
label = label.slice(0, maxLabelWidth - 1) + "…";
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
label = label.slice(0, maxLabelWidth - 1) + "…";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
477
748
|
return (
|
|
478
|
-
<box key={
|
|
749
|
+
<box key={`${item.type}-${item.id}`} height={1}>
|
|
479
750
|
<text
|
|
480
751
|
fg={
|
|
481
752
|
isFocusedActive ? colors.background : isActive ? colors.primary : colors.text
|
|
482
753
|
}
|
|
483
754
|
{...(isFocusedActive ? { bg: colors.primary } : {})}
|
|
484
755
|
>
|
|
485
|
-
{
|
|
486
|
-
{
|
|
756
|
+
{prefix}
|
|
757
|
+
{label}
|
|
487
758
|
</text>
|
|
488
759
|
</box>
|
|
489
760
|
);
|
|
@@ -522,68 +793,104 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
522
793
|
}}
|
|
523
794
|
/>
|
|
524
795
|
) : (
|
|
525
|
-
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
796
|
+
<box flexDirection="column" flexGrow={1} padding={1} justifyContent="space-between">
|
|
797
|
+
{/* Settings list area */}
|
|
798
|
+
<box flexDirection="column" flexGrow={1}>
|
|
799
|
+
<box
|
|
800
|
+
flexDirection="row"
|
|
801
|
+
justifyContent="space-between"
|
|
802
|
+
marginBottom={1}
|
|
803
|
+
height={1}
|
|
804
|
+
>
|
|
805
|
+
<text fg={colors.textMuted}>
|
|
806
|
+
{selectedCategory === "plugins" && selectedPluginId
|
|
807
|
+
? availablePlugins
|
|
808
|
+
.find((p) => p.id === selectedPluginId)
|
|
809
|
+
?.name.toUpperCase()
|
|
810
|
+
: CATEGORIES.find((c) => c.id === selectedCategory)?.label.toUpperCase()}
|
|
811
|
+
</text>
|
|
812
|
+
{(hasMoreAbove || hasMoreBelow) && (
|
|
813
|
+
<text fg={colors.textSubtle}>
|
|
814
|
+
{hasMoreAbove ? "▲" : " "}
|
|
815
|
+
{hasMoreBelow ? "▼" : " "}
|
|
816
|
+
</text>
|
|
817
|
+
)}
|
|
818
|
+
</box>
|
|
819
|
+
|
|
820
|
+
{visibleSettings.length === 0 && (
|
|
821
|
+
<text fg={colors.textMuted}>
|
|
822
|
+
{selectedCategory === "plugins" && !selectedPluginId
|
|
823
|
+
? "Select a plugin to configure"
|
|
824
|
+
: "No settings available"}
|
|
534
825
|
</text>
|
|
535
826
|
)}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
827
|
+
{visibleSettings.map((setting) => {
|
|
828
|
+
const realIdx = categorySettings.findIndex((s) => s.key === setting.key);
|
|
829
|
+
const isSelected = realIdx === selectedIndex && focusedPane === "settings";
|
|
830
|
+
const value = setting.getValue(config);
|
|
831
|
+
const isEditingThis =
|
|
832
|
+
setting.type === "number" && editingSettingKey === setting.key;
|
|
833
|
+
|
|
834
|
+
let displayValue: string;
|
|
835
|
+
if (setting.key === "theme") {
|
|
836
|
+
const themeName = themes.find((t) => t.id === value)?.name ?? value;
|
|
837
|
+
displayValue = `${themeName} ▸`;
|
|
838
|
+
} else if (setting.type === "toggle") {
|
|
839
|
+
displayValue = value ? "● ON" : "○ OFF";
|
|
840
|
+
} else if (setting.type === "select") {
|
|
841
|
+
displayValue = `◂ ${value} ▸`;
|
|
842
|
+
} else if (setting.type === "number" && !isEditingThis) {
|
|
843
|
+
displayValue =
|
|
844
|
+
setting.category === "budgets"
|
|
845
|
+
? formatBudgetDisplay(value as number | null)
|
|
846
|
+
: value === null
|
|
847
|
+
? "—"
|
|
848
|
+
: String(value);
|
|
849
|
+
} else {
|
|
850
|
+
displayValue = String(value);
|
|
851
|
+
}
|
|
558
852
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
flexGrow={1}
|
|
570
|
-
fg={isSelected && !isEditingThis ? colors.background : colors.text}
|
|
853
|
+
return (
|
|
854
|
+
<box
|
|
855
|
+
key={setting.key}
|
|
856
|
+
flexDirection="row"
|
|
857
|
+
height={1}
|
|
858
|
+
marginBottom={1}
|
|
859
|
+
paddingX={1}
|
|
860
|
+
{...(isSelected && !isEditingThis
|
|
861
|
+
? { backgroundColor: colors.primary }
|
|
862
|
+
: {})}
|
|
571
863
|
>
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
<span fg={colors.primary}>█</span>
|
|
578
|
-
</text>
|
|
579
|
-
) : (
|
|
580
|
-
<text fg={isSelected ? colors.background : colors.textMuted}>
|
|
581
|
-
{displayValue}
|
|
864
|
+
<text
|
|
865
|
+
flexGrow={1}
|
|
866
|
+
fg={isSelected && !isEditingThis ? colors.background : colors.text}
|
|
867
|
+
>
|
|
868
|
+
{setting.label}
|
|
582
869
|
</text>
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
870
|
+
{isEditingThis ? (
|
|
871
|
+
<text fg={colors.text}>
|
|
872
|
+
${inputValue}
|
|
873
|
+
<span fg={colors.primary}>█</span>
|
|
874
|
+
</text>
|
|
875
|
+
) : (
|
|
876
|
+
<text fg={isSelected ? colors.background : colors.textMuted}>
|
|
877
|
+
{displayValue}
|
|
878
|
+
</text>
|
|
879
|
+
)}
|
|
880
|
+
</box>
|
|
881
|
+
);
|
|
882
|
+
})}
|
|
883
|
+
</box>
|
|
884
|
+
|
|
885
|
+
{/* Help area — shows focused setting's description */}
|
|
886
|
+
<box height={helpAreaHeight} flexDirection="column" paddingX={1}>
|
|
887
|
+
<box height={1}>
|
|
888
|
+
<text fg={colors.border}>{"─".repeat(40)}</text>
|
|
889
|
+
</box>
|
|
890
|
+
<text fg={colors.textSubtle} height={3}>
|
|
891
|
+
{focusedDescription ?? " "}
|
|
892
|
+
</text>
|
|
893
|
+
</box>
|
|
587
894
|
</box>
|
|
588
895
|
)}
|
|
589
896
|
</box>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import type { ColorSchemePreference, ThemePlugin } from "@tokentop/plugin-sdk";
|
|
2
3
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import type { ColorSchemePreference, ThemePlugin } from "@/plugins/types/theme.ts";
|
|
4
4
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
5
5
|
|
|
6
6
|
interface ThemePickerProps {
|