|
|
@@ -15,13 +15,16 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
import {
|
|
|
ArrowRight, Bot, FileSearch, ClipboardList, Mail, NotebookPen,
|
|
|
- Loader2, MessageSquare, Route, X,
|
|
|
+ Loader2, MessageSquare, Route, X, FolderCode,
|
|
|
} from 'lucide-react'
|
|
|
import toast from 'react-hot-toast'
|
|
|
import { statusApi } from '../api/status'
|
|
|
import { agentsApi, type Agent } from '../api/agents'
|
|
|
import { agentpacksApi } from '../api/agentpacks'
|
|
|
import { Card, Spinner, Badge } from '../components/ui'
|
|
|
+import DirPickerModal from '../components/DirPickerModal'
|
|
|
+
|
|
|
+const WORKSPACE_PACK = 'workspace.assistant'
|
|
|
|
|
|
// 常用场景 → 内置包映射(包未安装时卡片自动隐藏)
|
|
|
const SCENARIOS = [
|
|
|
@@ -57,6 +60,8 @@ export default function Dashboard() {
|
|
|
const [routing, setRouting] = useState(false)
|
|
|
const [pickerOpen, setPickerOpen] = useState(false)
|
|
|
const [creatingPack, setCreatingPack] = useState<string | null>(null)
|
|
|
+ const [wsPickerOpen, setWsPickerOpen] = useState(false)
|
|
|
+ const [openingWs, setOpeningWs] = useState(false)
|
|
|
|
|
|
const { data: agentData } = useQuery({
|
|
|
queryKey: ['agents'], queryFn: agentsApi.list, retry: false,
|
|
|
@@ -120,6 +125,27 @@ export default function Dashboard() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ── 工作区对话(Claude Code 式):选文件夹 → 找/建工作区助手实例 → 带 ?dir 进对话 ──
|
|
|
+ async function openWorkspace(folder: string) {
|
|
|
+ setWsPickerOpen(false)
|
|
|
+ setOpeningWs(true)
|
|
|
+ try {
|
|
|
+ let agentId = agents
|
|
|
+ .filter(a => a.agent_template === WORKSPACE_PACK)
|
|
|
+ .sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))[0]?.id
|
|
|
+ if (!agentId) {
|
|
|
+ const res = await agentpacksApi.createAgent(WORKSPACE_PACK, { name: '工作区助手' })
|
|
|
+ qc.invalidateQueries({ queryKey: ['agents'] })
|
|
|
+ agentId = res.agent_id
|
|
|
+ }
|
|
|
+ navigate(`/chat/${agentId}?dir=${encodeURIComponent(folder)}`)
|
|
|
+ } catch (e) {
|
|
|
+ toast.error(e instanceof Error ? e.message : '打开工作区失败(工作区助手包是否已安装?)')
|
|
|
+ } finally {
|
|
|
+ setOpeningWs(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const scenarios = SCENARIOS.filter(s => installedPacks.has(s.packId))
|
|
|
|
|
|
return (
|
|
|
@@ -153,6 +179,22 @@ export default function Dashboard() {
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 工作区对话入口(Claude Code 式):选个文件夹直接对话 */}
|
|
|
+ <button
|
|
|
+ onClick={() => setWsPickerOpen(true)}
|
|
|
+ disabled={openingWs}
|
|
|
+ className="mt-3 w-full flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-2xl hover:border-emerald-300 hover:bg-emerald-50/40 transition-colors text-left disabled:opacity-60"
|
|
|
+ >
|
|
|
+ <span className="flex items-center justify-center w-9 h-9 rounded-xl bg-emerald-50 text-emerald-600 shrink-0">
|
|
|
+ {openingWs ? <Loader2 size={18} className="animate-spin" /> : <FolderCode size={18} />}
|
|
|
+ </span>
|
|
|
+ <span className="flex-1 min-w-0">
|
|
|
+ <span className="block text-sm font-medium text-gray-900">工作区对话</span>
|
|
|
+ <span className="block text-xs text-gray-500">选一个文件夹,像 Claude Code 一样直接读写改跑、多轮协作</span>
|
|
|
+ </span>
|
|
|
+ <ArrowRight size={16} className="text-gray-300 shrink-0" />
|
|
|
+ </button>
|
|
|
</div>
|
|
|
|
|
|
{/* ── 2. 继续上次的工作 ── */}
|
|
|
@@ -259,6 +301,14 @@ export default function Dashboard() {
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
+
|
|
|
+ {/* 工作区对话:选文件夹 */}
|
|
|
+ {wsPickerOpen && (
|
|
|
+ <DirPickerModal
|
|
|
+ onSelect={openWorkspace}
|
|
|
+ onClose={() => setWsPickerOpen(false)}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</div>
|
|
|
)
|
|
|
}
|