diff --git a/app/src/api.js b/app/src/api.js
index 1f1a253..9563dad 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -31,6 +31,7 @@ export const api = {
createTask: (pid, body) => http('POST', `/api/projects/${pid}/tasks`, body),
patchProject: (pid, body) => http('PATCH', `/api/projects/${pid}`, body),
createProject: (body) => http('POST', '/api/projects', body),
+ takeover: (taskId) => http('POST', `/api/tasks/${taskId}/takeover`),
// 附件上传走 multipart(不能用 JSON helper,content-type 由浏览器带 boundary)
uploadAttachments: async (taskId, files) => {
const fd = new FormData();
diff --git a/app/src/app.jsx b/app/src/app.jsx
index 9993736..d1a912b 100644
--- a/app/src/app.jsx
+++ b/app/src/app.jsx
@@ -103,6 +103,29 @@ function StreamModal({ agent, onClose }) {
);
}
+// 接管模态:展示 daemon 准备好的 worktree + 在自己终端起交互式 claude 的命令
+function TakeoverModal({ info, onClose }) {
+ const [copied, setCopied] = React.useState(false);
+ const copy = () => { try { navigator.clipboard.writeText(info.console); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { /* 非安全上下文 */ } };
+ return (
+
+ {onTakeover || attachments.length ? (
+
+ {onTakeover ? (
+
+ ) : null}
+ {attachments.length ? (
+ 📎 {attachments.map((a) => a.name).join(' · ')}
+ ) : null}
+
+ ) : null}
{label ? (
{label}
@@ -253,7 +267,7 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
);
}
-function TaskSection({ tasks, onToast, onCreate, t }) {
+function TaskSection({ tasks, onToast, onCreate, onTakeover, t }) {
const { Button, SectionHead } = window.MaestroDesignSystem_a6a290;
const [expandedId, setExpandedId] = React.useState(null);
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
@@ -333,7 +347,7 @@ function TaskSection({ tasks, onToast, onCreate, t }) {
{roots.map((tk) => (
+ byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} />
))}
diff --git a/src/api/server.ts b/src/api/server.ts
index cda3f92..5ef6549 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -9,7 +9,7 @@ import { syncProject, hasTodoJson } from '../sync/todo-sync.js';
import { resolvedExecutorModels } from '../executor/models.js';
import { classifyComplexity, type ClassifierFn } from '../executor/classify.js';
import { mergeBranch } from '../executor/merge.js';
-import { git, removeWorktree } from '../executor/worktree.js';
+import { git, removeWorktree, createWorktree } from '../executor/worktree.js';
import { cleanupTaskRunArtifacts } from '../executor/cleanup.js';
import { resolveLogo, LOGO_MIME } from './logo.js';
import { readTranscript, TranscriptError } from '../executor/transcript.js';
@@ -248,6 +248,24 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
return { attachments: store.addAttachments(id, saved) };
});
+ // 人工接管:准备(或复用)任务 worktree,返回用户在自己终端起交互式 claude 的命令。
+ // 适用卡住/需人工的任务——人接手手动改。不在 daemon 内起交互会话(无 TTY)。
+ app.post('/api/tasks/:id/takeover', async (req, reply) => {
+ const { id } = req.params as { id: string };
+ const task = store.getTask(id);
+ if (!task) return reply.code(404).send({ error: `任务不存在: ${id}` });
+ if (store.hasInflightRun(id)) return reply.code(400).send({ error: '任务有在途 run,无法接管' });
+ const project = store.getProject(task.projectId);
+ if (!project) return reply.code(404).send({ error: `项目不存在: ${task.projectId}` });
+ let worktree = task.result?.worktree ?? null;
+ let branch = task.result?.branch ?? null;
+ if (!worktree) {
+ const wt = await createWorktree(project.repoPath, id, project.defaultBranch);
+ worktree = wt.dir; branch = wt.branch;
+ }
+ return { taskId: id, worktree, branch, console: `cd ${worktree} && claude` };
+ });
+
// 部分更新任务(title/priority/complexity;complexity 重置逻辑在 Store.patchTask)
app.patch('/api/tasks/:id', (req) => {
const { id } = req.params as { id: string };