163 lines
5.3 KiB
JavaScript
163 lines
5.3 KiB
JavaScript
/**
|
|
* Automation Timeline Component
|
|
*
|
|
* Shows Observe → Intent → Actions → Verify for each automation step
|
|
*
|
|
* Credits: Windows-Use verification loop, Browser-Use agent
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import Spinner from 'ink-spinner';
|
|
import { colors } from '../../tui-theme.mjs';
|
|
import { getCapabilities } from '../../terminal-profile.mjs';
|
|
|
|
const h = React.createElement;
|
|
|
|
// Step phases
|
|
export const STEP_PHASES = {
|
|
OBSERVE: 'observe',
|
|
INTENT: 'intent',
|
|
ACTIONS: 'actions',
|
|
VERIFY: 'verify'
|
|
};
|
|
|
|
/**
|
|
* Single timeline step
|
|
*/
|
|
const TimelineStep = ({
|
|
stepNumber,
|
|
observe = null, // "What I see now"
|
|
intent = null, // "What I'm trying next"
|
|
actions = [], // Array of action descriptions
|
|
verify = null, // { passed, message }
|
|
isActive = false,
|
|
isExpanded = false,
|
|
width = 60
|
|
}) => {
|
|
const caps = getCapabilities();
|
|
const railV = caps.unicodeOK ? '│' : '|';
|
|
const bullet = caps.unicodeOK ? '●' : '*';
|
|
const checkmark = caps.unicodeOK ? '✓' : '+';
|
|
const crossmark = caps.unicodeOK ? '✗' : 'X';
|
|
|
|
// Collapsed view: just step number + status
|
|
if (!isExpanded && !isActive) {
|
|
const status = verify
|
|
? (verify.passed ? 'passed' : 'failed')
|
|
: 'pending';
|
|
const statusIcon = verify?.passed ? checkmark : (verify ? crossmark : '…');
|
|
const statusColor = verify?.passed ? colors.success : (verify ? colors.error : colors.muted);
|
|
|
|
return h(Box, { flexDirection: 'row' },
|
|
h(Text, { color: colors.muted }, `Step ${stepNumber}: `),
|
|
h(Text, { color: statusColor }, statusIcon),
|
|
h(Text, { color: colors.muted, dimColor: true },
|
|
intent ? ` ${intent.slice(0, width - 20)}` : ''
|
|
)
|
|
);
|
|
}
|
|
|
|
// Expanded/active view
|
|
return h(Box, { flexDirection: 'column', marginY: 0 },
|
|
// Step header
|
|
h(Box, { flexDirection: 'row' },
|
|
h(Text, { color: isActive ? colors.accent : colors.muted, bold: isActive },
|
|
`Step ${stepNumber}`
|
|
),
|
|
isActive ? h(Box, { marginLeft: 1 },
|
|
h(Spinner, { type: 'dots' })
|
|
) : null
|
|
),
|
|
|
|
// Observe section
|
|
observe ? h(Box, { flexDirection: 'row', paddingLeft: 2 },
|
|
h(Text, { color: 'cyan' }, `${railV} Observe: `),
|
|
h(Text, { color: colors.muted, wrap: 'truncate' },
|
|
observe.slice(0, width - 15)
|
|
)
|
|
) : null,
|
|
|
|
// Intent section
|
|
intent ? h(Box, { flexDirection: 'row', paddingLeft: 2 },
|
|
h(Text, { color: 'yellow' }, `${railV} Intent: `),
|
|
h(Text, { color: colors.fg }, intent.slice(0, width - 15))
|
|
) : null,
|
|
|
|
// Actions section
|
|
actions.length > 0 ? h(Box, { flexDirection: 'column', paddingLeft: 2 },
|
|
h(Text, { color: 'magenta' }, `${railV} Actions:`),
|
|
...actions.slice(0, 5).map((action, i) =>
|
|
h(Text, { key: i, color: colors.muted, dimColor: true },
|
|
`${railV} ${i + 1}. ${action.slice(0, width - 10)}`
|
|
)
|
|
),
|
|
actions.length > 5 ? h(Text, { color: colors.muted, dimColor: true },
|
|
`${railV} +${actions.length - 5} more`
|
|
) : null
|
|
) : null,
|
|
|
|
// Verify section
|
|
verify ? h(Box, { flexDirection: 'row', paddingLeft: 2 },
|
|
h(Text, { color: verify.passed ? colors.success : colors.error },
|
|
`${railV} Verify: ${verify.passed ? checkmark : crossmark} `
|
|
),
|
|
h(Text, { color: colors.muted }, verify.message || '')
|
|
) : null
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Automation Timeline
|
|
*
|
|
* Props:
|
|
* - steps: array of step objects
|
|
* - activeStepIndex: currently executing step (-1 if none)
|
|
* - isExpanded: show all details
|
|
* - width: available width
|
|
*/
|
|
const AutomationTimeline = ({
|
|
steps = [],
|
|
activeStepIndex = -1,
|
|
isExpanded = false,
|
|
title = 'Automation',
|
|
width = 80
|
|
}) => {
|
|
const caps = getCapabilities();
|
|
|
|
if (steps.length === 0) return null;
|
|
|
|
// Count stats
|
|
const verified = steps.filter(s => s.verify?.passed).length;
|
|
const failed = steps.filter(s => s.verify && !s.verify.passed).length;
|
|
const pending = steps.length - verified - failed;
|
|
|
|
return h(Box, { flexDirection: 'column' },
|
|
// Header with stats
|
|
h(Box, { flexDirection: 'row', marginBottom: 0 },
|
|
h(Text, { color: colors.muted, bold: true }, `${title} `),
|
|
h(Text, { color: colors.success }, `${verified}✓ `),
|
|
failed > 0 ? h(Text, { color: colors.error }, `${failed}✗ `) : null,
|
|
pending > 0 ? h(Text, { color: colors.muted }, `${pending}… `) : null
|
|
),
|
|
|
|
// Steps
|
|
...steps.map((step, i) =>
|
|
h(TimelineStep, {
|
|
key: i,
|
|
stepNumber: i + 1,
|
|
observe: step.observe,
|
|
intent: step.intent,
|
|
actions: step.actions || [],
|
|
verify: step.verify,
|
|
isActive: i === activeStepIndex,
|
|
isExpanded: isExpanded || i === activeStepIndex,
|
|
width: width - 4
|
|
})
|
|
)
|
|
);
|
|
};
|
|
|
|
export default AutomationTimeline;
|
|
export { AutomationTimeline, TimelineStep };
|