diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 21:26:11 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 21:26:11 +0100 |
| commit | 5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be (patch) | |
| tree | d605f7c02472f67a88f1c71c9258c1bf0823b44a /frontend/src/components/document/nodes/StepLogFeed.tsx | |
| parent | d1fdfb140cc440664f77a24886172f9976a05a31 (diff) | |
| download | soryu-5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be.tar.gz soryu-5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be.zip | |
revert PRs #93-#98; enforce strict-linear-DAG + mandatory directive verify (#100)
* revert: roll back PRs #93-#98 to pre-Lexical baseline
Reverts the entire chain of directive document UI work and the homepage redesign,
restoring the working tree to the state at 3679ceb (before c8b169d / PR #93).
PRs reverted:
- #93 c8b169d feat: Document UI for directive orchestration with Lexical editor
- #94 d6f01a6 fix: compilation error and warnings already merged via PR #93
- #95 5aa3faf fix: resolve compilation error and warnings in Rust backend
- #97 d513f93 feat: document UI with contract blocks, expandable logs, and interaction controls
- #96 6366941 feat: Redesign homepage with professional PC-98 styling
- #98 d1fdfb1 feat: revert broken directive PRs, re-implement Lexical document orchestrator
The directive Document UI experiments produced fragile output and merge artifacts;
follow-up commits in this PR change orchestration to favor strictly linear DAGs and
add goal/conflict verification so future runs do not require this kind of cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(directive): strict-linear-DAG planning + mandatory `directive verify`
Tightens directive orchestration so the final PR almost never needs a hand-merge:
1. Planning prompts now strongly bias toward strictly linear DAGs.
Parallel steps are reserved for genuinely independent work (e.g. disjoint
modules); the default for "in doubt" is sequential. Linear chains inherit
each previous step's worktree, so the final merge is typically just a
rebase against the base branch.
2. New CLI command `makima directive verify` does a local in-memory
`git merge-tree` of HEAD against `<remote>/<base>` and exits non-zero
with a list of conflicting files if the PR would not merge cleanly.
Pure-local — no API call, no working-tree mutation.
3. Completion / PR-creation prompts now mandate three pre-push checks:
a. build (`cargo check` and/or `tsc --noEmit`),
b. `makima directive verify --base <base_branch>` must exit 0, and
c. an explicit goal-alignment self-check against the diff.
The orchestrator is told NOT to push, create the PR, or call
`makima directive update` until all three pass. Skipping any of them
is documented as a directive failure.
The combination means that with a linear DAG the final PR-creation task
should almost never see a real conflict — when it does, that is treated as
a planning bug to escalate rather than something to paper over with
`-X theirs`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): TS errors pre-existing on master
- TaskSlideOutPanel: declare missing `selectedFileDiff` / `selectedFilePath`
state hooks that were referenced everywhere but never created, and
re-balance the JSX so the `<>...</>` fragment in the non-diff branch is
closed (the previous indentation/braces would not parse).
- api.ts: add a `getWorktreeDiff` thin wrapper around `getTaskDiff` so
TaskDetail's per-file click handler type-checks (the per-file slice is a
future improvement; today both return the full task diff).
- WorktreeFilesPanel: remove unused `isClickable` local; the gating already
reads `onFileClick` directly inline.
Run after revert: `npx tsc --noEmit` exits 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'frontend/src/components/document/nodes/StepLogFeed.tsx')
| -rw-r--r-- | frontend/src/components/document/nodes/StepLogFeed.tsx | 277 |
1 files changed, 0 insertions, 277 deletions
diff --git a/frontend/src/components/document/nodes/StepLogFeed.tsx b/frontend/src/components/document/nodes/StepLogFeed.tsx deleted file mode 100644 index 2f2f553..0000000 --- a/frontend/src/components/document/nodes/StepLogFeed.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; - -interface StepLogFeedProps { - taskId: string; - stepName: string; - stepStatus: string; - onCollapse: () => void; -} - -interface LogEntry { - timestamp: string; - content: string; - type: 'stdout' | 'stderr' | 'system' | 'user'; -} - -/** - * Live log feed for an expanded step row. - * Connects via WebSocket to stream task output and allows - * sending messages (comments) and interrupting the task. - */ -export function StepLogFeed({ taskId, stepName, stepStatus, onCollapse }: StepLogFeedProps) { - const [logs, setLogs] = useState<LogEntry[]>([]); - const [message, setMessage] = useState(''); - const [sending, setSending] = useState(false); - const [connected, setConnected] = useState(false); - const [error, setError] = useState<string | null>(null); - const logsEndRef = useRef<HTMLDivElement>(null); - const wsRef = useRef<WebSocket | null>(null); - const logContainerRef = useRef<HTMLDivElement>(null); - const inputRef = useRef<HTMLInputElement>(null); - - const isActive = ['running', 'starting'].includes(stepStatus.toLowerCase()); - - // Auto-scroll to bottom when new logs arrive - useEffect(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logs]); - - // Connect to WebSocket for live streaming - useEffect(() => { - if (!taskId) return; - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/v1/mesh/tasks/subscribe`; - - let ws: WebSocket; - let reconnectTimer: ReturnType<typeof setTimeout>; - let shouldReconnect = true; - - function connect() { - try { - ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.addEventListener('open', () => { - setConnected(true); - setError(null); - // Subscribe to this specific task - ws.send(JSON.stringify({ type: 'subscribe', taskId })); - }); - - ws.addEventListener('message', (evt) => { - try { - const data = JSON.parse(evt.data); - // Handle different message formats from the backend - if (data.taskId === taskId || data.task_id === taskId) { - const entry: LogEntry = { - timestamp: data.timestamp || new Date().toISOString(), - content: data.content || data.output || data.message || JSON.stringify(data), - type: data.type || data.stream || 'stdout', - }; - setLogs(prev => [...prev, entry]); - } - } catch { - // Non-JSON message, treat as raw log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: evt.data, - type: 'stdout', - }]); - } - }); - - ws.addEventListener('close', () => { - setConnected(false); - wsRef.current = null; - if (shouldReconnect && isActive) { - reconnectTimer = setTimeout(connect, 3000); - } - }); - - ws.addEventListener('error', () => { - setConnected(false); - setError('WebSocket connection failed'); - }); - } catch (err) { - setError('Failed to connect to log stream'); - } - } - - connect(); - - return () => { - shouldReconnect = false; - clearTimeout(reconnectTimer); - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - }; - }, [taskId, isActive]); - - // Keyboard shortcut: Escape to collapse - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCollapse(); - } - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [onCollapse]); - - // Send a message/comment to the task - const handleSendMessage = useCallback(async () => { - if (!message.trim() || !taskId || sending) return; - - setSending(true); - try { - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: message.trim() }), - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({ message: response.statusText })); - throw new Error(body.message || body.error || `HTTP ${response.status}`); - } - - // Add as a user message in the log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: message.trim(), - type: 'user', - }]); - setMessage(''); - inputRef.current?.focus(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to send message'); - } finally { - setSending(false); - } - }, [message, taskId, sending]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - // Prevent Escape from bubbling when input is focused - if (e.key === 'Escape') { - e.stopPropagation(); - inputRef.current?.blur(); - } - }, [handleSendMessage]); - - // Interrupt the running task - const handleInterrupt = useCallback(async () => { - if (!taskId) return; - try { - // Send a special interrupt message - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: '/interrupt' }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: 'Interrupt signal sent', - type: 'system', - }]); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to interrupt'); - } - }, [taskId]); - - const formatTimestamp = (ts: string) => { - try { - return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - } catch { - return ''; - } - }; - - return ( - <div className="step-log-feed"> - {/* Header */} - <div className="step-log-feed-header"> - <div className="step-log-feed-header-left"> - <span className="step-log-feed-title">{stepName} - Logs</span> - <span className={`step-log-feed-status ${connected ? 'connected' : 'disconnected'}`}> - {connected ? 'Live' : 'Disconnected'} - </span> - </div> - <div className="step-log-feed-header-right"> - {isActive && ( - <button - className="step-log-feed-interrupt-btn" - onClick={handleInterrupt} - title="Interrupt this contract" - > - ⏹ Interrupt - </button> - )} - <button - className="step-log-feed-collapse-btn" - onClick={onCollapse} - title="Collapse (Esc)" - > - ✕ - </button> - </div> - </div> - - {/* Log content */} - <div className="step-log-feed-content" ref={logContainerRef}> - {logs.length === 0 && !error && ( - <div className="step-log-feed-empty"> - {isActive - ? 'Waiting for log output...' - : 'No logs available for this step.'} - </div> - )} - - {error && ( - <div className="step-log-feed-error">{error}</div> - )} - - {logs.map((entry, idx) => ( - <div key={idx} className={`step-log-entry step-log-entry--${entry.type}`}> - <span className="step-log-entry-time">{formatTimestamp(entry.timestamp)}</span> - <span className="step-log-entry-content">{entry.content}</span> - </div> - ))} - <div ref={logsEndRef} /> - </div> - - {/* Message input (comment/interrupt controls) */} - {isActive && ( - <div className="step-log-feed-input"> - <input - ref={inputRef} - type="text" - className="step-log-feed-input-field" - placeholder="Send a message to this contract..." - value={message} - onChange={(e) => setMessage(e.target.value)} - onKeyDown={handleKeyDown} - disabled={sending} - /> - <button - className="step-log-feed-send-btn" - onClick={handleSendMessage} - disabled={!message.trim() || sending} - title="Send message (Enter)" - > - {sending ? '...' : '➤'} - </button> - </div> - )} - </div> - ); -} |
