Skip to content
Merged
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
657 changes: 654 additions & 3 deletions docs/index.html

Large diffs are not rendered by default.

1,250 changes: 1,250 additions & 0 deletions docs/superpowers/plans/2026-06-12-settings-ui-refactor.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions server/fresh-agent/adapters/codex/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,12 @@ export function classifyCodexItemRole(item: Record<string, unknown>): CodexDispl
}
}

function readCodexRawItems(rawTurn: Record<string, unknown>): Record<string, unknown>[] {
return Array.isArray(rawTurn.items)
? rawTurn.items.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object' && !Array.isArray(item))
: []
}

function readCodexTurnError(rawTurn: Record<string, unknown>): string | undefined {
const error = rawTurn.error
if (!error) return undefined
Expand Down
98 changes: 61 additions & 37 deletions src/components/ExtensionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface ExtensionsViewProps {
onNavigate: (view: AppView) => void
}

interface ExtensionsManagerProps {
className?: string
includeCli?: boolean
}

function categoryIcon(category: 'cli' | 'server' | 'client') {
switch (category) {
case 'server': return <Server className="w-4 h-4" />
Expand Down Expand Up @@ -260,7 +265,7 @@ function ExtensionCard({ item, expanded, onToggleExpand, onToggleEnabled, onConf
)
}

export default function ExtensionsView({ onNavigate }: ExtensionsViewProps) {
export function ExtensionsManager({ className, includeCli = true }: ExtensionsManagerProps = {}) {
useEnsureExtensionsRegistry()

const dispatch = useAppDispatch()
Expand Down Expand Up @@ -385,16 +390,65 @@ export default function ExtensionsView({ onNavigate }: ExtensionsViewProps) {
}
}, [dispatch, scheduleCwdValidation, scheduleTextSave])

const visibleItems = useMemo(
() => includeCli ? items : items.filter((item) => item.kind !== 'cli'),
[includeCli, items],
)

const groups = useMemo(() => {
const cli = items.filter((i) => i.kind === 'cli')
const server = items.filter((i) => i.kind === 'server')
const client = items.filter((i) => i.kind === 'client')
const cli = visibleItems.filter((i) => i.kind === 'cli')
const server = visibleItems.filter((i) => i.kind === 'server')
const client = visibleItems.filter((i) => i.kind === 'client')
return [
{ kind: 'cli' as const, items: cli },
{ kind: 'server' as const, items: server },
{ kind: 'client' as const, items: client },
].filter((g) => g.items.length > 0)
}, [items])
}, [visibleItems])

return (
<div className={cn('space-y-6', className)}>
{visibleItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Puzzle className="w-12 h-12 mb-4 opacity-50" />
<p className="text-lg font-medium">No extensions installed</p>
{includeCli && (
<p className="text-sm mt-1">
Drop a directory with a <code className="rounded bg-muted px-1 py-0.5 text-xs">freshell.json</code> into <code className="rounded bg-muted px-1 py-0.5 text-xs">~/.freshell/extensions/</code> and restart.
</p>
)}
</div>
) : (
groups.map((group) => (
<div key={group.kind}>
<h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
{groupLabel(group.kind)}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{group.items.map((item) => (
<ExtensionCard
key={item.id}
item={item}
expanded={expandedCards.has(item.id)}
onToggleExpand={() => toggleExpand(item.id)}
onToggleEnabled={handleToggleEnabled}
onConfigChange={handleConfigChange}
cwdDrafts={cwdDrafts}
cwdErrors={cwdErrors}
/>
))}
</div>
</div>
))
)}
</div>
)
}

export default function ExtensionsView({ onNavigate }: ExtensionsViewProps) {
useEnsureExtensionsRegistry()

const items = useAppSelector(selectManagedItems)

return (
<div className="h-full flex flex-col">
Expand All @@ -419,38 +473,8 @@ export default function ExtensionsView({ onNavigate }: ExtensionsViewProps) {

{/* Content */}
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-3xl px-3 py-4 md:px-6 md:py-6 space-y-6">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Puzzle className="w-12 h-12 mb-4 opacity-50" />
<p className="text-lg font-medium">No extensions installed</p>
<p className="text-sm mt-1">
Drop a directory with a <code className="rounded bg-muted px-1 py-0.5 text-xs">freshell.json</code> into <code className="rounded bg-muted px-1 py-0.5 text-xs">~/.freshell/extensions/</code> and restart.
</p>
</div>
) : (
groups.map((group) => (
<div key={group.kind}>
<h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
{groupLabel(group.kind)}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{group.items.map((item) => (
<ExtensionCard
key={item.id}
item={item}
expanded={expandedCards.has(item.id)}
onToggleExpand={() => toggleExpand(item.id)}
onToggleEnabled={handleToggleEnabled}
onConfigChange={handleConfigChange}
cwdDrafts={cwdDrafts}
cwdErrors={cwdErrors}
/>
))}
</div>
</div>
))
)}
<div className="mx-auto w-full max-w-3xl px-3 py-4 md:px-6 md:py-6">
<ExtensionsManager />
</div>
</div>
</div>
Expand Down
47 changes: 16 additions & 31 deletions src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,30 @@ import type {
ServerSettingsPatch,
} from '@/store/types'
import type { AppView } from '@/components/Sidebar'
import { useEnsureExtensionsRegistry } from '@/hooks/useEnsureExtensionsRegistry'
import { Puzzle, ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'
import AppearanceSettings from '@/components/settings/AppearanceSettings'
import WorkspaceSettings from '@/components/settings/WorkspaceSettings'
import SafetySettings from '@/components/settings/SafetySettings'
import AdvancedSettings from '@/components/settings/AdvancedSettings'
import AISettings from '@/components/settings/AISettings'
import CodingAgentsSettings from '@/components/settings/CodingAgentsSettings'
import PanesSettings from '@/components/settings/PanesSettings'
import NamingSettings from '@/components/settings/NamingSettings'
import NetworkSettings from '@/components/settings/NetworkSettings'

const SERVER_TEXT_SETTINGS_DEBOUNCE_MS = 500

const sections = [
{ id: 'appearance', label: 'Appearance' },
{ id: 'coding-agents', label: 'Coding Agents' },
{ id: 'panes', label: 'Panes' },
{ id: 'workspace', label: 'Workspace' },
{ id: 'ai', label: 'AI' },
{ id: 'safety', label: 'Safety' },
{ id: 'naming', label: 'Naming' },
{ id: 'network', label: 'Network' },
{ id: 'advanced', label: 'Advanced' },
] as const

type SectionId = typeof sections[number]['id']

export default function SettingsView({ onNavigate, onFirewallTerminal, onSharePanel }: { onNavigate?: (view: AppView) => void; onFirewallTerminal?: (cmd: { tabId: string; command: string }) => void; onSharePanel?: () => void } = {}) {
useEnsureExtensionsRegistry()

const dispatch = useAppDispatch()
const rawSettings = useAppSelector((s) => s.settings.settings)
const settings = useMemo(
Expand Down Expand Up @@ -111,26 +111,9 @@ export default function SettingsView({ onNavigate, onFirewallTerminal, onSharePa

{/* Content */}
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-2xl px-3 py-4 md:px-6 md:py-6 space-y-6">

{/* Manage Extensions — prominent, always visible */}
<button
onClick={() => onNavigate?.('extensions')}
className="w-full flex items-center justify-between rounded-lg border border-border/40 bg-card px-4 py-3 text-left hover:bg-muted/50 transition-colors"
aria-label="Manage extensions"
>
<div className="flex items-center gap-3">
<Puzzle className="w-5 h-5 text-muted-foreground" />
<div>
<div className="text-sm font-medium">Manage Extensions</div>
<div className="text-xs text-muted-foreground">View and configure installed extensions</div>
</div>
</div>
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</button>

<div className="mx-auto w-full max-w-3xl px-3 py-4 md:px-6 md:py-6 space-y-6">
{/* Tabs */}
<div className="flex gap-1 border-b border-border/30 -mx-1" role="tablist" aria-label="Settings sections">
<div className="flex flex-wrap gap-x-1 gap-y-1 border-b border-border/30 -mx-1" role="tablist" aria-label="Settings sections">
{sections.map((section) => (
<button
key={section.id}
Expand All @@ -150,12 +133,14 @@ export default function SettingsView({ onNavigate, onFirewallTerminal, onSharePa
</div>

{/* Tab content — only the active section renders */}
<div role="tabpanel" aria-label={`${activeSection} settings`}>
<div role="tabpanel" aria-label={`${activeSection} settings`} className="space-y-6">
{activeSection === 'appearance' && <AppearanceSettings {...sectionProps} />}
{activeSection === 'coding-agents' && <CodingAgentsSettings {...sectionProps} />}
{activeSection === 'panes' && <PanesSettings {...sectionProps} />}
{activeSection === 'workspace' && <WorkspaceSettings {...sectionProps} />}
{activeSection === 'ai' && <AISettings {...sectionProps} />}
{activeSection === 'safety' && (
<SafetySettings
{activeSection === 'naming' && <NamingSettings {...sectionProps} />}
{activeSection === 'network' && (
<NetworkSettings
{...sectionProps}
onNavigate={onNavigate}
onFirewallTerminal={onFirewallTerminal}
Expand Down
55 changes: 34 additions & 21 deletions src/components/fresh-agent/FreshAgentComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
useState,
type KeyboardEvent,
} from 'react'
import { Command, File, Folder, Loader2, Paperclip, Send, Square, X } from 'lucide-react'
import { File, Folder, ListStart, Loader2, Paperclip, Send, Square, X } from 'lucide-react'
import { api } from '@/lib/api'
import { getAuthToken } from '@/lib/auth'
import { useCoarsePointer } from '@/lib/pointer'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import type { FreshAgentSlashCommand } from '@shared/fresh-agent-slash-commands'

export type FreshAgentAttachment = {
Expand Down Expand Up @@ -703,26 +704,38 @@ export const FreshAgentComposer = forwardRef<FreshAgentComposerHandle, FreshAgen
>
<Square className="h-4 w-4" />
</button>
<button
type="button"
disabled={commands.length === 0}
className="fresh-agent-composer-action inline-flex h-11 w-11 items-center justify-center rounded-md border border-border/70 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:h-9 sm:w-9"
aria-label="Slash commands"
onClick={() => {
setMenuMode((mode) => mode === 'browse' ? null : 'browse')
setFilter('')
}}
>
<Command className="h-4 w-4" />
</button>
<button
type="submit"
disabled={disabled}
className="fresh-agent-composer-action inline-flex h-11 w-11 items-center justify-center rounded-md bg-primary text-sm font-medium text-primary-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:w-10"
aria-label="Send"
>
<Send className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
disabled={commands.length === 0}
className="fresh-agent-composer-action inline-flex h-11 w-11 items-center justify-center rounded-md border border-border/70 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:h-9 sm:w-9"
aria-label="Slash commands"
title="Slash commands"
onClick={() => {
setMenuMode((mode) => mode === 'browse' ? null : 'browse')
setFilter('')
}}
>
<ListStart className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent align="end">Slash commands</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="submit"
disabled={disabled}
className="fresh-agent-composer-action inline-flex h-11 w-11 items-center justify-center rounded-md bg-primary text-sm font-medium text-primary-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:w-10"
aria-label="Send"
title="Send message"
>
<Send className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent align="end">Send message</TooltipContent>
</Tooltip>
</div>
<input
ref={fileInputRef}
Expand Down
2 changes: 1 addition & 1 deletion src/components/fresh-agent/FreshAgentItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function itemToToolDisplay(item: FreshAgentTranscriptItem): FreshAgentToo
function renderText(text: string, markdown = false) {
const visibleText = stripSystemReminders(text)
if (!visibleText) return null
const plain = <p className="whitespace-pre-wrap break-words">{visibleText}</p>
const plain = <p className="whitespace-pre-wrap break-words leading-[inherit]">{visibleText}</p>
if (!markdown) return plain
return (
<div
Expand Down
2 changes: 1 addition & 1 deletion src/components/fresh-agent/FreshAgentTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ function FreshAgentTurnArticle({
}
return <FreshAgentItemCard key={block.item.id} item={block.item} markdown={!isUser} />
}) : isUser ? (
<p className="whitespace-pre-wrap break-words">{stripSystemReminders(turn.summary)}</p>
<p className="whitespace-pre-wrap break-words leading-[inherit]">{stripSystemReminders(turn.summary)}</p>
) : (
// Summary-only agent turns went through the plain-text path and
// showed literal backticks (live-test finding) — render markdown.
Expand Down
6 changes: 6 additions & 0 deletions src/components/panes/PanePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ function isWindowsLike(platform: string | null): boolean {
return platform === 'win32' || platform === 'wsl'
}

function isFreshAgentSessionDisabled(sessionType: string, disabledItems: readonly string[]): boolean {
return disabledItems.includes(sessionType)
}

interface PanePickerProps {
onSelect: (type: PanePickerType) => void
onCancel: () => void
Expand Down Expand Up @@ -119,6 +123,7 @@ export default function PanePicker({ onSelect, onCancel, isOnlyPane, tabId, pane
// Fresh-agent provider options: only show if underlying CLI is available, enabled, and not hidden by feature flag
const visibleFreshAgentConfigs = freshClientsEnabled ? getVisibleFreshAgentConfigs(featureFlags) : []
const freshAgentProviderOptions: PickerOption[] = visibleFreshAgentConfigs
.filter((config) => !isFreshAgentSessionDisabled(config.name, disabledExtensions))
.filter((config) => availableClis[config.codingCliProvider] && enabledProviders.includes(config.codingCliProvider) && !disabledExtensions.includes(config.codingCliProvider))
.map((config) => ({
type: config.name as PanePickerType,
Expand All @@ -131,6 +136,7 @@ export default function PanePicker({ onSelect, onCancel, isOnlyPane, tabId, pane
? FRESH_AGENT_REGISTRY
.filter((entry) => !visibleFreshAgentConfigs.some((config) => config.name === entry.sessionType))
.filter((entry) => !entry.disabled)
.filter((entry) => !isFreshAgentSessionDisabled(entry.sessionType, disabledExtensions))
.filter((entry) => !entry.hidden || featureFlags[entry.featureFlag ?? entry.sessionType] === true)
.filter((entry) => availableClis[entry.runtimeProvider] && enabledProviders.includes(entry.runtimeProvider) && !disabledExtensions.includes(entry.runtimeProvider))
.map((entry) => ({
Expand Down
Loading
Loading