|
|
@@ -12,6 +12,7 @@ import { agentsApi, type WorkspaceFile, type MemoryEntry, type CoreMemory, type
|
|
|
import { knowledgeApi } from '../api/knowledge'
|
|
|
import { useAppStore } from '../store/app'
|
|
|
import { Spinner } from '../components/ui'
|
|
|
+import DirPickerModal from '../components/DirPickerModal'
|
|
|
import { clsx } from '../lib/clsx'
|
|
|
import { useMode } from '../api/mode'
|
|
|
|
|
|
@@ -1112,7 +1113,8 @@ export default function Chat() {
|
|
|
})
|
|
|
// 工作区对话模式:/chat/{id}?dir=/path → 每次 run 把工作文件夹带进
|
|
|
// context.work_dir,让助手就地在该文件夹读写改跑(Claude Code 式)。
|
|
|
- const [workDir] = useState(() => new URLSearchParams(window.location.search).get('dir') ?? '')
|
|
|
+ const [workDir, setWorkDir] = useState(() => new URLSearchParams(window.location.search).get('dir') ?? '')
|
|
|
+ const [dirPickerOpen, setDirPickerOpen] = useState(false)
|
|
|
const [streaming, setStreaming] = useState(false)
|
|
|
const [memoryOpen, setMemoryOpen] = useState(false)
|
|
|
const abortRef = useRef<(() => void) | null>(null)
|
|
|
@@ -1379,12 +1381,14 @@ export default function Chat() {
|
|
|
input: userText,
|
|
|
parameters: {},
|
|
|
context: {
|
|
|
- ...(ctxRunId ? { run_id: ctxRunId, workspace_path: ctxPath } : {}),
|
|
|
+ // 工作区对话模式(workDir)不挂续跑上下文——干净会话,避免旧 run
|
|
|
+ // 工作区劫持 [工作目录];连续性靠 thread 记忆 + 文件夹里的真实文件。
|
|
|
+ ...((ctxRunId && !workDir) ? { run_id: ctxRunId, workspace_path: ctxPath } : {}),
|
|
|
...(workDir ? { work_dir: workDir } : {}),
|
|
|
},
|
|
|
- // Only meaningful when continuing — fresh runs always run as iterate
|
|
|
- mode: ctxRunId ? ctxMode : 'iterate',
|
|
|
- target_subagent: ctxRunId && ctxMode === 'edit' ? ctxTarget : '',
|
|
|
+ // Only meaningful when continuing — fresh / workspace runs run as iterate
|
|
|
+ mode: (ctxRunId && !workDir) ? ctxMode : 'iterate',
|
|
|
+ target_subagent: (ctxRunId && !workDir && ctxMode === 'edit') ? ctxTarget : '',
|
|
|
// 会话记忆:带上当前 thread(首条消息为空 → 后端新建,started 回填)
|
|
|
thread_id: threadIdRef.current ?? '',
|
|
|
}),
|
|
|
@@ -1715,15 +1719,17 @@ export default function Chat() {
|
|
|
{' · '}
|
|
|
{(agent?.config?.model as Record<string, unknown>)?.name as string ?? ''}
|
|
|
</p>
|
|
|
- {/* 工作区对话:当前工作文件夹 */}
|
|
|
+ {/* 工作区对话:当前工作文件夹(点击换文件夹) */}
|
|
|
{workDir && (
|
|
|
- <span
|
|
|
- className="inline-flex items-center gap-1 text-xs bg-emerald-50 text-emerald-700 border border-emerald-200 px-1.5 py-0.5 rounded-full max-w-[280px]"
|
|
|
- title={`工作文件夹(就地读写):${workDir}`}
|
|
|
+ <button
|
|
|
+ onClick={() => setDirPickerOpen(true)}
|
|
|
+ className="inline-flex items-center gap-1 text-xs bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 px-1.5 py-0.5 rounded-full max-w-[320px] transition-colors"
|
|
|
+ title={`工作文件夹(就地读写):${workDir}\n点击切换文件夹`}
|
|
|
>
|
|
|
<Folder size={9} className="shrink-0" />
|
|
|
<span className="truncate font-mono">{workDir.split('/').slice(-2).join('/')}</span>
|
|
|
- </span>
|
|
|
+ <span className="text-emerald-400 shrink-0">·换</span>
|
|
|
+ </button>
|
|
|
)}
|
|
|
{/* KB badges */}
|
|
|
{(agentKBData?.knowledge_bases ?? []).map(kb => (
|
|
|
@@ -1806,8 +1812,9 @@ export default function Chat() {
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- {/* Directory info bar — shown only when any dir is configured */}
|
|
|
- {(agent?.source_dir || agent?.work_dir || agent?.run_dir) && (
|
|
|
+ {/* Directory info bar — 专项模式才显示 agent 的来源/产物/日志;
|
|
|
+ 工作区对话模式(workDir)只认所选文件夹 F,隐藏这条避免目录打架 */}
|
|
|
+ {!workDir && (agent?.source_dir || agent?.work_dir || agent?.run_dir) && (
|
|
|
<DirInfoBar
|
|
|
sourceDir={agent.source_dir}
|
|
|
workDir={agent.work_dir}
|
|
|
@@ -1888,12 +1895,29 @@ export default function Chat() {
|
|
|
{memoryOpen && agentId && (
|
|
|
<MemoryPanel agentId={agentId} onClose={() => setMemoryOpen(false)} />
|
|
|
)}
|
|
|
+
|
|
|
+ {/* 工作区对话:换文件夹 → 换工作目录 + 开新会话 */}
|
|
|
+ {dirPickerOpen && (
|
|
|
+ <DirPickerModal
|
|
|
+ initial={workDir}
|
|
|
+ onClose={() => setDirPickerOpen(false)}
|
|
|
+ onSelect={(folder) => {
|
|
|
+ setDirPickerOpen(false)
|
|
|
+ if (!folder || folder === workDir) return
|
|
|
+ setWorkDir(folder)
|
|
|
+ const u = new URL(window.location.href)
|
|
|
+ u.searchParams.set('dir', folder)
|
|
|
+ window.history.replaceState({}, '', u.toString())
|
|
|
+ startNewThread() // 换文件夹 = 干净新会话
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</div>
|
|
|
|
|
|
{/* Continue-from-run banner — pinned above input so it's always visible
|
|
|
right before the user sends. Distinguishes "continue this run"
|
|
|
from "start a fresh conversation". */}
|
|
|
- {continueFromRunId && (() => {
|
|
|
+ {continueFromRunId && !workDir && (() => {
|
|
|
const prevRun = runsData?.runs?.find(r => r.id === continueFromRunId)
|
|
|
const prevInputPreview = prevRun?.input?.slice(0, 120) ?? ''
|
|
|
return (
|