Fix settings page (#17)

* fix: repair broken settings modal layout

The <nav> element with settings navigation buttons was a sibling of the
sidebar div instead of being nested inside it, and the main flex container
was missing its closing tag. This broke the sidebar/content flex layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore missing padding wrapper around Settings title

The merge in #15 dropped a wrapper div with px-4 py-4 padding and
entrance animation around the "Settings" heading, causing it to be
smashed against the top-left corner of the sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: consolidate types and stores into feature-sliced architecture

Removed root-level src/types/ and src/stores/ directories that contained
outdated duplicates of types and unused store implementations. Moved unique
config types (AgentConfig, Skill, Provider, Model) to src/shared/types/config.ts.
Relocated stores to their owning features (agentConfigStore → agents,
sessionStore → chat, skillStore → skills). Deduplicated ChatState, TokenState,
and Session types — src/shared/types/chat.ts is now canonical. Updated
AGENTS.md with clear guidelines on where types and stores should live.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-03-30 19:21:31 -07:00
committed by GitHub
parent c7ec58f660
commit 8c3471f43f
5 changed files with 138 additions and 164 deletions
+6 -1
View File
@@ -19,6 +19,10 @@ src/
api/ — Backend API integration (when feature calls backend)
types.ts — Feature-specific type definitions (when needed)
shared/
types/ — Canonical shared type definitions (single source of truth)
agents.ts — Agent, Persona, Provider types
chat.ts — ChatState, TokenState, Session, SSE events
messages.ts — Message, MessageContent, type guards
ui/ — Reusable UI components (button, etc.)
lib/ — Utilities (cn.ts for class merging)
theme/ — Theme provider, appearance settings
@@ -27,7 +31,6 @@ src/
api/ — API integration
constants/ — Shared constants
context/ — Shared contexts
types/ — Shared type definitions (single source of truth)
```
### Feature Organization
@@ -133,3 +136,5 @@ ThemeProvider manages three axes:
- Don't skip `type="button"` on buttons.
- Don't use color-only indicators without text alternatives.
- Never use `--no-verify` when pushing — fix the underlying lint/hook issues.
- Don't create root-level `src/types/` or `src/stores/` directories — types belong in `src/shared/types/`, stores belong in `src/features/<feature>/stores/`.
- Don't duplicate type definitions across files — each type has one canonical location.
@@ -100,133 +100,144 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
"flex w-44 flex-col border-r bg-background-secondary/50 transition-all duration-700 ease-out",
isLoaded ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-2",
)}
>
<h2 className="text-sm font-semibold">Settings</h2>
</div>
<nav className="flex flex-col gap-1 px-2">
{NAV_ITEMS.map((item, index) => (
<button
type="button"
key={item.id}
onClick={() => setActiveSection(item.id)}
className={cn(
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-600 ease-out",
activeSection === item.id
? "bg-background text-foreground shadow-sm"
: "text-foreground-secondary hover:bg-background-secondary/50 hover:text-foreground duration-300",
isLoaded
? "opacity-100 translate-x-0"
: "opacity-0 translate-x-4",
)}
style={{
transitionDelay: isLoaded ? "0ms" : `${index * 40 + 300}ms`,
}}
>
<item.icon className="h-4 w-4" />
{item.label}
</button>
))}
</nav>
</div>
{/* Content */}
<div className="relative flex-1 overflow-y-auto">
<button
type="button"
onClick={onClose}
className="absolute right-4 top-4 z-10 rounded-md p-1 text-foreground-secondary transition-colors hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
<div
className={cn(
"px-6 py-4 transition-all duration-400 ease-out",
isTransitioning
? "opacity-0 translate-y-2"
: "opacity-100 translate-y-0",
)}
>
<div
className={cn(
"transition-all duration-600 ease-out",
"px-4 py-4 transition-all duration-500 ease-out",
isLoaded
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-4",
? "opacity-100 translate-x-0"
: "opacity-0 -translate-x-2",
)}
style={{
transitionDelay: isLoaded ? "400ms" : "0ms",
}}
>
{activeSection === "appearance" && <AppearanceSettings />}
{activeSection === "general" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">General</h3>
<p className="mt-1 text-sm text-foreground-secondary">
General settings will appear here.
</p>
</div>
</div>
)}
{activeSection === "projects" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">Projects</h3>
<p className="mt-1 text-sm text-foreground-secondary">
Manage your projects.
</p>
</div>
<h2 className="text-sm font-semibold">Settings</h2>
</div>
<nav className="flex flex-col gap-1 px-2">
{NAV_ITEMS.map((item, index) => (
<button
type="button"
key={item.id}
onClick={() => setActiveSection(item.id)}
className={cn(
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-600 ease-out",
activeSection === item.id
? "bg-background text-foreground shadow-sm"
: "text-foreground-secondary hover:bg-background-secondary/50 hover:text-foreground duration-300",
isLoaded
? "opacity-100 translate-x-0"
: "opacity-0 translate-x-4",
)}
style={{
transitionDelay: isLoaded ? "0ms" : `${index * 40 + 300}ms`,
}}
>
<item.icon className="h-4 w-4" />
{item.label}
</button>
))}
</nav>
</div>
{/* Archived Projects */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Archived Projects</h3>
{!loadingArchived && archivedProjects.length === 0 && (
<p className="text-xs text-foreground-secondary">
No archived projects.
{/* Content */}
<div className="relative flex-1 overflow-y-auto">
<button
type="button"
onClick={onClose}
className="absolute right-4 top-4 z-10 rounded-md p-1 text-foreground-secondary transition-colors hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
<div
className={cn(
"px-6 py-4 transition-all duration-400 ease-out",
isTransitioning
? "opacity-0 translate-y-2"
: "opacity-100 translate-y-0",
)}
>
<div
className={cn(
"transition-all duration-600 ease-out",
isLoaded
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-4",
)}
style={{
transitionDelay: isLoaded ? "400ms" : "0ms",
}}
>
{activeSection === "appearance" && <AppearanceSettings />}
{activeSection === "general" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">General</h3>
<p className="mt-1 text-sm text-foreground-secondary">
General settings will appear here.
</p>
)}
{archivedProjects.map((project) => (
<div
key={project.id}
className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2"
>
<div className="flex items-center gap-2 min-w-0">
<span
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: project.color }}
/>
<span className="text-sm truncate">{project.name}</span>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<button
type="button"
onClick={() => handleRestore(project.id)}
className="px-2 py-1 text-xs font-medium rounded-md border border-border hover:bg-background-tertiary transition-colors"
>
Restore
</button>
<button
type="button"
onClick={() => setDeletingProject(project)}
className="px-2 py-1 text-xs font-medium rounded-md text-foreground-danger hover:bg-background-danger/10 transition-colors"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
</div>
)}
{activeSection === "about" && (
<div>
<h3 className="text-lg font-semibold">About</h3>
<p className="mt-1 text-sm text-foreground-secondary">
About information will appear here.
</p>
</div>
)}
)}
{activeSection === "projects" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">Projects</h3>
<p className="mt-1 text-sm text-foreground-secondary">
Manage your projects.
</p>
</div>
{/* Archived Projects */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Archived Projects</h3>
{!loadingArchived && archivedProjects.length === 0 && (
<p className="text-xs text-foreground-secondary">
No archived projects.
</p>
)}
{archivedProjects.map((project) => (
<div
key={project.id}
className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2"
>
<div className="flex items-center gap-2 min-w-0">
<span
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: project.color }}
/>
<span className="text-sm truncate">
{project.name}
</span>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<button
type="button"
onClick={() => handleRestore(project.id)}
className="px-2 py-1 text-xs font-medium rounded-md border border-border hover:bg-background-tertiary transition-colors"
>
Restore
</button>
<button
type="button"
onClick={() => setDeletingProject(project)}
className="px-2 py-1 text-xs font-medium rounded-md text-foreground-danger hover:bg-background-danger/10 transition-colors"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
)}
{activeSection === "about" && (
<div>
<h3 className="text-lg font-semibold">About</h3>
<p className="mt-1 text-sm text-foreground-secondary">
About information will appear here.
</p>
</div>
)}
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import type { Message } from "@/shared/types/messages";
import type { Session } from "@/shared/types/agents";
import type { Session } from "@/shared/types/chat";
export async function createSession(agentId?: string): Promise<Session> {
return invoke("create_session", { agentId });
+1 -44
View File
@@ -79,47 +79,4 @@ export interface CreateAgentRequest {
acpEndpoint?: string;
}
// Session types
export interface Session {
id: string;
title: string;
agentId?: string;
createdAt: string;
updatedAt: string;
messageCount: number;
lastMessagePreview?: string;
}
// Token tracking
export interface TokenState {
inputTokens: number;
outputTokens: number;
totalTokens: number;
accumulatedInput: number;
accumulatedOutput: number;
accumulatedTotal: number;
}
// Chat state
export type ChatState =
| "idle"
| "thinking"
| "streaming"
| "waiting"
| "compacting"
| "error";
// SSE event types (from goosed server)
export type MessageEventType =
| "message"
| "error"
| "finish"
| "modelChange"
| "notification"
| "updateConversation"
| "ping";
export interface MessageEvent {
type: MessageEventType;
data: unknown;
}
// Session, TokenState, ChatState, and MessageEventType are defined in ./chat.ts
+1
View File
@@ -1,2 +1,3 @@
export * from "./messages";
export * from "./agents";
export * from "./chat";