Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function App() {
const {
handleReorder,
handleToggle,
handleToggleOverviewLabel,
} = useSettingsPluginActions({
pluginSettings,
setPluginSettings,
Expand Down Expand Up @@ -241,6 +242,7 @@ function App() {
onRetryPlugin: handleRetryPlugin,
onReorder: handleReorder,
onToggle: handleToggle,
onToggleOverviewLabel: handleToggleOverviewLabel,
onAutoUpdateIntervalChange: handleAutoUpdateIntervalChange,
onThemeModeChange: handleThemeModeChange,
onDisplayModeChange: handleDisplayModeChange,
Expand Down
3 changes: 3 additions & 0 deletions src/components/app/app-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type AppContentActionProps = {
onRetryPlugin: (id: string) => void
onReorder: (orderedIds: string[]) => void
onToggle: (id: string) => void
onToggleOverviewLabel: (pluginId: string, label: string) => void
onAutoUpdateIntervalChange: (value: AutoUpdateIntervalMinutes) => void
onThemeModeChange: (mode: ThemeMode) => void
onDisplayModeChange: (mode: DisplayMode) => void
Expand All @@ -46,6 +47,7 @@ export function AppContent({
onRetryPlugin,
onReorder,
onToggle,
onToggleOverviewLabel,
onAutoUpdateIntervalChange,
onThemeModeChange,
onDisplayModeChange,
Expand Down Expand Up @@ -100,6 +102,7 @@ export function AppContent({
plugins={settingsPlugins}
onReorder={onReorder}
onToggle={onToggle}
onToggleOverviewLabel={onToggleOverviewLabel}
autoUpdateInterval={autoUpdateInterval}
onAutoUpdateIntervalChange={onAutoUpdateIntervalChange}
themeMode={themeMode}
Expand Down
7 changes: 5 additions & 2 deletions src/components/provider-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface ProviderCardProps {
onRetry?: () => void
scopeFilter?: "overview" | "all"
displayMode: DisplayMode
disabledOverviewLabels?: string[]
resetTimerDisplayMode?: ResetTimerDisplayMode
onResetTimerDisplayModeToggle?: () => void
}
Expand Down Expand Up @@ -94,6 +95,7 @@ export function ProviderCard({
displayMode,
resetTimerDisplayMode = "relative",
onResetTimerDisplayModeToggle,
disabledOverviewLabels = [],
}: ProviderCardProps) {
const cooldownRemainingMs = useMemo(() => {
if (!lastManualRefreshAt) return 0
Expand All @@ -102,14 +104,15 @@ export function ProviderCard({
}, [lastManualRefreshAt])

// Filter lines based on scope - match by label since runtime lines can differ from manifest
const disabledSet = new Set(disabledOverviewLabels)
const overviewLabels = new Set(
skeletonLines
.filter(line => line.scope === "overview")
.filter(line => line.scope === "overview" && !disabledSet.has(line.label))
.map(line => line.label)
)
const filteredSkeletonLines = scopeFilter === "all"
? skeletonLines
: skeletonLines.filter(line => line.scope === "overview")
: skeletonLines.filter(line => line.scope === "overview" && !disabledSet.has(line.label))
const filteredLines = scopeFilter === "all"
? lines
: lines.filter(line => overviewLabels.has(line.label))
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/app/use-app-plugin-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { PluginMeta } from "@/lib/plugin-types"
import type { PluginSettings } from "@/lib/settings"
import type { PluginState } from "@/hooks/app/types"

export type DisplayPluginState = { meta: PluginMeta } & PluginState
export type DisplayPluginState = { meta: PluginMeta; disabledOverviewLabels: string[] } & PluginState

type UseAppPluginViewsArgs = {
activeView: ActiveView
Expand Down Expand Up @@ -33,7 +33,7 @@ export function useAppPluginViews({
if (!meta) return null
const state =
pluginStates[id] ?? { data: null, loading: false, error: null, lastManualRefreshAt: null }
return { meta, ...state }
return { meta, ...state, disabledOverviewLabels: pluginSettings.disabledOverviewLabels?.[id] || [] }
})
.filter((plugin): plugin is DisplayPluginState => Boolean(plugin))
}, [pluginSettings, pluginStates, pluginsMeta])
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/app/use-settings-plugin-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,32 @@ export function useSettingsPluginActions({
startBatch,
])

const handleToggleOverviewLabel = useCallback((pluginId: string, label: string) => {
if (!pluginSettings) return
const currentDisabled = pluginSettings.disabledOverviewLabels?.[pluginId] || []
const isCurrentlyDisabled = currentDisabled.includes(label)

const nextDisabled = isCurrentlyDisabled
? currentDisabled.filter((l) => l !== label)
: [...currentDisabled, label]

const nextSettings: PluginSettings = {
...pluginSettings,
disabledOverviewLabels: {
...pluginSettings.disabledOverviewLabels,
[pluginId]: nextDisabled,
},
Comment on lines +103 to +108
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the last disabled label is re-enabled, handleToggleOverviewLabel will persist an empty array for that plugin id (e.g. { disabledOverviewLabels: { [pluginId]: [] } }). This is semantically equivalent to omitting the key, but arePluginSettingsEqual() will treat {} vs {id: []} as different and it also bloats the stored settings. Consider deleting the key when nextDisabled.length === 0 instead of storing an empty list.

Suggested change
const nextSettings: PluginSettings = {
...pluginSettings,
disabledOverviewLabels: {
...pluginSettings.disabledOverviewLabels,
[pluginId]: nextDisabled,
},
const nextDisabledOverviewLabels = {
...(pluginSettings.disabledOverviewLabels ?? {}),
}
if (nextDisabled.length === 0) {
delete nextDisabledOverviewLabels[pluginId]
} else {
nextDisabledOverviewLabels[pluginId] = nextDisabled
}
const nextSettings: PluginSettings = {
...pluginSettings,
disabledOverviewLabels: nextDisabledOverviewLabels,

Copilot uses AI. Check for mistakes.
}

setPluginSettings(nextSettings)
scheduleTrayIconUpdate("settings", TRAY_SETTINGS_DEBOUNCE_MS)
void savePluginSettings(nextSettings).catch((error) => {
console.error("Failed to save plugin overview label toggle:", error)
})
}, [pluginSettings, scheduleTrayIconUpdate, setPluginSettings])
return {
handleReorder,
handleToggle,
handleToggleOverviewLabel,
}
}
6 changes: 5 additions & 1 deletion src/hooks/app/use-settings-plugin-list.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useMemo } from "react"
import type { PluginMeta } from "@/lib/plugin-types"
import type { ManifestLine, PluginMeta } from "@/lib/plugin-types"
import type { PluginSettings } from "@/lib/settings"

export type SettingsPluginState = {
id: string
name: string
enabled: boolean
lines: ManifestLine[]
disabledOverviewLabels: string[]
}
Comment on lines 5 to 11
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSettingsPluginList now returns additional required fields (lines, disabledOverviewLabels). Any callers/tests doing strict equality against the previous { id, name, enabled } shape will break; update the corresponding consumers/tests to include the new properties (or assert via expect.objectContaining).

Copilot uses AI. Check for mistakes.

type UseSettingsPluginListArgs = {
Expand All @@ -26,6 +28,8 @@ export function useSettingsPluginList({ pluginSettings, pluginsMeta }: UseSettin
id,
name: meta.name,
enabled: !pluginSettings.disabled.includes(id),
lines: meta.lines,
disabledOverviewLabels: pluginSettings.disabledOverviewLabels?.[id] || [],
}
})
.filter((plugin): plugin is SettingsPluginState => Boolean(plugin))
Expand Down
1 change: 1 addition & 0 deletions src/lib/plugin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ export type PluginDisplayState = {
loading: boolean
error: string | null
lastManualRefreshAt: number | null
disabledOverviewLabels?: string[]
}
27 changes: 26 additions & 1 deletion src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const REFRESH_COOLDOWN_MS = 300_000;
export type PluginSettings = {
order: string[];
disabled: string[];
disabledOverviewLabels?: Record<string, string[]>;
};

export type AutoUpdateIntervalMinutes = 5 | 15 | 30 | 60;
Expand Down Expand Up @@ -83,6 +84,7 @@ const DEFAULT_ENABLED_PLUGINS = new Set(["claude", "codex", "cursor"]);
export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
order: [],
disabled: [],
disabledOverviewLabels: {},
};

export async function loadPluginSettings(): Promise<PluginSettings> {
Expand All @@ -91,6 +93,7 @@ export async function loadPluginSettings(): Promise<PluginSettings> {
return {
order: Array.isArray(stored.order) ? stored.order : [],
disabled: Array.isArray(stored.disabled) ? stored.disabled : [],
disabledOverviewLabels: stored.disabledOverviewLabels || {},
};
}

Expand Down Expand Up @@ -148,7 +151,16 @@ export function normalizePluginSettings(
disabled.push(id);
}
}
return { order, disabled };
const disabledOverviewLabels: Record<string, string[]> = {};
if (settings.disabledOverviewLabels) {
for (const [id, labels] of Object.entries(settings.disabledOverviewLabels)) {
if (knownSet.has(id) && Array.isArray(labels)) {
disabledOverviewLabels[id] = labels;
}
}
Comment on lines +154 to +160
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizePluginSettings() copies disabledOverviewLabels values as-is when they are arrays, but doesn’t sanitize the array contents (e.g. non-string entries) or normalize ordering / deduplicate. Since arePluginSettingsEqual() performs an order-sensitive comparison, semantically identical label sets with different ordering will be treated as unequal. Consider filtering to strings and sorting (and optionally removing duplicates) during normalization to keep settings deterministic and avoid unnecessary re-saves.

Copilot uses AI. Check for mistakes.
}

return { order, disabled, disabledOverviewLabels };
}

export function arePluginSettingsEqual(
Expand All @@ -163,6 +175,19 @@ export function arePluginSettingsEqual(
for (let i = 0; i < a.disabled.length; i += 1) {
if (a.disabled[i] !== b.disabled[i]) return false;
}
const aLabels = a.disabledOverviewLabels || {};
const bLabels = b.disabledOverviewLabels || {};
const aKeys = Object.keys(aLabels);
const bKeys = Object.keys(bLabels);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
const aList = aLabels[key];
const bList = bLabels[key];
if (!bList || aList.length !== bList.length) return false;
for (let i = 0; i < aList.length; i += 1) {
if (aList[i] !== bList[i]) return false;
}
}
return true;
}

Expand Down
1 change: 1 addition & 0 deletions src/pages/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function OverviewPage({
displayMode={displayMode}
resetTimerDisplayMode={resetTimerDisplayMode}
onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle}
disabledOverviewLabels={plugin.disabledOverviewLabels}
/>
))}
</div>
Expand Down
73 changes: 48 additions & 25 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface PluginConfig {
id: string;
name: string;
enabled: boolean;
lines: import("@/lib/plugin-types").ManifestLine[];
disabledOverviewLabels: string[];
}

const TRAY_PREVIEW_SIZE_PX = getTrayIconSizePx(1);
Expand Down Expand Up @@ -196,9 +198,11 @@ function MenubarIconStylePreview({
function SortablePluginItem({
plugin,
onToggle,
onToggleOverviewLabel,
}: {
plugin: PluginConfig;
onToggle: (id: string) => void;
onToggleOverviewLabel: (pluginId: string, label: string) => void;
}) {
const {
attributes,
Expand All @@ -208,45 +212,61 @@ function SortablePluginItem({
transition,
isDragging,
} = useSortable({ id: plugin.id });

const style = {
transform: CSS.Transform.toString(transform),
transition,
};

const overviewLines = plugin.lines ? plugin.lines.filter((l) => l.scope === "overview") : [];
return (
<div
ref={setNodeRef}
style={style}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-md bg-card",
"border border-transparent",
"flex flex-col gap-2 px-3 py-2 rounded-md bg-card border border-transparent",
isDragging && "opacity-50 border-border"
)}
>
<button
type="button"
className="touch-none cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground transition-colors"
{...attributes}
{...listeners}
>
<GripVertical className="h-4 w-4" />
<div className="flex items-center gap-3">
<button
type="button"
className="touch-none cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground transition-colors"
{...attributes}
{...listeners}
>
<GripVertical className="h-4 w-4" />
</button>

<span
className={cn(
"flex-1 text-sm",
!plugin.enabled && "text-muted-foreground"
)}
>
{plugin.name}
</span>

<Checkbox
key={`${plugin.id}-${plugin.enabled}`}
checked={plugin.enabled}
onCheckedChange={() => onToggle(plugin.id)}
/>
<span
className={cn(
"flex-1 text-sm",
!plugin.enabled && "text-muted-foreground"
)}
>
{plugin.name}
</span>
<Checkbox
key={`${plugin.id}-${plugin.enabled}`}
checked={plugin.enabled}
onCheckedChange={() => onToggle(plugin.id)}
/>
</div>
{plugin.enabled && overviewLines.length > 0 && (
<div className="flex flex-col gap-1.5 pl-7 pr-1 pb-1">
{overviewLines.map((line) => {
const isLineEnabled = !plugin.disabledOverviewLabels.includes(line.label);
return (
<label key={line.label} className="flex items-center gap-2 text-xs select-none text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
<Checkbox
checked={isLineEnabled}
onCheckedChange={() => onToggleOverviewLabel(plugin.id, line.label)}
className="h-3.5 w-3.5"
/>
{line.label}
</label>
);
})}
</div>
)}
</div>
);
}
Expand All @@ -255,6 +275,7 @@ interface SettingsPageProps {
plugins: PluginConfig[];
onReorder: (orderedIds: string[]) => void;
onToggle: (id: string) => void;
onToggleOverviewLabel: (pluginId: string, label: string) => void;
autoUpdateInterval: AutoUpdateIntervalMinutes;
onAutoUpdateIntervalChange: (value: AutoUpdateIntervalMinutes) => void;
themeMode: ThemeMode;
Expand All @@ -276,6 +297,7 @@ export function SettingsPage({
plugins,
onReorder,
onToggle,
onToggleOverviewLabel,
autoUpdateInterval,
onAutoUpdateIntervalChange,
themeMode,
Expand Down Expand Up @@ -504,6 +526,7 @@ export function SettingsPage({
key={plugin.id}
plugin={plugin}
onToggle={onToggle}
onToggleOverviewLabel={onToggleOverviewLabel}
/>
))}
</SortableContext>
Expand Down
Loading