@aion0/forge 0.10.79 → 0.10.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE_NOTES.md +4 -5
- package/app/api/tasks/[id]/hook/stop/route.ts +15 -0
- package/app/api/tasks/route.ts +2 -1
- package/cli/mw.mjs +7 -5
- package/cli/mw.ts +8 -6
- package/components/Dashboard.tsx +61 -28
- package/components/InlinePipelineView.tsx +22 -5
- package/components/PipelineHistory.tsx +306 -0
- package/components/TaskDetail.tsx +28 -1
- package/components/TmuxTaskTerminal.tsx +105 -0
- package/components/WebTerminal.tsx +7 -0
- package/docs/design_automation_records/Automation Redesign.dc.html +2019 -0
- package/docs/design_automation_records/README.md +232 -0
- package/lib/chat/agent-loop.ts +6 -0
- package/lib/chat/tool-dispatcher.ts +110 -9
- package/lib/help-docs/05-pipelines.md +31 -0
- package/lib/help-docs/25-chat-tools.md +23 -0
- package/lib/pipeline.ts +27 -3
- package/lib/task-manager.ts +73 -3
- package/lib/task-tmux-backend.ts +625 -0
- package/lib/workspace/skill-installer.ts +18 -8
- package/package.json +1 -1
- package/proxy.ts +5 -4
- package/src/core/db/database.ts +1 -0
- package/src/types/index.ts +3 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef, memo, useCallback, useMemo } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, memo, useCallback, useMemo, lazy, Suspense } from 'react';
|
|
4
4
|
import MarkdownContent from './MarkdownContent';
|
|
5
5
|
import NewTaskModal from './NewTaskModal';
|
|
6
6
|
import type { Task, TaskLogEntry } from '@/src/types';
|
|
7
7
|
|
|
8
|
+
const TmuxTaskTerminal = lazy(() => import('./TmuxTaskTerminal'));
|
|
9
|
+
|
|
8
10
|
// Bound the rendered log/diff to keep React from choking on huge sessions.
|
|
9
11
|
// Each LogEntry can include MarkdownContent and tool_use payloads (often
|
|
10
12
|
// kilobytes per entry); rendering even ~200 fat entries can take a beat.
|
|
@@ -29,6 +31,7 @@ export default function TaskDetail({
|
|
|
29
31
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
30
32
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
31
33
|
const [tab, setTab] = useState<'log' | 'diff' | 'result'>('log');
|
|
34
|
+
const [showSession, setShowSession] = useState(false);
|
|
32
35
|
const [expandedTools, setExpandedTools] = useState<Set<number>>(new Set());
|
|
33
36
|
const [followUpText, setFollowUpText] = useState('');
|
|
34
37
|
const [editing, setEditing] = useState(false);
|
|
@@ -166,6 +169,15 @@ export default function TaskDetail({
|
|
|
166
169
|
<span className="text-[10px] text-[var(--text-secondary)] font-mono">{task.id}</span>
|
|
167
170
|
</div>
|
|
168
171
|
<div className="flex items-center gap-2">
|
|
172
|
+
{task.backend === 'tmux' && (
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setShowSession(s => !s)}
|
|
175
|
+
className={`text-[10px] px-2 py-0.5 border rounded transition-colors ${showSession ? 'text-white bg-violet-500 border-violet-500' : 'text-violet-400 border-violet-400/30 hover:bg-violet-400 hover:text-white'}`}
|
|
176
|
+
title="Toggle tmux session panel"
|
|
177
|
+
>
|
|
178
|
+
⌨ Session
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
169
181
|
<button onClick={() => setEditing(true)} className="text-[10px] px-2 py-0.5 text-[var(--accent)] border border-[var(--accent)]/30 rounded hover:bg-[var(--accent)] hover:text-white">
|
|
170
182
|
Edit
|
|
171
183
|
</button>
|
|
@@ -191,6 +203,12 @@ export default function TaskDetail({
|
|
|
191
203
|
{task.startedAt && <span>Started: {new Date(task.startedAt).toLocaleString()}</span>}
|
|
192
204
|
{task.completedAt && <span>Completed: {new Date(task.completedAt).toLocaleString()}</span>}
|
|
193
205
|
{task.costUSD != null && <span>Cost: ${task.costUSD.toFixed(4)}</span>}
|
|
206
|
+
{task.backend === 'tmux' && (
|
|
207
|
+
<span className="px-1.5 py-0.5 rounded bg-violet-500/15 text-violet-400 font-medium">tmux</span>
|
|
208
|
+
)}
|
|
209
|
+
{task.agent && task.agent !== 'claude' && (
|
|
210
|
+
<span className="font-mono text-[var(--text-secondary)]">{task.agent}</span>
|
|
211
|
+
)}
|
|
194
212
|
</div>
|
|
195
213
|
</div>
|
|
196
214
|
|
|
@@ -318,6 +336,15 @@ export default function TaskDetail({
|
|
|
318
336
|
</div>
|
|
319
337
|
)}
|
|
320
338
|
|
|
339
|
+
{/* Tmux session panel — shown at the bottom when ⌨ Session is toggled */}
|
|
340
|
+
{showSession && task.backend === 'tmux' && (
|
|
341
|
+
<div className="border-t border-violet-500/30 shrink-0" style={{ height: 320 }}>
|
|
342
|
+
<Suspense fallback={<div className="p-3 text-[var(--text-secondary)] text-xs">Loading terminal…</div>}>
|
|
343
|
+
<TmuxTaskTerminal taskId={task.id} />
|
|
344
|
+
</Suspense>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
|
|
321
348
|
{editing && (
|
|
322
349
|
<NewTaskModal
|
|
323
350
|
editTask={{ id: task.id, projectName: task.projectName, prompt: task.prompt, priority: task.priority, mode: task.mode, scheduledAt: task.scheduledAt }}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
function getWsUrl(): string {
|
|
6
|
+
if (typeof window === 'undefined') return 'ws://localhost:8404';
|
|
7
|
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
8
|
+
const webPort = parseInt(window.location.port) || 8403;
|
|
9
|
+
return `${proto}//${window.location.hostname}:${webPort + 1}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function TmuxTaskTerminal({ taskId }: { taskId: string }) {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const el = containerRef.current;
|
|
17
|
+
if (!el) return;
|
|
18
|
+
|
|
19
|
+
let disposed = false;
|
|
20
|
+
let termRef: import('@xterm/xterm').Terminal | null = null;
|
|
21
|
+
let wsRef: WebSocket | null = null;
|
|
22
|
+
let roRef: ResizeObserver | null = null;
|
|
23
|
+
|
|
24
|
+
Promise.all([
|
|
25
|
+
import('@xterm/xterm'),
|
|
26
|
+
import('@xterm/addon-fit'),
|
|
27
|
+
]).then(([{ Terminal }, { FitAddon }]) => {
|
|
28
|
+
if (disposed) return;
|
|
29
|
+
|
|
30
|
+
const cs = getComputedStyle(document.documentElement);
|
|
31
|
+
const tv = (n: string) => cs.getPropertyValue(n).trim();
|
|
32
|
+
|
|
33
|
+
const term = new Terminal({
|
|
34
|
+
cursorBlink: true,
|
|
35
|
+
fontSize: 13,
|
|
36
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
37
|
+
scrollback: 10000,
|
|
38
|
+
theme: {
|
|
39
|
+
background: tv('--term-bg') || '#0d1117',
|
|
40
|
+
foreground: tv('--term-fg') || '#c9d1d9',
|
|
41
|
+
cursor: tv('--term-cursor') || '#7c5bf0',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
termRef = term;
|
|
45
|
+
|
|
46
|
+
const fitAddon = new FitAddon();
|
|
47
|
+
term.loadAddon(fitAddon);
|
|
48
|
+
term.open(el);
|
|
49
|
+
setTimeout(() => { try { fitAddon.fit(); } catch {} }, 50);
|
|
50
|
+
|
|
51
|
+
const ro = new ResizeObserver(() => {
|
|
52
|
+
try { fitAddon.fit(); } catch {}
|
|
53
|
+
if (wsRef?.readyState === WebSocket.OPEN) {
|
|
54
|
+
wsRef.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
roRef = ro;
|
|
58
|
+
ro.observe(el);
|
|
59
|
+
|
|
60
|
+
const sessionName = `fgt-${taskId}`;
|
|
61
|
+
const ws = new WebSocket(getWsUrl());
|
|
62
|
+
wsRef = ws;
|
|
63
|
+
|
|
64
|
+
ws.onopen = () => {
|
|
65
|
+
if (!disposed) {
|
|
66
|
+
ws.send(JSON.stringify({ type: 'attach', sessionName, cols: term.cols, rows: term.rows }));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
ws.onmessage = (e) => {
|
|
70
|
+
try {
|
|
71
|
+
const msg = JSON.parse(e.data);
|
|
72
|
+
if (msg.type === 'output') term.write(msg.data);
|
|
73
|
+
} catch {}
|
|
74
|
+
};
|
|
75
|
+
ws.onclose = () => {
|
|
76
|
+
if (!disposed) term.write('\r\n\x1b[90m[disconnected — session may have ended]\x1b[0m\r\n');
|
|
77
|
+
};
|
|
78
|
+
ws.onerror = () => {
|
|
79
|
+
if (!disposed) term.write('\r\n\x1b[91m[connection error]\x1b[0m\r\n');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
term.onData(data => {
|
|
83
|
+
if (wsRef?.readyState === WebSocket.OPEN) {
|
|
84
|
+
wsRef.send(JSON.stringify({ type: 'input', data }));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
disposed = true;
|
|
91
|
+
roRef?.disconnect();
|
|
92
|
+
wsRef?.close();
|
|
93
|
+
termRef?.dispose();
|
|
94
|
+
};
|
|
95
|
+
}, [taskId]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex flex-col h-full bg-[#0d1117]">
|
|
99
|
+
<div className="text-[10px] text-[var(--text-secondary)] px-3 py-1 border-b border-[var(--border)] shrink-0 font-mono">
|
|
100
|
+
fgt-{taskId}
|
|
101
|
+
</div>
|
|
102
|
+
<div ref={containerRef} className="flex-1 overflow-hidden p-1" />
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -14,6 +14,7 @@ import '@xterm/xterm/css/xterm.css';
|
|
|
14
14
|
export interface WebTerminalHandle {
|
|
15
15
|
openSessionInTerminal: (sessionId: string, projectPath: string) => void;
|
|
16
16
|
openProjectTerminal: (projectPath: string, projectName: string, agentId?: string, resumeMode?: boolean, sessionId?: string, profileEnv?: Record<string, string>) => void;
|
|
17
|
+
openExistingSession: (sessionName: string, label: string) => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface WebTerminalProps {
|
|
@@ -501,6 +502,12 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
501
502
|
if (targetTabId !== null) setActiveTabId(targetTabId);
|
|
502
503
|
}, 0);
|
|
503
504
|
},
|
|
505
|
+
openExistingSession(sessionName: string, label: string) {
|
|
506
|
+
const tree = makeTerminal(sessionName);
|
|
507
|
+
const newTab: TabState = { id: nextId++, label, tree, ratios: {}, activeId: firstTerminalId(tree) };
|
|
508
|
+
setTabs(prev => [...prev, newTab]);
|
|
509
|
+
setTimeout(() => setActiveTabId(newTab.id), 0);
|
|
510
|
+
},
|
|
504
511
|
}), [skipPermissions]);
|
|
505
512
|
|
|
506
513
|
// ─── Tab operations ───────────────────────────────────
|