mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 10:08:38 +02:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user