claudeup 4.6.1 → 4.8.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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/gap-fill-versions.test.ts +382 -0
  3. package/src/data/settings-catalog.js +2 -7
  4. package/src/data/settings-catalog.ts +2 -7
  5. package/src/opentui.d.ts +7 -2
  6. package/src/prerunner/index.js +31 -17
  7. package/src/prerunner/index.ts +35 -18
  8. package/src/services/claude-settings.js +74 -0
  9. package/src/services/claude-settings.ts +92 -0
  10. package/src/services/plugin-manager.js +13 -16
  11. package/src/services/plugin-manager.ts +17 -16
  12. package/src/services/settings-manager.js +84 -5
  13. package/src/services/settings-manager.ts +86 -5
  14. package/src/ui/adapters/settingsAdapter.js +8 -8
  15. package/src/ui/adapters/settingsAdapter.ts +8 -8
  16. package/src/ui/components/TabBar.js +1 -23
  17. package/src/ui/components/TabBar.tsx +1 -26
  18. package/src/ui/components/modals/ConfirmModal.js +1 -1
  19. package/src/ui/components/modals/ConfirmModal.tsx +17 -16
  20. package/src/ui/components/modals/InputModal.js +2 -13
  21. package/src/ui/components/modals/InputModal.tsx +21 -24
  22. package/src/ui/components/modals/LoadingModal.js +1 -1
  23. package/src/ui/components/modals/LoadingModal.tsx +6 -6
  24. package/src/ui/components/modals/MessageModal.js +4 -4
  25. package/src/ui/components/modals/MessageModal.tsx +13 -13
  26. package/src/ui/components/modals/ModalContainer.js +25 -2
  27. package/src/ui/components/modals/ModalContainer.tsx +25 -2
  28. package/src/ui/components/modals/SelectModal.js +3 -4
  29. package/src/ui/components/modals/SelectModal.tsx +18 -15
  30. package/src/ui/renderers/settingsRenderers.js +1 -1
  31. package/src/ui/renderers/settingsRenderers.tsx +5 -3
  32. package/src/ui/screens/CliToolsScreen.js +2 -2
  33. package/src/ui/screens/CliToolsScreen.tsx +3 -1
  34. package/src/ui/screens/EnvVarsScreen.js +27 -10
  35. package/src/ui/screens/EnvVarsScreen.tsx +33 -16
  36. package/src/ui/screens/McpRegistryScreen.js +2 -2
  37. package/src/ui/screens/McpRegistryScreen.tsx +3 -1
  38. package/src/ui/screens/McpScreen.js +1 -1
  39. package/src/ui/screens/McpScreen.tsx +2 -1
  40. package/src/ui/screens/ModelSelectorScreen.js +2 -2
  41. package/src/ui/screens/ModelSelectorScreen.tsx +3 -2
  42. package/src/ui/screens/ProfilesScreen.js +1 -1
  43. package/src/ui/screens/ProfilesScreen.tsx +2 -1
  44. package/src/ui/screens/StatusLineScreen.js +1 -1
  45. package/src/ui/screens/StatusLineScreen.tsx +2 -1
  46. package/src/ui/state/DimensionsContext.js +2 -2
  47. package/src/ui/state/DimensionsContext.tsx +3 -3
  48. package/src/ui/components/ScrollableDetail.js +0 -23
  49. package/src/ui/components/ScrollableDetail.tsx +0 -55
@@ -1,6 +1,7 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import semver from "semver";
4
5
  import { parsePluginId } from "../utils/string-utils.js";
5
6
  const CLAUDE_DIR = ".claude";
6
7
  const SETTINGS_FILE = "settings.json";
@@ -377,6 +378,79 @@ export async function saveGlobalInstalledPluginVersion(pluginId, version) {
377
378
  // Update registry for user scope
378
379
  await updateInstalledPluginsRegistry(pluginId, version, "user");
379
380
  }
381
+ /**
382
+ * Gap-fill: ensure global installedPluginVersions is at least as high as
383
+ * project and local scopes. When a plugin is updated at project scope,
384
+ * global may lag behind, causing Claude Code to resolve stale cache paths.
385
+ *
386
+ * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
387
+ */
388
+ export async function gapFillInstalledPluginVersions(projectPath) {
389
+ const globalVersions = await getGlobalInstalledPluginVersions();
390
+ // Collect versions from project and local scopes
391
+ const allVersions = {};
392
+ // Start with global versions
393
+ for (const [id, ver] of Object.entries(globalVersions)) {
394
+ allVersions[id] = [ver];
395
+ }
396
+ // Add project scope versions
397
+ if (projectPath) {
398
+ try {
399
+ const settings = await readSettings(projectPath);
400
+ for (const [id, ver] of Object.entries(settings.installedPluginVersions || {})) {
401
+ if (!allVersions[id])
402
+ allVersions[id] = [];
403
+ allVersions[id].push(ver);
404
+ }
405
+ }
406
+ catch {
407
+ /* skip unreadable */
408
+ }
409
+ // Add local scope versions
410
+ try {
411
+ const localSettings = await readLocalSettings(projectPath);
412
+ for (const [id, ver] of Object.entries(localSettings.installedPluginVersions || {})) {
413
+ if (!allVersions[id])
414
+ allVersions[id] = [];
415
+ allVersions[id].push(ver);
416
+ }
417
+ }
418
+ catch {
419
+ /* skip unreadable */
420
+ }
421
+ }
422
+ // Find plugins where global needs bumping
423
+ const bumped = [];
424
+ for (const [pluginId, versions] of Object.entries(allVersions)) {
425
+ // Filter to valid semver versions and sort descending
426
+ const valid = versions.filter((v) => semver.valid(v));
427
+ if (valid.length === 0)
428
+ continue;
429
+ const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
430
+ const globalVer = globalVersions[pluginId];
431
+ // If global is missing, invalid semver, or lower than the highest, bump it
432
+ if (!globalVer ||
433
+ !semver.valid(globalVer) ||
434
+ semver.lt(globalVer, highest)) {
435
+ bumped.push({
436
+ pluginId,
437
+ oldVersion: globalVer || "(missing)",
438
+ newVersion: highest,
439
+ });
440
+ }
441
+ }
442
+ // Apply bumps in a single write
443
+ if (bumped.length > 0) {
444
+ const settings = await readGlobalSettings();
445
+ settings.installedPluginVersions =
446
+ settings.installedPluginVersions || {};
447
+ for (const { pluginId, newVersion } of bumped) {
448
+ settings.installedPluginVersions[pluginId] = newVersion;
449
+ }
450
+ await writeGlobalSettings(settings);
451
+ }
452
+ return bumped;
453
+ }
380
454
  export async function removeGlobalInstalledPluginVersion(pluginId) {
381
455
  const settings = await readGlobalSettings();
382
456
  if (settings.installedPluginVersions) {
@@ -1,6 +1,7 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import semver from "semver";
4
5
  import type {
5
6
  ClaudeSettings,
6
7
  ClaudeLocalSettings,
@@ -544,6 +545,97 @@ export async function saveGlobalInstalledPluginVersion(
544
545
  await updateInstalledPluginsRegistry(pluginId, version, "user");
545
546
  }
546
547
 
548
+ /**
549
+ * Gap-fill: ensure global installedPluginVersions is at least as high as
550
+ * project and local scopes. When a plugin is updated at project scope,
551
+ * global may lag behind, causing Claude Code to resolve stale cache paths.
552
+ *
553
+ * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
554
+ */
555
+ export async function gapFillInstalledPluginVersions(
556
+ projectPath?: string,
557
+ ): Promise<Array<{ pluginId: string; oldVersion: string; newVersion: string }>> {
558
+ const globalVersions = await getGlobalInstalledPluginVersions();
559
+
560
+ // Collect versions from project and local scopes
561
+ const allVersions: Record<string, string[]> = {};
562
+
563
+ // Start with global versions
564
+ for (const [id, ver] of Object.entries(globalVersions)) {
565
+ allVersions[id] = [ver];
566
+ }
567
+
568
+ // Add project scope versions
569
+ if (projectPath) {
570
+ try {
571
+ const settings = await readSettings(projectPath);
572
+ for (const [id, ver] of Object.entries(
573
+ settings.installedPluginVersions || {},
574
+ )) {
575
+ if (!allVersions[id]) allVersions[id] = [];
576
+ allVersions[id].push(ver);
577
+ }
578
+ } catch {
579
+ /* skip unreadable */
580
+ }
581
+
582
+ // Add local scope versions
583
+ try {
584
+ const localSettings = await readLocalSettings(projectPath);
585
+ for (const [id, ver] of Object.entries(
586
+ localSettings.installedPluginVersions || {},
587
+ )) {
588
+ if (!allVersions[id]) allVersions[id] = [];
589
+ allVersions[id].push(ver);
590
+ }
591
+ } catch {
592
+ /* skip unreadable */
593
+ }
594
+ }
595
+
596
+ // Find plugins where global needs bumping
597
+ const bumped: Array<{
598
+ pluginId: string;
599
+ oldVersion: string;
600
+ newVersion: string;
601
+ }> = [];
602
+
603
+ for (const [pluginId, versions] of Object.entries(allVersions)) {
604
+ // Filter to valid semver versions and sort descending
605
+ const valid = versions.filter((v) => semver.valid(v));
606
+ if (valid.length === 0) continue;
607
+
608
+ const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
609
+ const globalVer = globalVersions[pluginId];
610
+
611
+ // If global is missing, invalid semver, or lower than the highest, bump it
612
+ if (
613
+ !globalVer ||
614
+ !semver.valid(globalVer) ||
615
+ semver.lt(globalVer, highest)
616
+ ) {
617
+ bumped.push({
618
+ pluginId,
619
+ oldVersion: globalVer || "(missing)",
620
+ newVersion: highest,
621
+ });
622
+ }
623
+ }
624
+
625
+ // Apply bumps in a single write
626
+ if (bumped.length > 0) {
627
+ const settings = await readGlobalSettings();
628
+ settings.installedPluginVersions =
629
+ settings.installedPluginVersions || {};
630
+ for (const { pluginId, newVersion } of bumped) {
631
+ settings.installedPluginVersions[pluginId] = newVersion;
632
+ }
633
+ await writeGlobalSettings(settings);
634
+ }
635
+
636
+ return bumped;
637
+ }
638
+
547
639
  export async function removeGlobalInstalledPluginVersion(
548
640
  pluginId: string,
549
641
  ): Promise<void> {
@@ -5,7 +5,6 @@ import { getConfiguredMarketplaces, getEnabledPlugins, readSettings, writeSettin
5
5
  import { defaultMarketplaces } from "../data/marketplaces.js";
6
6
  import { scanLocalMarketplaces, repairAllMarketplaces, } from "./local-marketplace.js";
7
7
  import { formatMarketplaceName, isValidGitHubRepo, parsePluginId, } from "../utils/string-utils.js";
8
- import { updateMarketplace } from "./claude-cli.js";
9
8
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
10
9
  let localMarketplacesPromise = null;
11
10
  // Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
@@ -434,9 +433,14 @@ export async function getLocalMarketplacesInfo() {
434
433
  const autoSyncedMarketplaces = new Set();
435
434
  /**
436
435
  * If the remote manifest lists plugins that aren't in the local cache,
437
- * the local clone is stale. Silently run `claude plugin marketplace update`
438
- * to pull the latest, then invalidate the local cache so the next scan
439
- * picks up the new plugins.
436
+ * the local clone is stale. Log a warning but do NOT trigger a marketplace
437
+ * update from claudeup.
438
+ *
439
+ * Previously this called `updateMarketplace()` which invokes Claude Code's
440
+ * `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
441
+ * permanently delete the marketplace directory on clone failure.
442
+ * Claude Code's own background autoupdate handles marketplace refreshing
443
+ * after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
440
444
  */
441
445
  async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
442
446
  if (autoSyncedMarketplaces.has(mpName))
@@ -445,20 +449,13 @@ async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
445
449
  if (!localMp)
446
450
  return localMarketplaces;
447
451
  const localNames = new Set(localMp.plugins.map((p) => p.name));
448
- const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
449
- if (!hasMissing)
452
+ const missingPlugins = remotePluginNames.filter((name) => !localNames.has(name));
453
+ if (missingPlugins.length === 0)
450
454
  return localMarketplaces;
451
455
  autoSyncedMarketplaces.add(mpName);
452
- try {
453
- await updateMarketplace(mpName);
454
- // Invalidate local cache so re-scan picks up new plugins
455
- localMarketplacesPromise = null;
456
- return getLocalMarketplaces();
457
- }
458
- catch {
459
- // Update failed (no network, CLI missing, etc.) — continue with stale data
460
- return localMarketplaces;
461
- }
456
+ // Log stale state but don't trigger update — Claude Code handles refresh
457
+ console.log(`ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`);
458
+ return localMarketplaces;
462
459
  }
463
460
  /**
464
461
  * Refresh claudeup's internal cache
@@ -27,8 +27,6 @@ import {
27
27
  isValidGitHubRepo,
28
28
  parsePluginId,
29
29
  } from "../utils/string-utils.js";
30
- import { updateMarketplace } from "./claude-cli.js";
31
-
32
30
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
33
31
  let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
34
32
  null;
@@ -619,9 +617,14 @@ const autoSyncedMarketplaces = new Set<string>();
619
617
 
620
618
  /**
621
619
  * If the remote manifest lists plugins that aren't in the local cache,
622
- * the local clone is stale. Silently run `claude plugin marketplace update`
623
- * to pull the latest, then invalidate the local cache so the next scan
624
- * picks up the new plugins.
620
+ * the local clone is stale. Log a warning but do NOT trigger a marketplace
621
+ * update from claudeup.
622
+ *
623
+ * Previously this called `updateMarketplace()` which invokes Claude Code's
624
+ * `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
625
+ * permanently delete the marketplace directory on clone failure.
626
+ * Claude Code's own background autoupdate handles marketplace refreshing
627
+ * after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
625
628
  */
626
629
  async function autoSyncIfStale(
627
630
  mpName: string,
@@ -634,19 +637,17 @@ async function autoSyncIfStale(
634
637
  if (!localMp) return localMarketplaces;
635
638
 
636
639
  const localNames = new Set(localMp.plugins.map((p) => p.name));
637
- const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
638
- if (!hasMissing) return localMarketplaces;
640
+ const missingPlugins = remotePluginNames.filter(
641
+ (name) => !localNames.has(name),
642
+ );
643
+ if (missingPlugins.length === 0) return localMarketplaces;
639
644
 
640
645
  autoSyncedMarketplaces.add(mpName);
641
- try {
642
- await updateMarketplace(mpName);
643
- // Invalidate local cache so re-scan picks up new plugins
644
- localMarketplacesPromise = null;
645
- return getLocalMarketplaces();
646
- } catch {
647
- // Update failed (no network, CLI missing, etc.) — continue with stale data
648
- return localMarketplaces;
649
- }
646
+ // Log stale state but don't trigger update — Claude Code handles refresh
647
+ console.log(
648
+ `ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`,
649
+ );
650
+ return localMarketplaces;
650
651
  }
651
652
 
652
653
  export interface RefreshAndRepairResult {
@@ -1,3 +1,5 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } from "./claude-settings.js";
2
4
  /** Read the current value of a setting from the given scope */
3
5
  export async function readSettingValue(setting, scope, projectPath) {
@@ -62,11 +64,6 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
62
64
  }
63
65
  }
64
66
  else if (setting.storage.type === "attribution-text") {
65
- const attr = settings.attribution;
66
- // If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
67
- if (attr && attr.commit === "" && attr.pr === "") {
68
- return;
69
- }
70
67
  if (value && value.trim().length > 0) {
71
68
  // Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
72
69
  settings.attribution = {
@@ -162,3 +159,85 @@ export async function readAllSettingsBothScopes(catalog, projectPath) {
162
159
  }
163
160
  return result;
164
161
  }
162
+ /**
163
+ * Discover available output styles from installed plugins.
164
+ * Scans enabled plugins for outputStyles entries and *-output-style plugin names.
165
+ * Returns options suitable for a select setting.
166
+ */
167
+ export async function discoverOutputStyles(projectPath) {
168
+ const styles = [
169
+ { label: "Default", value: "" },
170
+ ];
171
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
172
+ const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
173
+ // Collect enabled plugins from both scopes
174
+ const enabledPlugins = new Set();
175
+ try {
176
+ const userSettings = await readGlobalSettings();
177
+ for (const [k, v] of Object.entries(userSettings.enabledPlugins || {})) {
178
+ if (v)
179
+ enabledPlugins.add(k);
180
+ }
181
+ }
182
+ catch { }
183
+ try {
184
+ const projSettings = await readSettings(projectPath);
185
+ for (const [k, v] of Object.entries(projSettings.enabledPlugins || {})) {
186
+ if (v)
187
+ enabledPlugins.add(k);
188
+ }
189
+ }
190
+ catch { }
191
+ const seen = new Set();
192
+ for (const pluginId of enabledPlugins) {
193
+ const [pluginName, marketplace] = pluginId.split("@");
194
+ // Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
195
+ if (pluginName.endsWith("-output-style")) {
196
+ const styleName = pluginName
197
+ .replace(/-output-style$/, "")
198
+ .split("-")
199
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
200
+ .join(" ");
201
+ if (!seen.has(styleName)) {
202
+ seen.add(styleName);
203
+ styles.push({ label: styleName, value: styleName });
204
+ }
205
+ continue;
206
+ }
207
+ // Check plugin.json for outputStyles entries
208
+ if (!marketplace)
209
+ continue;
210
+ const pluginDir = path.join(cacheDir, marketplace, pluginName);
211
+ try {
212
+ const versions = fs.readdirSync(pluginDir).sort();
213
+ const latest = versions[versions.length - 1];
214
+ if (!latest)
215
+ continue;
216
+ const manifestPath = path.join(pluginDir, latest, "plugin.json");
217
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
218
+ const outputStyles = manifest.outputStyles;
219
+ if (!outputStyles?.length)
220
+ continue;
221
+ for (const stylePath of outputStyles) {
222
+ const fullPath = path.join(pluginDir, latest, stylePath);
223
+ try {
224
+ const content = fs.readFileSync(fullPath, "utf-8");
225
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
226
+ if (fmMatch) {
227
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
228
+ if (nameMatch) {
229
+ const name = nameMatch[1].trim();
230
+ if (!seen.has(name)) {
231
+ seen.add(name);
232
+ styles.push({ label: name, value: name });
233
+ }
234
+ }
235
+ }
236
+ }
237
+ catch { }
238
+ }
239
+ }
240
+ catch { }
241
+ }
242
+ return styles;
243
+ }
@@ -1,3 +1,5 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import {
2
4
  readSettings,
3
5
  writeSettings,
@@ -80,11 +82,6 @@ export async function writeSettingValue(
80
82
  delete (settings as any).attribution;
81
83
  }
82
84
  } else if (setting.storage.type === "attribution-text") {
83
- const attr = (settings as any).attribution;
84
- // If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
85
- if (attr && attr.commit === "" && attr.pr === "") {
86
- return;
87
- }
88
85
  if (value && value.trim().length > 0) {
89
86
  // Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
90
87
  (settings as any).attribution = {
@@ -188,3 +185,87 @@ export async function readAllSettingsBothScopes(
188
185
  }
189
186
  return result;
190
187
  }
188
+
189
+ /**
190
+ * Discover available output styles from installed plugins.
191
+ * Scans enabled plugins for outputStyles entries and *-output-style plugin names.
192
+ * Returns options suitable for a select setting.
193
+ */
194
+ export async function discoverOutputStyles(
195
+ projectPath?: string,
196
+ ): Promise<Array<{ label: string; value: string }>> {
197
+ const styles: Array<{ label: string; value: string }> = [
198
+ { label: "Default", value: "" },
199
+ ];
200
+
201
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
202
+ const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
203
+
204
+ // Collect enabled plugins from both scopes
205
+ const enabledPlugins = new Set<string>();
206
+ try {
207
+ const userSettings = await readGlobalSettings();
208
+ for (const [k, v] of Object.entries((userSettings as any).enabledPlugins || {})) {
209
+ if (v) enabledPlugins.add(k);
210
+ }
211
+ } catch {}
212
+ try {
213
+ const projSettings = await readSettings(projectPath);
214
+ for (const [k, v] of Object.entries((projSettings as any).enabledPlugins || {})) {
215
+ if (v) enabledPlugins.add(k);
216
+ }
217
+ } catch {}
218
+
219
+ const seen = new Set<string>();
220
+
221
+ for (const pluginId of enabledPlugins) {
222
+ const [pluginName, marketplace] = pluginId.split("@");
223
+
224
+ // Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
225
+ if (pluginName.endsWith("-output-style")) {
226
+ const styleName = pluginName
227
+ .replace(/-output-style$/, "")
228
+ .split("-")
229
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
230
+ .join(" ");
231
+ if (!seen.has(styleName)) {
232
+ seen.add(styleName);
233
+ styles.push({ label: styleName, value: styleName });
234
+ }
235
+ continue;
236
+ }
237
+
238
+ // Check plugin.json for outputStyles entries
239
+ if (!marketplace) continue;
240
+ const pluginDir = path.join(cacheDir, marketplace, pluginName);
241
+ try {
242
+ const versions = fs.readdirSync(pluginDir).sort();
243
+ const latest = versions[versions.length - 1];
244
+ if (!latest) continue;
245
+ const manifestPath = path.join(pluginDir, latest, "plugin.json");
246
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
247
+ const outputStyles = manifest.outputStyles as string[] | undefined;
248
+ if (!outputStyles?.length) continue;
249
+
250
+ for (const stylePath of outputStyles) {
251
+ const fullPath = path.join(pluginDir, latest, stylePath);
252
+ try {
253
+ const content = fs.readFileSync(fullPath, "utf-8");
254
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
255
+ if (fmMatch) {
256
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
257
+ if (nameMatch) {
258
+ const name = nameMatch[1].trim();
259
+ if (!seen.has(name)) {
260
+ seen.add(name);
261
+ styles.push({ label: name, value: name });
262
+ }
263
+ }
264
+ }
265
+ } catch {}
266
+ }
267
+ } catch {}
268
+ }
269
+
270
+ return styles;
271
+ }
@@ -52,12 +52,12 @@ export function getEffectiveValue(scoped) {
52
52
  }
53
53
  export function formatValue(setting, value) {
54
54
  if (value === undefined || value === "") {
55
- if (setting.defaultValue !== undefined) {
56
- return setting.type === "boolean"
57
- ? setting.defaultValue === "true"
58
- ? "on"
59
- : "off"
60
- : setting.defaultValue || "default";
55
+ if (setting.type === "boolean" && setting.defaultValue !== undefined) {
56
+ return setting.defaultValue === "true" ? "on" : "off";
57
+ }
58
+ if (setting.type === "select" && setting.defaultValue !== undefined) {
59
+ const opt = setting.options?.find((o) => o.value === setting.defaultValue);
60
+ return opt ? opt.label : setting.defaultValue;
61
61
  }
62
62
  return "—";
63
63
  }
@@ -68,8 +68,8 @@ export function formatValue(setting, value) {
68
68
  const opt = setting.options.find((o) => o.value === value);
69
69
  return opt ? opt.label : value;
70
70
  }
71
- if (value.length > 20) {
72
- return value.slice(0, 20) + "...";
71
+ if (value.length > 12) {
72
+ return value.slice(0, 12) + "";
73
73
  }
74
74
  return value;
75
75
  }
@@ -94,12 +94,12 @@ export function formatValue(
94
94
  value: string | undefined,
95
95
  ): string {
96
96
  if (value === undefined || value === "") {
97
- if (setting.defaultValue !== undefined) {
98
- return setting.type === "boolean"
99
- ? setting.defaultValue === "true"
100
- ? "on"
101
- : "off"
102
- : setting.defaultValue || "default";
97
+ if (setting.type === "boolean" && setting.defaultValue !== undefined) {
98
+ return setting.defaultValue === "true" ? "on" : "off";
99
+ }
100
+ if (setting.type === "select" && setting.defaultValue !== undefined) {
101
+ const opt = setting.options?.find((o) => o.value === setting.defaultValue);
102
+ return opt ? opt.label : setting.defaultValue;
103
103
  }
104
104
  return "—";
105
105
  }
@@ -113,8 +113,8 @@ export function formatValue(
113
113
  return opt ? opt.label : value;
114
114
  }
115
115
 
116
- if (value.length > 20) {
117
- return value.slice(0, 20) + "...";
116
+ if (value.length > 12) {
117
+ return value.slice(0, 12) + "";
118
118
  }
119
119
  return value;
120
120
  }
@@ -1,5 +1,4 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
3
2
  const TABS = [
4
3
  { key: "1", label: "Plugins", screen: "plugins" },
5
4
  { key: "2", label: "Skills", screen: "skills" },
@@ -8,28 +7,7 @@ const TABS = [
8
7
  { key: "5", label: "Profiles", screen: "profiles" },
9
8
  { key: "6", label: "CLI", screen: "cli-tools" },
10
9
  ];
11
- export function TabBar({ currentScreen, onTabChange }) {
12
- // Handle number key shortcuts (1-5)
13
- useKeyboardHandler((input, key) => {
14
- if (!onTabChange)
15
- return;
16
- // Number keys 1-5
17
- const tabIndex = Number.parseInt(input, 10);
18
- if (tabIndex >= 1 && tabIndex <= TABS.length) {
19
- const tab = TABS[tabIndex - 1];
20
- if (tab && tab.screen !== currentScreen) {
21
- onTabChange(tab.screen);
22
- }
23
- }
24
- // Tab key to cycle through tabs
25
- if (key.tab) {
26
- const currentIndex = TABS.findIndex((t) => t.screen === currentScreen);
27
- const nextIndex = key.shift
28
- ? (currentIndex - 1 + TABS.length) % TABS.length
29
- : (currentIndex + 1) % TABS.length;
30
- onTabChange(TABS[nextIndex].screen);
31
- }
32
- });
10
+ export function TabBar({ currentScreen }) {
33
11
  return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
34
12
  const isSelected = tab.screen === currentScreen;
35
13
  const isLast = index === TABS.length - 1;
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
3
2
  import type { Screen } from "../state/types.js";
4
3
 
5
4
  interface Tab {
@@ -19,33 +18,9 @@ const TABS: Tab[] = [
19
18
 
20
19
  interface TabBarProps {
21
20
  currentScreen: Screen;
22
- onTabChange?: (screen: Screen) => void;
23
21
  }
24
22
 
25
- export function TabBar({ currentScreen, onTabChange }: TabBarProps) {
26
- // Handle number key shortcuts (1-5)
27
- useKeyboardHandler((input, key) => {
28
- if (!onTabChange) return;
29
-
30
- // Number keys 1-5
31
- const tabIndex = Number.parseInt(input, 10);
32
- if (tabIndex >= 1 && tabIndex <= TABS.length) {
33
- const tab = TABS[tabIndex - 1];
34
- if (tab && tab.screen !== currentScreen) {
35
- onTabChange(tab.screen);
36
- }
37
- }
38
-
39
- // Tab key to cycle through tabs
40
- if (key.tab) {
41
- const currentIndex = TABS.findIndex((t) => t.screen === currentScreen);
42
- const nextIndex = key.shift
43
- ? (currentIndex - 1 + TABS.length) % TABS.length
44
- : (currentIndex + 1) % TABS.length;
45
- onTabChange(TABS[nextIndex].screen);
46
- }
47
- });
48
-
23
+ export function TabBar({ currentScreen }: TabBarProps) {
49
24
  return (
50
25
  <box flexDirection="row" gap={0}>
51
26
  {TABS.map((tab, index) => {
@@ -9,6 +9,6 @@ export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
9
9
  onCancel();
10
10
  }
11
11
  });
12
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "yellow", backgroundColor: "#1a1a2e", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("text", { children: _jsx("strong", { children: title }) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { children: message }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "green", children: "[Y]" }), _jsx("span", { children: "es " }), _jsx("span", { fg: "red", children: "[N]" }), _jsx("span", { children: "o" })] }) })] }));
12
+ return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
13
13
  }
14
14
  export default ConfirmModal;