Skip to content

Commit bc3d279

Browse files
committed
feat: view cycle toggle — Chat → Editor → Terminal pill bar
Replaced mobile bottom tab grid with clean 3-pill cycle toggle: - Chat (with grad bg), Editor, Terminal as primary cycle - Active pill: brand color fill + label expansion animation - Inactive: icon only, subtle text color - More button (···) opens settings - Clean, iOS-native feel with rounded-full pills - Added kanban + mcp to VIEW_ICONS
1 parent 558732b commit bc3d279

8 files changed

Lines changed: 56 additions & 103 deletions

File tree

app/page.tsx

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,15 @@ const VIEW_ICONS: Record<string, { icon: string; label: string }> = {
8787
git: { icon: 'lucide:git-branch', label: 'Git' },
8888
skills: { icon: 'lucide:sparkles', label: 'Skills' },
8989
prompts: { icon: 'lucide:book-open', label: 'Prompts' },
90-
mcp: { icon: 'lucide:plug', label: 'MCP' },
9190
settings: { icon: 'lucide:settings', label: 'Settings' },
9291
terminal: { icon: 'lucide:terminal', label: 'Terminal' },
92+
kanban: { icon: 'lucide:kanban', label: 'Kanban' },
93+
mcp: { icon: 'lucide:plug', label: 'MCP' },
9394
}
9495

96+
/** Primary view cycle: Chat → Editor → Terminal */
97+
const VIEW_CYCLE: ViewId[] = ['chat', 'editor', 'terminal']
98+
9599
const MODE_BUTTONS: Array<{ id: AppMode; icon: string; label: string }> = [
96100
{ id: 'classic', icon: 'lucide:code-2', label: 'Classic' },
97101
{ id: 'chat', icon: 'lucide:message-square', label: 'Chat' },
@@ -133,7 +137,7 @@ export default function EditorLayout() {
133137
const mobileViewTabs = useMemo(() => {
134138
// On mobile, curate tabs to useful views + always include settings
135139
const mobile = visibleViews.filter(
136-
(v) => !['preview', 'diff', 'skills', 'prompts', 'mcp'].includes(v),
140+
(v) => !['preview', 'diff', 'skills', 'prompts'].includes(v),
137141
)
138142
if (!mobile.includes('terminal')) mobile.push('terminal')
139143
return mobile.slice(0, 5)
@@ -746,43 +750,50 @@ export default function EditorLayout() {
746750
overscrollBehavior: 'none',
747751
}}
748752
>
749-
<div
750-
className="grid"
751-
style={{
752-
gridTemplateColumns: `repeat(${mobileViewTabs.length}, minmax(0, 1fr))`,
753-
minHeight: 49,
754-
}}
755-
>
756-
{mobileViewTabs.map((v) => {
753+
{/* Primary view cycle toggle: Chat → Editor → Terminal */}
754+
<div className="flex items-center justify-center gap-1 px-3 py-2">
755+
{VIEW_CYCLE.map((v) => {
757756
const isActive = activeView === v
757+
const meta = VIEW_ICONS[v]
758758
return (
759759
<motion.button
760760
key={v}
761761
type="button"
762-
onClick={() => {
763-
setView(v)
764-
}}
765-
whileTap={{ scale: 0.92 }}
766-
className={`relative mx-1 my-1 flex min-w-0 flex-col items-center gap-0.5 rounded-xl border py-2 text-[10px] font-medium transition-colors touch-manipulation ${
762+
onClick={() => setView(v)}
763+
whileTap={{ scale: 0.95 }}
764+
className={`relative flex items-center gap-2 px-5 py-2.5 rounded-full text-[13px] font-medium transition-all duration-200 touch-manipulation ${
767765
isActive
768-
? 'border-[color-mix(in_srgb,var(--brand)_28%,var(--border))] bg-[color-mix(in_srgb,var(--brand)_10%,transparent)] text-[var(--brand)] shadow-[0_8px_20px_color-mix(in_srgb,var(--brand)_10%,transparent)]'
769-
: 'border-transparent text-[var(--text-disabled)]'
770-
} ${flashedTab === v ? 'animate-badge-pop' : ''}`}
771-
title={VIEW_ICONS[v].label}
772-
style={{ minHeight: 44, WebkitTapHighlightColor: 'transparent' }}
766+
? 'bg-[var(--brand)] text-[var(--brand-contrast)] shadow-[0_4px_16px_color-mix(in_srgb,var(--brand)_30%,transparent)]'
767+
: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'
768+
}`}
769+
style={{ WebkitTapHighlightColor: 'transparent' }}
773770
>
774-
<span className="relative">
775-
<Icon icon={VIEW_ICONS[v].icon} width={24} height={24} />
776-
{v === 'git' && dirtyCount > 0 && (
777-
<span className="absolute -right-2 -top-1 min-w-[14px] rounded-full bg-[var(--brand)] px-0.5 text-center text-[8px] font-bold leading-[14px] text-[var(--brand-contrast)]">
778-
{dirtyCount > 9 ? '9+' : dirtyCount}
779-
</span>
780-
)}
781-
</span>
782-
<span className="max-w-full truncate">{VIEW_ICONS[v].label}</span>
771+
<Icon icon={meta.icon} width={18} height={18} />
772+
{isActive && (
773+
<motion.span
774+
initial={{ width: 0, opacity: 0 }}
775+
animate={{ width: 'auto', opacity: 1 }}
776+
exit={{ width: 0, opacity: 0 }}
777+
transition={{ duration: 0.2 }}
778+
className="overflow-hidden whitespace-nowrap"
779+
>
780+
{meta.label}
781+
</motion.span>
782+
)}
783783
</motion.button>
784784
)
785785
})}
786+
787+
{/* More views overflow */}
788+
<motion.button
789+
type="button"
790+
onClick={() => setView('settings')}
791+
whileTap={{ scale: 0.95 }}
792+
className="flex items-center justify-center w-10 h-10 rounded-full text-[var(--text-disabled)] hover:text-[var(--text-secondary)] transition-colors touch-manipulation"
793+
style={{ WebkitTapHighlightColor: 'transparent' }}
794+
>
795+
<Icon icon="lucide:more-horizontal" width={18} height={18} />
796+
</motion.button>
786797
</div>
787798
</div>
788799
)}

components/agent-panel.tsx

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
375375
const [confirmClear, setConfirmClear] = useState(false)
376376

377377
// ─── Inline picker state ──────────────────────────────────────
378-
const [activePicker, setActivePicker] = useState<'skill' | 'mcp' | 'prompt' | null>(null)
378+
const [activePicker, setActivePicker] = useState<'skill' | 'prompt' | null>(null)
379379
const [pickerQuery, setPickerQuery] = useState('')
380380
const [pickerIndex, setPickerIndex] = useState(0)
381381
const [sending, setSending] = useState(false)
@@ -1950,12 +1950,6 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
19501950
setPickerQuery(query)
19511951
return
19521952
}
1953-
if (input === '/mcp' || input.startsWith('/mcp ')) {
1954-
const query = input.replace(/^\/mcp\s*/, '')
1955-
setActivePicker('mcp')
1956-
setPickerQuery(query)
1957-
return
1958-
}
19591953
if (input === '/prompt' || input.startsWith('/prompt ')) {
19601954
const query = input.replace(/^\/prompt\s*/, '')
19611955
setActivePicker('prompt')
@@ -1975,9 +1969,6 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
19751969
if (input === '/skill' || input === '/skill ' || input.startsWith('/skill use') || input === '/skill use') {
19761970
return []
19771971
}
1978-
if (input === '/mcp' || input.startsWith('/mcp ')) {
1979-
return []
1980-
}
19811972
if (input === '/prompt' || input.startsWith('/prompt ')) {
19821973
return []
19831974
}
@@ -2020,7 +2011,6 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
20202011
{ cmd: '/skill', desc: 'Open skill commands', icon: 'lucide:sparkles' },
20212012
{ cmd: '/skill find', desc: 'Search for more skills', icon: 'lucide:search' },
20222013
{ cmd: '/skill use', desc: 'Apply a bundled skill', icon: 'lucide:play' },
2023-
{ cmd: '/mcp', desc: 'Select an MCP server', icon: 'lucide:plug' },
20242014
{ cmd: '/prompt', desc: 'Use a prompt template', icon: 'lucide:book-open' },
20252015
]
20262016
const term = input.toLowerCase()
@@ -2056,25 +2046,6 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
20562046
]
20572047
}, [])
20582048

2059-
const mcpPickerItems = useMemo<PickerItem[]>(() => {
2060-
const stored = typeof window !== 'undefined' ? localStorage.getItem('knot-code:mcp:servers') : null
2061-
if (stored) {
2062-
try {
2063-
const servers = JSON.parse(stored) as Array<{ id: string; name: string; type: string; enabled: boolean }>
2064-
return servers.map((s) => ({
2065-
id: s.id,
2066-
name: s.name,
2067-
description: `${s.type} server`,
2068-
icon: 'lucide:plug',
2069-
enabled: s.enabled,
2070-
}))
2071-
} catch {
2072-
// Ignore parse errors, return empty array
2073-
}
2074-
}
2075-
return []
2076-
}, [])
2077-
20782049
const promptPickerItems = useMemo<PickerItem[]>(() => {
20792050
return [
20802051
{ id: 'explain-like-5', name: 'Explain Like I\'m 5', description: 'Simple explanation of complex topics', icon: 'lucide:baby' },
@@ -2092,8 +2063,6 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
20922063
const handlePickerSelect = useCallback((item: PickerItem) => {
20932064
if (activePicker === 'skill') {
20942065
setInput(`/skill use ${item.id} `)
2095-
} else if (activePicker === 'mcp') {
2096-
setInput(`/mcp ${item.id} `)
20972066
} else if (activePicker === 'prompt') {
20982067
// For prompt templates, replace the command with the template name or insert it
20992068
setInput(`Use the "${item.name}" template: `)
@@ -2112,14 +2081,12 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
21122081

21132082
const currentPickerItems = useMemo(() => {
21142083
if (activePicker === 'skill') return skillPickerItems
2115-
if (activePicker === 'mcp') return mcpPickerItems
21162084
if (activePicker === 'prompt') return promptPickerItems
21172085
return []
2118-
}, [activePicker, skillPickerItems, mcpPickerItems, promptPickerItems])
2086+
}, [activePicker, skillPickerItems, promptPickerItems])
21192087

21202088
const pickerTitle = useMemo(() => {
21212089
if (activePicker === 'skill') return 'Select Skill'
2122-
if (activePicker === 'mcp') return 'Select MCP Server'
21232090
if (activePicker === 'prompt') return 'Select Prompt Template'
21242091
return ''
21252092
}, [activePicker])
@@ -2129,21 +2096,11 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
21292096
icon: 'lucide:sparkles',
21302097
heading: 'Getting Started with Skills',
21312098
steps: [
2132-
'Open Skills view (⌘6)',
2099+
'Open Skills view (⌘5)',
21332100
'Enable skills from the catalog',
21342101
'Skills will appear here once active',
21352102
],
21362103
}
2137-
if (activePicker === 'mcp') return {
2138-
icon: 'lucide:plug',
2139-
heading: 'Getting Started with MCP',
2140-
steps: [
2141-
'Open MCP Library (⌘5)',
2142-
'Browse and install a server',
2143-
'Configure API keys if needed',
2144-
'Come back here to use it',
2145-
],
2146-
}
21472104
if (activePicker === 'prompt') return {
21482105
icon: 'lucide:book-open',
21492106
heading: 'Create Your First Prompt',

components/chat/chat-input-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ interface ChatInputBarProps {
6464
setAtMenuIdx: (v: number | ((i: number) => number)) => void
6565
setAtQuery: (q: string) => void
6666
selectAtFile: (path: string) => void
67-
activePicker: 'skill' | 'mcp' | 'prompt' | null
67+
activePicker: 'skill' | 'prompt' | null
6868
pickerItems: PickerItem[]
6969
pickerQuery: string
7070
pickerIndex: number

components/plugins/spotify/spotify-player.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,12 @@ export function SpotifyPlayer() {
507507
)}
508508

509509
{/* ─── Now playing ─── */}
510-
<div className="flex-1 flex flex-col">
510+
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto">
511511
{track ? (
512512
<div className="flex flex-col gap-2 p-3">
513513
{/* Album art */}
514514
{albumArt && (
515-
<div className="w-full aspect-square rounded-lg overflow-hidden bg-[var(--bg-subtle)]">
515+
<div className="w-full max-h-[200px] rounded-lg overflow-hidden bg-[var(--bg-subtle)]">
516516
<img
517517
src={albumArt}
518518
alt={track.album.name}

components/plugins/widget-pip-window.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export function WidgetPipWindow() {
235235

236236
{/* Plugin content */}
237237
<div
238-
className="w-full overflow-hidden bg-[var(--bg)]"
238+
className="w-full overflow-y-auto bg-[var(--bg)]"
239239
style={{
240240
height: size.h - 28,
241241
pointerEvents: isDragging || isResizing ? 'none' : 'auto',

components/view-router.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ const SkillsView = dynamic(
2121
() => import('@/components/views/skills-view').then((m) => m.SkillsView),
2222
{ ssr: false },
2323
)
24-
const McpLibraryView = dynamic(
25-
() => import('@/components/views/mcp-library-view').then((m) => m.McpLibraryView),
26-
{ ssr: false },
27-
)
2824
const PromptLibraryView = dynamic(
2925
() => import('@/components/views/prompt-library-view').then((m) => m.PromptLibraryView),
3026
{ ssr: false },
@@ -57,7 +53,6 @@ const VIEW_ICONS: Record<string, { label: string }> = {
5753
kanban: { label: 'Kanban' },
5854
skills: { label: 'Skills' },
5955
prompts: { label: 'Prompts' },
60-
mcp: { label: 'MCP Library' },
6156
settings: { label: 'Settings' },
6257
terminal: { label: 'Terminal' },
6358
}
@@ -136,7 +131,6 @@ export function ViewRouter() {
136131
{activeView === 'kanban' && <KanbanView />}
137132
{activeView === 'skills' && <SkillsView />}
138133
{activeView === 'prompts' && <PromptLibraryView />}
139-
{activeView === 'mcp' && <McpLibraryView />}
140134
{activeView === 'settings' && <SettingsPanel onBack={() => setView('chat')} />}
141135
{activeView === 'terminal' && (
142136
<div className="flex-1 flex min-h-0 min-w-0 overflow-hidden">

components/workspace-sidebar.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -190,34 +190,26 @@ export function WorkspaceSidebar({ collapsed, onToggle, repoName }: Props) {
190190
)}
191191
</button>
192192

193-
<button
194-
onClick={() => setView('mcp')}
195-
className={`activity-bar-btn ${activeView === 'mcp' ? 'activity-bar-btn--active' : ''}`}
196-
title="MCP (⌘5)"
197-
>
198-
<Icon icon="lucide:plug" width={24} height={24} />
199-
</button>
200-
201193
<button
202194
onClick={() => setView('skills')}
203195
className={`activity-bar-btn ${activeView === 'skills' ? 'activity-bar-btn--active' : ''}`}
204-
title="Skills (⌘6)"
196+
title="Skills (⌘5)"
205197
>
206198
<Icon icon="lucide:wand-2" width={24} height={24} />
207199
</button>
208200

209201
<button
210202
onClick={() => setView('prompts')}
211203
className={`activity-bar-btn ${activeView === 'prompts' ? 'activity-bar-btn--active' : ''}`}
212-
title="Prompts (⌘7)"
204+
title="Prompts (⌘6)"
213205
>
214206
<Icon icon="lucide:book-open" width={24} height={24} />
215207
</button>
216208

217209
<button
218210
onClick={() => setView('kanban')}
219211
className={`activity-bar-btn ${activeView === 'kanban' ? 'activity-bar-btn--active' : ''}`}
220-
title="Kanban (⌘8)"
212+
title="Kanban (⌘7)"
221213
>
222214
<Icon icon="lucide:kanban" width={24} height={24} />
223215
</button>
@@ -296,10 +288,9 @@ export function WorkspaceSidebar({ collapsed, onToggle, repoName }: Props) {
296288
{ id: 'editor' as const, icon: 'lucide:code', label: 'Editor', shortcut: '⌘2' },
297289
{ id: 'preview' as const, icon: 'lucide:eye', label: 'Preview', shortcut: '⌘3' },
298290
{ id: 'git' as const, icon: 'lucide:git-branch', label: 'Git', shortcut: '⌘4' },
299-
{ id: 'mcp' as const, icon: 'lucide:plug', label: 'MCP', shortcut: '⌘5' },
300-
{ id: 'skills' as const, icon: 'lucide:wand-2', label: 'Skills', shortcut: '⌘6' },
301-
{ id: 'prompts' as const, icon: 'lucide:book-open', label: 'Prompts', shortcut: '⌘7' },
302-
{ id: 'kanban' as const, icon: 'lucide:kanban', label: 'Kanban', shortcut: '⌘8' },
291+
{ id: 'skills' as const, icon: 'lucide:wand-2', label: 'Skills', shortcut: '⌘5' },
292+
{ id: 'prompts' as const, icon: 'lucide:book-open', label: 'Prompts', shortcut: '⌘6' },
293+
{ id: 'kanban' as const, icon: 'lucide:kanban', label: 'Kanban', shortcut: '⌘7' },
303294
] as const).map((item) => (
304295
<button
305296
key={item.id}

lib/mode-registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const MODE_REGISTRY: Record<AppMode, ModeSpec> = {
2525
id: 'classic',
2626
label: 'Classic',
2727
description: 'Traditional editor — no chat, files open',
28-
visibleViews: ['editor', 'preview', 'git', 'skills', 'prompts', 'mcp'],
28+
visibleViews: ['editor', 'preview', 'git', 'skills', 'prompts'],
2929
defaultView: 'editor',
3030
autoExpandEditor: true,
3131
accent: '#d2a34f',
@@ -43,7 +43,7 @@ export const MODE_REGISTRY: Record<AppMode, ModeSpec> = {
4343
id: 'chat',
4444
label: 'Chat',
4545
description: 'Chat-first mode with optional editor and git views',
46-
visibleViews: ['chat', 'editor', 'git', 'skills', 'prompts', 'mcp'],
46+
visibleViews: ['chat', 'editor', 'git', 'skills', 'prompts'],
4747
defaultView: 'chat',
4848
hideTabs: true,
4949
accent: '#86b5ff',
@@ -61,7 +61,7 @@ export const MODE_REGISTRY: Record<AppMode, ModeSpec> = {
6161
id: 'tui',
6262
label: 'TUI',
6363
description: 'Terminal-first — editor available on demand',
64-
visibleViews: ['chat', 'editor', 'git', 'skills', 'prompts', 'mcp'],
64+
visibleViews: ['chat', 'editor', 'git', 'skills', 'prompts'],
6565
defaultView: 'editor',
6666
terminalCenter: true,
6767
hideTabs: true,

0 commit comments

Comments
 (0)