Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Local agent/runtime state
.omc/
.omx/
.superpowers/

# macOS / OS metadata
.DS_Store
Expand Down
68 changes: 43 additions & 25 deletions DESIGN.md

Large diffs are not rendered by default.

29 changes: 12 additions & 17 deletions apps/companion/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { isTauri } from "@tauri-apps/api/core";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityCalendarView } from "./activity/ActivityCalendarView";
import { createActivityRefresh } from "./application/activity";
import { resolvedCompanionTheme } from "./application/preferences";
import { buildMenuModel, type MenuModel } from "./application/state";
import { useSystemThemeMode } from "./application/systemThemeMode";
import { useCompanionSettings } from "./application/useCompanionSettings";
import type { GlobalState, Task } from "./domain/model";
import {
Expand All @@ -18,13 +16,12 @@ import {
watchRoots,
} from "./infrastructure/tauriClient";
import { startWorkspaceMonitor } from "./infrastructure/workspaceMonitor";
import { AppSummary } from "./ui/AppSummary";
import { AppToolbar } from "./ui/AppToolbar";
import { AgentHeader } from "./ui/AgentHeader";
import { AgentTabs, type CompanionView } from "./ui/AgentTabs";
import { ProjectGroup } from "./ui/ProjectGroup";
import { SettingsView } from "./ui/SettingsView";
import { StatusAlert } from "./ui/StatusAlert";
import type { TaskActionKind } from "./ui/TaskRow";
import { type CompanionView, ViewNav } from "./ui/ViewNav";

const EMPTY_STATE: GlobalState = { projects: [], errors: [] };
const TAURI_RUNTIME_UNAVAILABLE = "Tauri runtime unavailable";
Expand Down Expand Up @@ -57,7 +54,6 @@ export function App() {
const [status, setStatus] = useState("Ready");
const [visibleError, setVisibleError] = useState<string>();
const [currentView, setCurrentView] = useState<CompanionView>("main");
const systemThemeMode = useSystemThemeMode();
const model: MenuModel = buildMenuModel(state);
const tauriRuntimeAvailable = isTauri();
const refreshWithActivity = useMemo(
Expand Down Expand Up @@ -92,7 +88,7 @@ export function App() {
updateLaunchAtLogin,
updatePreferences,
} = useCompanionSettings({ onError: showError, onStatus: showStatus });
const activeTheme = resolvedCompanionTheme(preferences, systemThemeMode);
const activeTheme = preferences.theme;

const applyState = useCallback(
(next: GlobalState) => {
Expand Down Expand Up @@ -188,14 +184,13 @@ export function App() {

return (
<main data-font={preferences.font} data-theme={activeTheme}>
<header>
<AppSummary summary={model.summary} />
<AppToolbar
onRefresh={() => void refresh()}
onQuit={handleQuit}
status={status}
/>
</header>
<AgentHeader
theme={activeTheme}
summary={model.summary}
status={status}
onRefresh={() => void refresh()}
onQuit={handleQuit}
/>
<StatusAlert message={visibleError} />
{currentView === "main" ? (
<section className="view-panel" aria-label="Main View">
Expand All @@ -206,6 +201,7 @@ export function App() {
<ProjectGroup
key={group.root}
group={group}
theme={activeTheme}
onAction={(root, task, kind) =>
void handleTaskAction(root, task, kind)
}
Expand All @@ -228,7 +224,6 @@ export function App() {
{currentView === "settings" ? (
<SettingsView
preferences={preferences}
systemThemeMode={systemThemeMode}
launchAtLogin={launchAtLogin}
launchAtLoginLoading={launchAtLoginLoading}
onLaunchAtLoginChange={(enabled) => void updateLaunchAtLogin(enabled)}
Expand All @@ -240,7 +235,7 @@ export function App() {
{error.root}: {error.message}
</p>
))}
<ViewNav currentView={currentView} onViewChange={setCurrentView} />
<AgentTabs currentView={currentView} onViewChange={setCurrentView} />
</main>
);
}
27 changes: 15 additions & 12 deletions apps/companion/src/activity/activity-calendar.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
align-items: stretch;
background: var(--surface-1);
border: 1px solid var(--line);
border-radius: var(--radius-row);
border-radius: 6px;
display: block;
padding: 10px;
text-align: left;
Expand Down Expand Up @@ -82,37 +82,37 @@

.cal-chip[data-color="0"],
.cal-session[data-color="0"] {
--cal-bg: rgba(203, 166, 247, 0.18);
--cal-bg: var(--cal-bg-1);
--cal-color: var(--cal-1);
}

.cal-chip[data-color="1"],
.cal-session[data-color="1"] {
--cal-bg: rgba(137, 180, 250, 0.18);
--cal-bg: var(--cal-bg-2);
--cal-color: var(--cal-2);
}

.cal-chip[data-color="2"],
.cal-session[data-color="2"] {
--cal-bg: rgba(166, 227, 161, 0.18);
--cal-bg: var(--cal-bg-3);
--cal-color: var(--cal-3);
}

.cal-chip[data-color="3"],
.cal-session[data-color="3"] {
--cal-bg: rgba(249, 226, 175, 0.18);
--cal-bg: var(--cal-bg-4);
--cal-color: var(--cal-4);
}

.cal-chip[data-color="4"],
.cal-session[data-color="4"] {
--cal-bg: rgba(245, 194, 231, 0.18);
--cal-bg: var(--cal-bg-5);
--cal-color: var(--cal-5);
}

.cal-chip[data-color="5"],
.cal-session[data-color="5"] {
--cal-bg: rgba(148, 226, 213, 0.18);
--cal-bg: var(--cal-bg-6);
--cal-color: var(--cal-6);
}

Expand All @@ -134,7 +134,7 @@
}

.cal-hour-label {
color: var(--faint);
color: var(--muted);
font-size: 10px;
line-height: 1;
padding-top: 1px;
Expand All @@ -150,7 +150,7 @@
.cal-day-column {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 10px;
border-radius: 4px;
min-width: 0;
overflow: hidden;
position: relative;
Expand Down Expand Up @@ -188,8 +188,7 @@
background: var(--cal-bg);
border-color: var(--cal-color, var(--accent));
border-left: 3px solid var(--cal-color, var(--accent));
border-radius: 7px;
box-shadow: none;
border-radius: 4px;
color: var(--text);
display: grid;
gap: 1px;
Expand Down Expand Up @@ -217,6 +216,10 @@
background: var(--cal-bg);
}

.cal-session[data-selected="true"] {
border-color: var(--emphasis);
}

@supports (background: color-mix(in srgb, black, white)) {
.cal-session {
background: color-mix(
Expand Down Expand Up @@ -282,7 +285,7 @@
.cal-detail {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 10px;
border-radius: 4px;
display: grid;
gap: 5px;
padding: 8px;
Expand Down
100 changes: 17 additions & 83 deletions apps/companion/src/application/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import { load } from "@tauri-apps/plugin-store";
import {
COMPANION_THEME_MODE_OPTIONS,
COMPANION_THEME_OPTIONS,
type CompanionResolvedThemeMode,
type CompanionTheme,
type CompanionThemeFamily,
type CompanionThemeMode,
isCompanionThemeFamily,
isCompanionThemeMode,
isLegacyCompanionTheme,
resolvedThemeValue,
themeFamilyFromLegacyTheme,
themeModeFromLegacyTheme,
isCompanionTheme,
} from "./themePreferences";

export type {
CompanionResolvedThemeMode,
CompanionTheme,
CompanionThemeFamily,
CompanionThemeMode,
CompanionThemeModeOption,
CompanionThemeOption,
} from "./themePreferences";
export {
COMPANION_THEME_MODE_OPTIONS,
COMPANION_THEME_OPTIONS,
resolvedThemeValue,
};
export { COMPANION_THEME_OPTIONS };

export const COMPANION_PREFERENCES_STORE_FILE = "companion-preferences.json";

Expand All @@ -42,8 +25,7 @@ export type CompanionFont = (typeof COMPANION_FONT_VALUES)[number];

export type CompanionPreferences = {
readonly font: CompanionFont;
readonly themeFamily: CompanionThemeFamily;
readonly themeMode: CompanionThemeMode;
readonly theme: CompanionTheme;
};

export type CompanionFontOption = {
Expand All @@ -59,8 +41,7 @@ export type PreferenceSanitizationResult = {

export type PreferenceStoreEntries = {
readonly font: CompanionFont;
readonly themeFamily: CompanionThemeFamily;
readonly themeMode: CompanionThemeMode;
readonly theme: CompanionTheme;
};

export type CompanionPreferenceStore = {
Expand All @@ -71,8 +52,7 @@ export type CompanionPreferenceStore = {

export const DEFAULT_COMPANION_PREFERENCES: CompanionPreferences = {
font: "system-mono",
themeFamily: "companion",
themeMode: "system",
theme: "claude",
};

export const COMPANION_FONT_OPTIONS: readonly CompanionFontOption[] = [
Expand Down Expand Up @@ -119,66 +99,31 @@ export function isCompanionFont(value: unknown): value is CompanionFont {
}
}

function migrateRemovedThemeFamily(
value: unknown,
): CompanionThemeFamily | undefined {
switch (value) {
case "breakfast":
case "solarized":
case "dracula":
case "catppuccin":
case "github":
case "gruvbox":
case "nord":
return "companion";
default:
return undefined;
}
}

export function sanitizeCompanionPreferences(input: {
readonly font?: unknown;
readonly theme?: unknown;
readonly themeFamily?: unknown;
readonly themeMode?: unknown;
readonly theme?: unknown;
}): PreferenceSanitizationResult {
const font = isCompanionFont(input.font)
? input.font
: DEFAULT_COMPANION_PREFERENCES.font;
const legacyTheme = isLegacyCompanionTheme(input.theme)
const theme = isCompanionTheme(input.theme)
? input.theme
: undefined;
const removedThemeFamily = migrateRemovedThemeFamily(input.themeFamily);
const themeFamily = isCompanionThemeFamily(input.themeFamily)
? input.themeFamily
: removedThemeFamily
? removedThemeFamily
: legacyTheme
? themeFamilyFromLegacyTheme(legacyTheme)
: DEFAULT_COMPANION_PREFERENCES.themeFamily;
const themeMode = isCompanionThemeMode(input.themeMode)
? input.themeMode
: legacyTheme
? themeModeFromLegacyTheme(legacyTheme)
: DEFAULT_COMPANION_PREFERENCES.themeMode;
: isCompanionTheme(input.themeFamily)
? input.themeFamily
: DEFAULT_COMPANION_PREFERENCES.theme;
return {
preferences: { font, themeFamily, themeMode },
sanitized:
font !== input.font ||
themeFamily !== input.themeFamily ||
themeMode !== input.themeMode,
preferences: { font, theme },
sanitized: font !== input.font || theme !== input.theme,
};
}

export function shouldRestoreFailedPreferenceUpdate(
current: CompanionPreferences,
attempted: CompanionPreferences,
): boolean {
return (
current.font === attempted.font &&
current.themeFamily === attempted.themeFamily &&
current.themeMode === attempted.themeMode
);
return current.font === attempted.font && current.theme === attempted.theme;
}

export type PreferenceSaveOperation = () => Promise<void>;
Expand All @@ -195,15 +140,14 @@ export function preferencesToStoreEntries(
): PreferenceStoreEntries {
return {
font: preferences.font,
themeFamily: preferences.themeFamily,
themeMode: preferences.themeMode,
theme: preferences.theme,
};
}

export async function loadCompanionPreferenceStore(): Promise<CompanionPreferenceStore> {
return load(COMPANION_PREFERENCES_STORE_FILE, {
autoSave: false,
defaults: preferencesToStoreEntries(DEFAULT_COMPANION_PREFERENCES),
defaults: {},
});
}

Expand All @@ -212,9 +156,9 @@ export async function readCompanionPreferences(
): Promise<PreferenceSanitizationResult> {
return sanitizeCompanionPreferences({
font: await store.get<unknown>("font"),
theme: await store.get<unknown>("theme"),
themeFamily: await store.get<unknown>("themeFamily"),
themeMode: await store.get<unknown>("themeMode"),
theme: await store.get<unknown>("theme"),
});
}

Expand All @@ -224,16 +168,6 @@ export async function writeCompanionPreferences(
): Promise<void> {
const entries = preferencesToStoreEntries(preferences);
await store.set("font", entries.font);
await store.set("themeFamily", entries.themeFamily);
await store.set("themeMode", entries.themeMode);
await store.set("theme", entries.theme);
await store.save();
}

export function resolvedCompanionTheme(
preferences: CompanionPreferences,
systemMode: CompanionResolvedThemeMode,
): CompanionTheme {
const mode =
preferences.themeMode === "system" ? systemMode : preferences.themeMode;
return resolvedThemeValue(preferences.themeFamily, mode);
}
Loading
Loading