Merge pull request #11 from multica-ai/forrestchang/folder-select-ui

feat(ui): add inline folder selector to message input
This commit is contained in:
Jiayuan
2026-01-15 02:21:40 +08:00
committed by GitHub
4 changed files with 82 additions and 23 deletions

View File

@@ -8,7 +8,6 @@ import { AppSidebar } from './components/AppSidebar'
import { Modals } from './components/Modals'
import { ThemeProvider } from './contexts/ThemeContext'
import { SidebarProvider } from '@/components/ui/sidebar'
import { useModalStore } from './stores/modalStore'
import { useUIStore } from './stores/uiStore'
import {
RightPanel,
@@ -32,13 +31,12 @@ function AppContent(): React.JSX.Element {
createSession,
selectSession,
deleteSession,
clearCurrentSession,
sendPrompt,
cancelRequest,
clearError,
} = useApp()
const openModal = useModalStore((s) => s.openModal)
// UI state
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
@@ -47,7 +45,7 @@ function AppContent(): React.JSX.Element {
const [defaultAgentId, setDefaultAgentId] = useState('opencode')
const handleNewSession = () => {
openModal('newSession')
clearCurrentSession()
}
const handleCreateSession = async (cwd: string) => {
@@ -55,6 +53,13 @@ function AppContent(): React.JSX.Element {
await createSession(cwd, defaultAgentId)
}
const handleSelectFolder = async () => {
const dir = await window.electronAPI.selectDirectory()
if (dir) {
await createSession(dir, defaultAgentId)
}
}
const handleSelectSession = async (sessionId: string) => {
// Select session (agent starts automatically via resumeSession)
await selectSession(sessionId)
@@ -105,7 +110,6 @@ function AppContent(): React.JSX.Element {
updates={sessionUpdates}
isProcessing={isProcessing}
hasSession={!!currentSession}
onNewSession={handleNewSession}
/>
{/* Input */}
@@ -114,6 +118,8 @@ function AppContent(): React.JSX.Element {
onCancel={cancelRequest}
isProcessing={isProcessing}
disabled={!currentSession}
workingDirectory={currentSession?.workingDirectory}
onSelectFolder={handleSelectFolder}
/>
</main>

View File

@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import type { StoredSessionUpdate } from '../../../shared/types'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { ChevronDown } from 'lucide-react'
import { ToolCallItem, type ToolCall } from './ToolCallItem'
@@ -16,10 +15,9 @@ interface ChatViewProps {
updates: StoredSessionUpdate[]
isProcessing: boolean
hasSession: boolean
onNewSession?: () => void
}
export function ChatView({ updates, isProcessing, hasSession, onNewSession }: ChatViewProps) {
export function ChatView({ updates, isProcessing, hasSession }: ChatViewProps) {
const bottomRef = useRef<HTMLDivElement>(null)
const pendingPermission = usePermissionStore((s) => s.pendingRequest)
@@ -36,16 +34,11 @@ export function ChatView({ updates, isProcessing, hasSession, onNewSession }: Ch
<div className="flex flex-1 items-center justify-center">
<div className="text-center">
<h1 className="mb-2 text-3xl font-bold">Multica</h1>
<p className="mb-4 text-muted-foreground">
<p className="text-muted-foreground">
{hasSession
? 'Start a conversation with your coding agent'
: 'Create a session to start chatting'}
: 'Select a folder below to start'}
</p>
{!hasSession && onNewSession && (
<Button onClick={onNewSession}>
New Session
</Button>
)}
</div>
</div>
)

View File

@@ -2,7 +2,7 @@
* Message input component
*/
import { useState, useRef, useEffect } from 'react'
import { ArrowUp, Square } from 'lucide-react'
import { ArrowUp, Square, Folder } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface MessageInputProps {
@@ -11,6 +11,8 @@ interface MessageInputProps {
isProcessing: boolean
disabled: boolean
placeholder?: string
workingDirectory?: string | null
onSelectFolder: () => Promise<void>
}
export function MessageInput({
@@ -19,11 +21,18 @@ export function MessageInput({
isProcessing,
disabled,
placeholder = 'Type a message...',
workingDirectory,
onSelectFolder,
}: MessageInputProps) {
const [value, setValue] = useState('')
const [isComposing, setIsComposing] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
// Get folder name from path
const folderName = workingDirectory
? workingDirectory.split('/').filter(Boolean).pop() || workingDirectory
: null
// Auto-resize textarea
useEffect(() => {
const textarea = textareaRef.current
@@ -55,11 +64,46 @@ export function MessageInput({
}
const canSubmit = !disabled && value.trim().length > 0
const hasFolder = !!workingDirectory
// Render folder selection mode when no folder is selected
if (!hasFolder) {
return (
<div className="p-4">
<div className="mx-auto max-w-3xl">
<div className="bg-secondary/50 hover:bg-secondary transition-colors duration-200 rounded-xl p-3 border border-border">
{/* Folder selection prompt */}
<div className="flex items-center gap-3">
<Folder className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<span className="text-muted-foreground flex-1">Select a folder to start...</span>
<Button
variant="outline"
size="sm"
onClick={onSelectFolder}
className="flex-shrink-0"
>
Browse
</Button>
</div>
{/* Bottom toolbar */}
<div className="flex items-center justify-end pt-3 mt-3 border-t border-border/50">
<Button size="icon" disabled className="h-8 w-8 rounded-full">
<ArrowUp className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
)
}
// Normal chat input mode
return (
<div className="p-4">
<div className="mx-auto max-w-3xl">
<div className="bg-secondary/50 hover:bg-secondary focus-within:bg-secondary transition-colors duration-200 rounded-md p-2 border border-border">
<div className="bg-secondary/50 hover:bg-secondary focus-within:bg-secondary transition-colors duration-200 rounded-xl p-3 border border-border">
{/* Text input */}
<textarea
ref={textareaRef}
value={value}
@@ -67,12 +111,24 @@ export function MessageInput({
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
placeholder={disabled ? 'Select or create a session first' : placeholder}
placeholder={placeholder}
disabled={disabled}
rows={1}
className="w-full resize-none bg-transparent px-2 py-1 text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
className="w-full resize-none bg-transparent px-1 py-1 text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
/>
<div className="flex justify-end pt-1">
{/* Bottom toolbar */}
<div className="flex items-center justify-between pt-2 mt-2 border-t border-border/50">
{/* Folder indicator */}
<button
onClick={onSelectFolder}
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-background/50"
>
<Folder className="h-3.5 w-3.5" />
<span className="max-w-[150px] truncate">{folderName}</span>
</button>
{/* Send button */}
<Button
size="icon"
onClick={isProcessing ? onCancel : handleSubmit}
@@ -87,9 +143,6 @@ export function MessageInput({
</Button>
</div>
</div>
<div className="mt-1 text-center text-xs text-muted-foreground">
Enter to send, Shift+Enter for new line
</div>
</div>
</div>
)

View File

@@ -29,6 +29,7 @@ export interface AppActions {
createSession: (cwd: string, agentId: string) => Promise<void>
selectSession: (sessionId: string) => Promise<void>
deleteSession: (sessionId: string) => Promise<void>
clearCurrentSession: () => void
// Agent actions (per-session)
sendPrompt: (content: string) => Promise<void>
@@ -224,6 +225,11 @@ export function useApp(): AppState & AppActions {
}
}, [currentSession, loadSessions, loadRunningStatus])
const clearCurrentSession = useCallback(() => {
setCurrentSession(null)
setSessionUpdates([])
}, [])
const sendPrompt = useCallback(async (content: string) => {
if (!currentSession) {
setError('No active session')
@@ -282,6 +288,7 @@ export function useApp(): AppState & AppActions {
createSession,
selectSession,
deleteSession,
clearCurrentSession,
sendPrompt,
cancelRequest,
clearError,