feat: 人工接管任务(takeover)+ 任务详情展示附件
B-③ takeover(⑧ ★ POST /api/tasks/:id/takeover):
后端:
- POST /api/tasks/:id/takeover:准备(或复用 result.worktree)任务 worktree,
返回 {taskId, worktree, branch, console}——console 为用户在自己终端起交互式
claude 的命令(daemon 无 TTY,不在进程内起交互会话);在途 run 时拒绝
前端:
- api.takeover;TakeoverModal(展示命令 + 一键复制 + 分支提示)
- TaskDetail 加「⌨ 人工接管」按钮(onTakeover 经 TaskSection→TaskRow 线程下来)
- 顺带:TaskDetail 展示任务附件(📎 文件名,读 task._raw.attachments)
验证:typecheck 干净;206 测试通过;前端 build + dev 渲染 0 错误。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
+32
-1
@@ -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 (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
|
||||
<div style={{ position: 'relative', width: 'min(620px,92vw)', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 'var(--radius-lg,10px)', padding: 18, boxShadow: '0 0 0 1px var(--line-soft), 0 24px 64px rgba(0,0,0,.6)' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: 'var(--cyan)', marginBottom: 12 }}>⌨ 人工接管 · {info.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 8 }}>worktree 已就绪。在你自己的终端运行下面命令,起交互式 Claude Code 手动接手:</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch' }}>
|
||||
<code style={{ flex: 1, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--cyan-dim)', borderRadius: 4, padding: '10px 12px', fontSize: 12.5, color: 'var(--ink)', wordBreak: 'break-all' }}>{info.console}</code>
|
||||
<button onClick={copy} style={{ flex: 'none', fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer', background: 'transparent', color: copied ? 'var(--green)' : 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 4, padding: '0 12px' }}>{copied ? '✓ 已复制' : '复制'}</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--faint)', marginTop: 10 }}>分支 {info.branch} · 完成后用 git 提交,可经审核区合并。</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||
<button onClick={onClose} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 4, padding: '5px 14px', cursor: 'pointer' }}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const I18N = window.MAESTRO_I18N;
|
||||
const [lang, setLang] = React.useState(() => {
|
||||
@@ -125,6 +148,7 @@ export function App() {
|
||||
const [showConfig, setShowConfig] = React.useState(false);
|
||||
const [archiveItem, setArchiveItem] = React.useState(null);
|
||||
const [streamAgent, setStreamAgent] = React.useState(null);
|
||||
const [takeoverInfo, setTakeoverInfo] = React.useState(null);
|
||||
const [toasts, setToasts] = React.useState([]);
|
||||
const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
|
||||
React.useEffect(() => {
|
||||
@@ -281,6 +305,12 @@ export function App() {
|
||||
setShowConfig(false); toast('ok', t.toastSaved); loadGlobal();
|
||||
} catch (e) { toast('warn', '保存失败:' + e.message); }
|
||||
};
|
||||
const takeover = async (task) => {
|
||||
try {
|
||||
const info = await api.takeover(task.id);
|
||||
setTakeoverInfo({ ...info, title: task.title });
|
||||
} catch (e) { toast('warn', '接管失败:' + e.message); }
|
||||
};
|
||||
const [showNewProject, setShowNewProject] = React.useState(false);
|
||||
const createProject = async (body) => {
|
||||
try {
|
||||
@@ -312,7 +342,7 @@ export function App() {
|
||||
<window.MaestroKitAgentSection agents={agents} quota={quota} t={t} onOpenStream={setStreamAgent} />
|
||||
<window.MaestroKitGateSection approvals={gates} onDecide={decide} t={t} />
|
||||
{tasks.length ? (
|
||||
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} onCreate={createTask} t={t} />
|
||||
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} onCreate={createTask} onTakeover={takeover} t={t} />
|
||||
) : (
|
||||
<div style={{ padding: '46px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, marginTop: 18 }}>
|
||||
<b style={{ display: 'block', fontSize: 15, color: 'var(--muted)', marginBottom: 6, letterSpacing: '.2em' }}>{t.empty}</b>
|
||||
@@ -325,6 +355,7 @@ export function App() {
|
||||
{archiveItem ? <window.MaestroKitArchiveModal item={archiveItem} t={t} onClose={() => setArchiveItem(null)} /> : null}
|
||||
{showNewProject ? <NewProjectModal t={t} lang={lang} onCreate={createProject} onClose={() => setShowNewProject(false)} /> : null}
|
||||
{streamAgent ? <StreamModal agent={streamAgent} onClose={() => setStreamAgent(null)} /> : null}
|
||||
{takeoverInfo ? <TakeoverModal info={takeoverInfo} onClose={() => setTakeoverInfo(null)} /> : null}
|
||||
<div style={{ position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
|
||||
{toasts.map((x) => <Toast key={x.id} kind={x.kind}>{x.text}</Toast>)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user