loop-task 1.5.5 → 2.0.1
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/README.md +127 -21
- package/dist/board/App.js +1 -1
- package/dist/board/components/ActionButtons.js +1 -1
- package/dist/board/components/BoardButton.js +1 -1
- package/dist/board/components/ConfirmModal.js +1 -1
- package/dist/board/components/ContextHelpModal.js +1 -1
- package/dist/board/components/CreateForm.js +1 -1
- package/dist/board/components/CreateProjectModal.js +1 -1
- package/dist/board/components/DeleteProjectConfirm.js +1 -1
- package/dist/board/components/DetailView.js +1 -1
- package/dist/board/components/EditProjectModal.js +1 -1
- package/dist/board/components/FilterBar.js +1 -1
- package/dist/board/components/Footer.js +1 -1
- package/dist/board/components/Header.js +1 -1
- package/dist/board/components/HelpModal.js +1 -1
- package/dist/board/components/Inspector.js +1 -1
- package/dist/board/components/LogModal.js +1 -1
- package/dist/board/components/Navigator.js +1 -1
- package/dist/board/components/ProjectsModal.js +1 -1
- package/dist/board/components/ProjectsPage.js +1 -1
- package/dist/board/components/RunHistory.js +1 -1
- package/dist/board/components/SearchBox.js +1 -1
- package/dist/board/components/SearchSelect.js +1 -1
- package/dist/board/components/TaskBrowser.js +1 -1
- package/dist/board/components/TaskFilterBar.js +1 -1
- package/dist/board/components/TaskForm.js +1 -1
- package/dist/board/components/Timeline.js +1 -1
- package/dist/board/focus-context.js +1 -1
- package/dist/board/format.js +15 -6
- package/dist/board/index.js +1 -1
- package/dist/board/toast.js +1 -1
- package/dist/cli/import-validator.js +151 -0
- package/dist/cli/import-writer.js +77 -0
- package/dist/cli.js +111 -17
- package/dist/config/constants.js +52 -0
- package/dist/daemon/file-watcher.js +171 -0
- package/dist/daemon/http-server.js +605 -0
- package/dist/daemon/index.js +36 -6
- package/dist/daemon/manager.js +94 -13
- package/dist/daemon/projects.js +10 -0
- package/dist/daemon/server.js +21 -0
- package/dist/daemon/task-manager.js +7 -0
- package/dist/esm-loader.js +4 -1
- package/dist/hooks/useLoopFormValidation.js +140 -0
- package/dist/i18n/en.json +237 -17
- package/dist/shared/fs-utils.js +34 -1
- package/dist/tui/app.js +675 -151
- package/dist/tui/commands.js +114 -0
- package/dist/tui/components/ActionButtons.js +49 -0
- package/dist/tui/components/Button.js +18 -0
- package/dist/tui/components/ChainEditor.js +112 -0
- package/dist/tui/components/CommandBuilderField.js +143 -0
- package/dist/tui/components/CommandEditorModal.js +159 -0
- package/dist/tui/components/CommandInput.js +303 -0
- package/dist/tui/components/CommandsBrowserModal.js +77 -0
- package/dist/tui/components/ConfirmModal.js +9 -0
- package/dist/tui/components/ContextHelpModal.js +8 -0
- package/dist/tui/components/CreateForm.js +223 -0
- package/dist/tui/components/DebugPanel.js +8 -0
- package/dist/tui/components/ExportModal.js +37 -0
- package/dist/tui/components/FilterBar.js +13 -0
- package/dist/tui/components/FocusableButton.js +16 -0
- package/dist/tui/components/FocusableInput.js +69 -0
- package/dist/tui/components/FocusableList.js +46 -0
- package/dist/tui/components/FocusableSearchSelect.js +82 -0
- package/dist/tui/components/Footer.js +42 -0
- package/dist/tui/components/Header.js +40 -0
- package/dist/tui/components/HelpGuideModal.js +21 -0
- package/dist/tui/components/HelpModal.js +59 -0
- package/dist/tui/components/InlineCommandEditor.js +143 -0
- package/dist/tui/components/Inspector.js +31 -0
- package/dist/tui/components/LeftPanel.js +33 -0
- package/dist/tui/components/LogModal.js +121 -0
- package/dist/tui/components/Modal.js +11 -0
- package/dist/tui/components/Navigator.js +68 -0
- package/dist/tui/components/PatchEditForm.js +78 -0
- package/dist/tui/components/ProjectForm.js +64 -0
- package/dist/tui/components/ProjectsModal.js +40 -0
- package/dist/tui/components/ProjectsPage.js +99 -0
- package/dist/tui/components/RightPanel.js +34 -0
- package/dist/tui/components/RunHistory.js +108 -0
- package/dist/tui/components/SearchSelect.js +67 -0
- package/dist/tui/components/SelectModal.js +76 -0
- package/dist/tui/components/TabBar.js +20 -0
- package/dist/tui/components/TaskBrowser.js +96 -0
- package/dist/tui/components/TaskFilterBar.js +9 -0
- package/dist/tui/components/TaskForm.js +134 -0
- package/dist/tui/components/TaskPickerModal.js +60 -0
- package/dist/tui/components/Toast.js +37 -0
- package/dist/tui/components/WelcomeScreen.js +70 -0
- package/dist/tui/components/WizardForm.js +178 -0
- package/dist/tui/daemon.js +142 -0
- package/dist/tui/format.js +111 -0
- package/dist/tui/hooks/useBreakpoint.js +7 -0
- package/dist/tui/hooks/useHoverState.js +11 -0
- package/dist/tui/hooks/useLogStream.js +32 -0
- package/dist/tui/hooks/useLoopPolling.js +28 -0
- package/dist/tui/index.js +24 -0
- package/dist/tui/router.js +16 -0
- package/dist/tui/state.js +175 -0
- package/dist/tui/theme.js +96 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/utils/paste.js +10 -0
- package/dist/tui/utils/validation.js +56 -0
- package/package.json +13 -8
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { darkTheme as theme, tabAccentColor } from "../theme.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { Navigator } from "./Navigator.js";
|
|
7
|
+
import { TaskNavigator } from "./TaskBrowser.js";
|
|
8
|
+
import { FocusableList } from "./FocusableList.js";
|
|
9
|
+
function Chip(props) {
|
|
10
|
+
const { label, value, highlighted } = props;
|
|
11
|
+
return (_jsxs(Text, { color: highlighted ? theme.text.primary : theme.text.muted, children: [label, " ", value] }));
|
|
12
|
+
}
|
|
13
|
+
export function LeftPanel(props) {
|
|
14
|
+
const { isFocused, navActive = true, activeTab, query, loops, selectedIndex, filters, sort, breakpoint, projects, onSelect, onActivate, tasks, taskSelectedIndex, onTaskSelect, onTaskActivate, currentProjectName, projectFilters, projectSelectedIndex = 0, onProjectSelect, onProjectActivate, projectLoops, } = props;
|
|
15
|
+
const accentColor = tabAccentColor(activeTab);
|
|
16
|
+
const borderColor = isFocused ? accentColor : theme.border.default;
|
|
17
|
+
const hasFilter = query.length > 0;
|
|
18
|
+
const loopCountFor = (projectId) => (projectLoops ?? loops).filter((l) => l.projectId === projectId).length;
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", width: "60%", flexShrink: 0, borderStyle: "single", borderColor: borderColor, children: [_jsxs(Box, { paddingLeft: 1, gap: 1, children: [hasFilter ? (_jsx(Text, { color: accentColor, children: t("board.chipFilter", { query }) })) : null, activeTab === "loops" ? (_jsxs(_Fragment, { children: [_jsx(Chip, { label: t("board.chipProject"), value: currentProjectName ?? t("board.chipAll"), highlighted: currentProjectName != null }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipStatus"), value: filters.status, highlighted: filters.status !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipSort"), value: sort, highlighted: sort !== "description" })] })) : null, activeTab === "projects" && projectFilters ? (_jsxs(_Fragment, { children: [projectFilters.query.length > 0 ? (_jsx(Text, { color: accentColor, children: t("board.chipSearch", { query: projectFilters.query }) })) : null, _jsx(Chip, { label: t("board.chipLoops"), value: projectFilters.hasLoops, highlighted: projectFilters.hasLoops !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipType"), value: projectFilters.isSystem, highlighted: projectFilters.isSystem !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipSort"), value: projectFilters.sort, highlighted: projectFilters.sort !== "name" })] })) : null] }), activeTab === "loops" ? (_jsx(Navigator, { visible: loops, total: loops.length, selectedIndex: selectedIndex, breakpoint: breakpoint, projects: projects, onSelect: onSelect, onActivate: onActivate, isFocused: isFocused, navActive: navActive })) : activeTab === "tasks" ? (_jsx(TaskNavigator, { visible: tasks, total: tasks.length, selectedIndex: taskSelectedIndex, query: query, onSelect: onTaskSelect, onActivate: onTaskActivate, isFocused: isFocused, navActive: navActive, allTasks: tasks })) : (_jsx(ProjectNavigator, { projects: projects, loops: projectLoops ?? loops, selectedIndex: projectSelectedIndex, onSelect: onProjectSelect ?? (() => { }), onActivate: onProjectActivate ?? (() => { }), isFocused: isFocused, navActive: navActive, loopCountFor: loopCountFor }))] }));
|
|
20
|
+
}
|
|
21
|
+
function ProjectNavigator(props) {
|
|
22
|
+
const { projects, selectedIndex, onSelect, onActivate, isFocused, navActive = true, loopCountFor } = props;
|
|
23
|
+
if (projects.length === 0) {
|
|
24
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsx(FocusableList, { items: projects, selectedIndex: selectedIndex, isFocused: isFocused, navActive: navActive, limit: 15, onSelect: onSelect, onActivate: onActivate, renderItem: (project, isSelected) => {
|
|
27
|
+
const count = loopCountFor(project.id);
|
|
28
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
29
|
+
const countFg = isSelected ? theme.text.inverse : theme.text.muted;
|
|
30
|
+
const createdFg = isSelected ? theme.text.inverse : theme.text.muted;
|
|
31
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: project.color, children: "\u25CF" }), _jsxs(Text, { color: fg, children: [" ", project.name.padEnd(18)] }), _jsxs(Text, { color: countFg, children: [String(count).padStart(3), " "] }), _jsx(Text, { color: createdFg, children: project.createdAt.slice(0, 10) })] }));
|
|
32
|
+
} }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { Modal } from "./Modal.js";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
import { streamRunLog } from "../daemon.js";
|
|
8
|
+
import { formatDate } from "../format.js";
|
|
9
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
10
|
+
const MAX_VISIBLE_LINES = 20;
|
|
11
|
+
function colorForLine(line, run) {
|
|
12
|
+
if (line.includes("[Run #"))
|
|
13
|
+
return theme.accent.loop;
|
|
14
|
+
if (line.includes("--- Chain:"))
|
|
15
|
+
return theme.accent.task;
|
|
16
|
+
if (line.trimStart().startsWith("[exit")) {
|
|
17
|
+
const match = /\[exit\s+(\d+)/.exec(line);
|
|
18
|
+
const code = match ? Number(match[1]) : run.exitCode;
|
|
19
|
+
return code === 0 ? theme.semantic.success : theme.semantic.danger;
|
|
20
|
+
}
|
|
21
|
+
return theme.text.primary;
|
|
22
|
+
}
|
|
23
|
+
export function LogModal(props) {
|
|
24
|
+
const [lines, setLines] = useState(props.logLines);
|
|
25
|
+
const [streaming, setStreaming] = useState(false);
|
|
26
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
27
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
28
|
+
const [follow, setFollow] = useState(true);
|
|
29
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLines(props.logLines);
|
|
32
|
+
}, [props.logLines]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!props.loopId)
|
|
35
|
+
return;
|
|
36
|
+
if (props.run.status !== "running")
|
|
37
|
+
return;
|
|
38
|
+
setStreaming(true);
|
|
39
|
+
const socket = streamRunLog(props.loopId, props.run.runNumber, (line) => setLines((prev) => [...prev, line]), () => setStreaming(false), () => setStreaming(false));
|
|
40
|
+
return () => {
|
|
41
|
+
socket.destroy();
|
|
42
|
+
};
|
|
43
|
+
}, [props.loopId, props.run.runNumber, props.run.status]);
|
|
44
|
+
const filtered = searchQuery
|
|
45
|
+
? lines.filter((l) => l.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
46
|
+
: lines;
|
|
47
|
+
const totalLines = filtered.length;
|
|
48
|
+
const startIdx = follow
|
|
49
|
+
? Math.max(0, totalLines - MAX_VISIBLE_LINES)
|
|
50
|
+
: scrollOffset;
|
|
51
|
+
const endIdx = startIdx + MAX_VISIBLE_LINES;
|
|
52
|
+
const visible = filtered.slice(startIdx, endIdx);
|
|
53
|
+
useInput((input, key) => {
|
|
54
|
+
if (searchMode) {
|
|
55
|
+
if (key.escape || key.return) {
|
|
56
|
+
setSearchMode(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.backspace || key.delete) {
|
|
60
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (input && !key.ctrl && input.length === 1 && input >= " " && input <= "~") {
|
|
64
|
+
setSearchQuery((q) => q + input);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.escape) {
|
|
70
|
+
// Escape handled by App's global popLayer(); no-op here to avoid double-fire
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (input === "/") {
|
|
74
|
+
setSearchMode(true);
|
|
75
|
+
setSearchQuery("");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (input === "c" && !key.ctrl) {
|
|
79
|
+
copyToClipboard(lines.join("\n"));
|
|
80
|
+
props.onCopy?.();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.downArrow) {
|
|
84
|
+
setFollow(false);
|
|
85
|
+
setScrollOffset((o) => Math.min(o + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (key.upArrow) {
|
|
89
|
+
setFollow(false);
|
|
90
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const isLoading = props.loading || streaming;
|
|
95
|
+
const statusIcon = props.run.status === "running"
|
|
96
|
+
? "\u21bb"
|
|
97
|
+
: props.run.exitCode === 0
|
|
98
|
+
? "\u2713"
|
|
99
|
+
: "\u2717";
|
|
100
|
+
const statusColor = props.run.status === "running"
|
|
101
|
+
? theme.semantic.warning
|
|
102
|
+
: props.run.exitCode === 0
|
|
103
|
+
? theme.semantic.success
|
|
104
|
+
: theme.semantic.danger;
|
|
105
|
+
return (_jsxs(Modal, { title: `Run #${props.run.runNumber} - ${formatDate(props.run.startedAt)}`, onClose: props.onClose, width: "95%", height: "70%", children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " ", props.run.status === "running"
|
|
106
|
+
? t("board.logModalRunning")
|
|
107
|
+
: t("board.logModalExit", { code: props.run.exitCode })] }), props.run.duration > 0 ? (_jsxs(Text, { color: theme.text.muted, children: [" ", t("board.logModalDuration"), " ", props.run.duration, "ms"] })) : null, _jsx(Text, { color: theme.text.muted, children: searchMode
|
|
108
|
+
? `/${searchQuery}`
|
|
109
|
+
: follow
|
|
110
|
+
? "f follow"
|
|
111
|
+
: `[${startIdx}-${endIdx}/${totalLines}]` })] }), searchMode ? (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text.muted, children: "Search: " }), _jsxs(Text, { color: theme.text.primary, children: [searchQuery, "_"] })] })) : null, _jsx(Box, { flexDirection: "column", children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: isLoading
|
|
112
|
+
? t("board.logModalLoading")
|
|
113
|
+
: searchQuery
|
|
114
|
+
? t("board.logModalNoMatches")
|
|
115
|
+
: t("board.logModalEmpty") })) : (visible.map((line, i) => {
|
|
116
|
+
const realIdx = startIdx + i;
|
|
117
|
+
return (_jsx(Text, { color: colorForLine(line, props.run), wrap: "truncate", children: line }, realIdx));
|
|
118
|
+
})) }), _jsx(Box, { marginTop: 1, justifyContent: "space-between", children: _jsx(Text, { color: theme.text.muted, children: searchMode
|
|
119
|
+
? t("board.logModalSearchHint")
|
|
120
|
+
: t("board.logModalFooterHints") }) })] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
export function Modal(props) {
|
|
5
|
+
useInput((_input, key) => {
|
|
6
|
+
if (key.escape) {
|
|
7
|
+
props.onClose();
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.elevated, paddingX: 2, paddingY: 1, flexDirection: "column", width: props.width ?? "60%", height: props.height, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: props.title }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, children: props.children })] }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { ScrollList } from "ink-scroll-list";
|
|
5
|
+
import { darkTheme as theme, statusColor } from "../theme.js";
|
|
6
|
+
import { describeLoop, sinceLabel, statusLabel, timingLabel, truncate } from "../format.js";
|
|
7
|
+
import { t } from "../../i18n/index.js";
|
|
8
|
+
const DESC_WIDTH = 32;
|
|
9
|
+
const SINCE_WIDTH = 13;
|
|
10
|
+
const RUNS_WIDTH = 4;
|
|
11
|
+
const SKIPPED_WIDTH = 4;
|
|
12
|
+
const STATUS_WIDTH = 8;
|
|
13
|
+
const COL_GAP = 1;
|
|
14
|
+
const LIMIT = 15;
|
|
15
|
+
export function Navigator(props) {
|
|
16
|
+
const { visible, total, selectedIndex, projects, onSelect, onActivate, isFocused, navActive = true } = props;
|
|
17
|
+
const n = visible.length;
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (n === 0)
|
|
20
|
+
return;
|
|
21
|
+
if (key.upArrow || input === "k") {
|
|
22
|
+
const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
|
|
23
|
+
onSelect(next);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.downArrow || input === "j") {
|
|
27
|
+
const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
|
|
28
|
+
onSelect(next);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (key.ctrl && key.return) {
|
|
32
|
+
onActivate(selectedIndex);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}, { isActive: isFocused && navActive });
|
|
36
|
+
const title = t("board.navigatorCount", {
|
|
37
|
+
visible: String(visible.length),
|
|
38
|
+
total: String(total),
|
|
39
|
+
});
|
|
40
|
+
function projectColor(loop) {
|
|
41
|
+
const proj = projects.find((p) => p.id === loop.projectId);
|
|
42
|
+
return proj?.color ?? theme.text.muted;
|
|
43
|
+
}
|
|
44
|
+
function isFailed(loop) {
|
|
45
|
+
return loop.lastExitCode !== null && loop.lastExitCode !== 0;
|
|
46
|
+
}
|
|
47
|
+
function renderLoop(loop, isSelected) {
|
|
48
|
+
const desc = truncate(describeLoop(loop), DESC_WIDTH);
|
|
49
|
+
const since = sinceLabel(loop);
|
|
50
|
+
const timing = timingLabel(loop);
|
|
51
|
+
const failed = isFailed(loop);
|
|
52
|
+
const sColor = failed ? theme.semantic.danger : statusColor(loop.status);
|
|
53
|
+
const sLabel = statusLabel(loop.status);
|
|
54
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
55
|
+
const dotChar = failed ? "\u2717 " : "\u25cf ";
|
|
56
|
+
const dotColor = failed
|
|
57
|
+
? theme.semantic.danger
|
|
58
|
+
: isSelected
|
|
59
|
+
? theme.text.inverse
|
|
60
|
+
: projectColor(loop);
|
|
61
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: dotColor, children: dotChar }), _jsx(Text, { color: fg, children: desc.padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: since.padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.runCount).padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.skippedCount).padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: isSelected ? theme.text.inverse : sColor, children: sLabel.padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: timing }), loop.status === "running" ? (_jsxs(Text, { color: theme.semantic.success, children: [" ", _jsx(Spinner, { type: "dots" })] })) : null] }));
|
|
62
|
+
}
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.noMatch") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.headerDescription").padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSince").padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerRuns").padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSkipped").padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerStatus").padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerTiming") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { selectedIndex: selectedIndex, height: LIMIT, children: visible.map((loop, i) => {
|
|
64
|
+
const isSelected = i === selectedIndex;
|
|
65
|
+
const indicator = isSelected ? "\u203a " : " ";
|
|
66
|
+
return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.active : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: indicator }), renderLoop(loop, isSelected)] }, i));
|
|
67
|
+
}) }) })] }))] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
import { FocusableInput } from "./FocusableInput.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
7
|
+
const LABEL_WIDTH = 15;
|
|
8
|
+
const DIVIDER_CHAR = "\u2500";
|
|
9
|
+
const FOCUSED_MARKER = "\u203A";
|
|
10
|
+
export function PatchEditForm(props) {
|
|
11
|
+
const { title, fields, activeField, activeFieldValue, onActiveFieldChange, onActiveFieldCommit, onActiveFieldCancel, onActiveFieldActivate, pendingChanges, focusedRowIndex, onFocusedRowChange, validationErrors, onSave, onCancel, onCopy, } = props;
|
|
12
|
+
useInput((_input, key) => {
|
|
13
|
+
if (activeField === null) {
|
|
14
|
+
if (key.upArrow) {
|
|
15
|
+
onFocusedRowChange(Math.max(0, focusedRowIndex - 1));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (key.downArrow) {
|
|
19
|
+
onFocusedRowChange(Math.min(fields.length - 1, focusedRowIndex + 1));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (key.return) {
|
|
23
|
+
const field = fields[focusedRowIndex];
|
|
24
|
+
if (field) {
|
|
25
|
+
const shown = displayValue(field.key, field.value);
|
|
26
|
+
onActiveFieldActivate(field.key, shown);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.ctrl && _input === "y") {
|
|
31
|
+
const field = fields[focusedRowIndex];
|
|
32
|
+
if (field) {
|
|
33
|
+
const shown = displayValue(field.key, field.value);
|
|
34
|
+
copyToClipboard(shown);
|
|
35
|
+
onCopy(shown);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.ctrl && _input === "s") {
|
|
40
|
+
onSave();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.escape) {
|
|
44
|
+
onCancel();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (key.escape) {
|
|
50
|
+
onActiveFieldCancel();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const pendingCount = Object.keys(pendingChanges).length;
|
|
56
|
+
const divider = DIVIDER_CHAR.repeat(50);
|
|
57
|
+
function displayValue(fieldKey, originalValue) {
|
|
58
|
+
if (fieldKey in pendingChanges) {
|
|
59
|
+
return pendingChanges[fieldKey];
|
|
60
|
+
}
|
|
61
|
+
return originalValue;
|
|
62
|
+
}
|
|
63
|
+
const hintText = activeField !== null
|
|
64
|
+
? t("patch.hintEditing")
|
|
65
|
+
: pendingCount > 0
|
|
66
|
+
? `${t("patch.hintNavPending")} . Ctrl+Y ${t("patch.hintCopy").toLowerCase()}`
|
|
67
|
+
: `${t("patch.hintNavIdle")}`;
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { bold: true, color: theme.accent.loop, children: title }) }), pendingCount > 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("patch.pendingChanges", { count: String(pendingCount) }) }) })) : (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("patch.noChanges") }) })), _jsx(Box, { children: _jsx(Text, { color: theme.border.dim, children: divider }) }), fields.map((field, index) => {
|
|
69
|
+
const isActive = activeField === field.key;
|
|
70
|
+
const hasPending = field.key in pendingChanges;
|
|
71
|
+
const shownValue = displayValue(field.key, field.value);
|
|
72
|
+
const isFocused = activeField === null && index === focusedRowIndex;
|
|
73
|
+
const marker = isFocused ? FOCUSED_MARKER : " ";
|
|
74
|
+
const markerColor = isFocused ? theme.accent.brand : theme.text.muted;
|
|
75
|
+
const fieldError = validationErrors[field.key];
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: markerColor, children: marker }) }), _jsx(Box, { width: LABEL_WIDTH, flexShrink: 0, children: _jsx(Text, { color: theme.text.muted, children: field.label }) }), _jsx(Box, { flexGrow: 1, children: isActive ? (_jsx(FocusableInput, { value: activeFieldValue, onChange: onActiveFieldChange, onSubmit: onActiveFieldCommit })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.text.primary, children: shownValue }), hasPending ? (_jsx(Text, { color: theme.semantic.warning, children: " \u25CF" })) : null] })) })] }), fieldError ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: LABEL_WIDTH, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: theme.semantic.danger, children: fieldError }) })] })) : null] }, field.key));
|
|
77
|
+
}), _jsx(Box, { children: _jsx(Text, { color: theme.border.dim, children: divider }) }), pendingCount > 0 ? (_jsxs(Box, { paddingLeft: 1, flexDirection: "row", children: [_jsx(Text, { color: theme.accent.brand, children: "[Save] Ctrl+S " }), _jsx(Text, { color: theme.text.muted, children: "[Cancel] Esc" })] })) : null, _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: hintText }) })] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useCallback, useState, useRef } from "react";
|
|
3
|
+
import { createProject, updateProject } from "../daemon.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { WizardForm } from "./WizardForm.js";
|
|
6
|
+
import { SelectModal, SelectValueField } from "./SelectModal.js";
|
|
7
|
+
import { PROJECT_COLORS, PROJECT_COLOR_KEYS } from "../../config/constants.js";
|
|
8
|
+
export function ProjectFormView(props) {
|
|
9
|
+
const { mode, editProject, onCancel, onDone } = props;
|
|
10
|
+
const colorKeyFor = (color) => {
|
|
11
|
+
const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
|
|
12
|
+
return found ?? "cyan";
|
|
13
|
+
};
|
|
14
|
+
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
|
15
|
+
const colorFieldRef = useRef(null);
|
|
16
|
+
const colorOptions = useMemo(() => PROJECT_COLOR_KEYS.map((k) => ({ value: k, label: k })), []);
|
|
17
|
+
const steps = useMemo(() => [
|
|
18
|
+
{
|
|
19
|
+
key: "name",
|
|
20
|
+
prompt: t("project.wizard.namePrompt"),
|
|
21
|
+
hint: t("project.wizard.nameHint"),
|
|
22
|
+
required: true,
|
|
23
|
+
inputType: "text",
|
|
24
|
+
defaultValue: editProject?.name ?? undefined,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "color",
|
|
28
|
+
prompt: t("project.wizard.colorPrompt"),
|
|
29
|
+
hint: t("project.wizard.colorHint"),
|
|
30
|
+
required: true,
|
|
31
|
+
defaultValue: colorKeyFor(editProject?.color),
|
|
32
|
+
onActivate: () => setColorPickerOpen(true),
|
|
33
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
34
|
+
colorFieldRef.current = { value, onChange, onAdvance };
|
|
35
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("project.wizard.colorPrompt"), isActive: isActive }));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
], [editProject]);
|
|
39
|
+
const handleComplete = useCallback((values) => {
|
|
40
|
+
const name = (values.name ?? "").trim();
|
|
41
|
+
if (!name)
|
|
42
|
+
return;
|
|
43
|
+
const colorKey = values.color ?? "cyan";
|
|
44
|
+
const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
|
|
45
|
+
if (mode === "edit" && editProject) {
|
|
46
|
+
updateProject(editProject.id, name, color)
|
|
47
|
+
.then(() => onDone(true, name))
|
|
48
|
+
.catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
createProject(name, color)
|
|
52
|
+
.then(() => onDone(false, name))
|
|
53
|
+
.catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
}, [mode, editProject, onDone]);
|
|
56
|
+
const title = mode === "edit"
|
|
57
|
+
? t("project.editTitle")
|
|
58
|
+
: t("project.createTitle");
|
|
59
|
+
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: colorPickerOpen }), colorPickerOpen ? (_jsx(SelectModal, { title: t("project.wizard.colorPrompt"), options: colorOptions, initialValue: colorFieldRef.current?.value, onSelect: (option) => {
|
|
60
|
+
colorFieldRef.current?.onChange(option.value);
|
|
61
|
+
colorFieldRef.current?.onAdvance();
|
|
62
|
+
setColorPickerOpen(false);
|
|
63
|
+
}, onClose: () => setColorPickerOpen(false) })) : null] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
export function ProjectsModal(props) {
|
|
7
|
+
const { projects, loops, currentProjectId, onSelect, onClose } = props;
|
|
8
|
+
const [selectedIndex, setSelectedIndex] = React.useState(() => {
|
|
9
|
+
const allIdx = projects.findIndex((p) => p.id === "all");
|
|
10
|
+
if (allIdx >= 0)
|
|
11
|
+
return allIdx;
|
|
12
|
+
const currentIdx = projects.findIndex((p) => p.id === currentProjectId);
|
|
13
|
+
return currentIdx >= 0 ? currentIdx : 0;
|
|
14
|
+
});
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (key.upArrow) {
|
|
17
|
+
setSelectedIndex((i) => i <= 0 ? projects.length - 1 : i - 1);
|
|
18
|
+
}
|
|
19
|
+
else if (key.downArrow) {
|
|
20
|
+
setSelectedIndex((i) => i >= projects.length - 1 ? 0 : i + 1);
|
|
21
|
+
}
|
|
22
|
+
else if (key.return) {
|
|
23
|
+
const project = projects[selectedIndex];
|
|
24
|
+
if (project)
|
|
25
|
+
onSelect(project.id);
|
|
26
|
+
}
|
|
27
|
+
else if (key.escape) {
|
|
28
|
+
onClose();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const loopCount = (pid) => loops.filter((l) => (l.projectId ?? "default") === pid).length;
|
|
32
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.elevated, flexDirection: "column", paddingX: 2, paddingY: 1, minWidth: 40, maxWidth: 60, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("project.filterTitle") }), projects.map((project, i) => {
|
|
33
|
+
const isSelected = i === selectedIndex;
|
|
34
|
+
const isActive = project.id === currentProjectId;
|
|
35
|
+
const prefix = isSelected ? "\u203a " : " ";
|
|
36
|
+
const bg = isSelected ? theme.bg.active : undefined;
|
|
37
|
+
const fg = isSelected ? theme.text.inverse : isActive ? theme.accent.brand : theme.text.primary;
|
|
38
|
+
return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, _jsx(Text, { color: project.color ?? theme.text.primary, children: "\u25cf " }), project.name, " ", _jsxs(Text, { color: theme.text.muted, children: ["(", loopCount(project.id), ")"] })] }) }, project.id));
|
|
39
|
+
}), _jsxs(Text, { color: theme.text.muted, children: ["\n", t("board.logModalEscClose")] })] }) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput, useFocus } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { FocusableList } from "./FocusableList.js";
|
|
6
|
+
import { FocusableButton } from "./FocusableButton.js";
|
|
7
|
+
import { Modal } from "./Modal.js";
|
|
8
|
+
import { t } from "../../i18n/index.js";
|
|
9
|
+
import { deleteProject } from "../daemon.js";
|
|
10
|
+
function loopCountFor(loops, projectId, defaultProjectId, knownProjectIds) {
|
|
11
|
+
const direct = loops.filter((l) => l.projectId === projectId).length;
|
|
12
|
+
if (projectId === defaultProjectId) {
|
|
13
|
+
const orphans = loops.filter((l) => !knownProjectIds.has(l.projectId)).length;
|
|
14
|
+
return direct + orphans;
|
|
15
|
+
}
|
|
16
|
+
return direct;
|
|
17
|
+
}
|
|
18
|
+
function DeleteConfirm(props) {
|
|
19
|
+
return (_jsxs(Modal, { title: t("project.deleteTitle"), onClose: props.onCancel, width: 50, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.text.primary, children: t("project.deleteConfirm", { name: props.project.name }) }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.deleteWarning", { count: props.loopCount }) }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(FocusableButton, { label: t("board.yes"), color: theme.semantic.danger, onPress: props.onConfirm, variant: "danger" }), _jsx(FocusableButton, { label: t("board.no"), color: theme.text.secondary, onPress: props.onCancel })] })] }));
|
|
20
|
+
}
|
|
21
|
+
function ListFocusWrapper(props) {
|
|
22
|
+
const { isFocused } = useFocus();
|
|
23
|
+
if (props.projects.length === 0) {
|
|
24
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
|
|
25
|
+
}
|
|
26
|
+
const nameWidth = 20;
|
|
27
|
+
const loopsWidth = 12;
|
|
28
|
+
const headerSeparator = " \u00B7 ";
|
|
29
|
+
return (_jsxs(React.Fragment, { children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, children: [_jsx(Text, { color: theme.text.muted, children: " ".concat(t("project.headerName").padEnd(nameWidth)) }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, String(t("project.headerLoops")).padEnd(loopsWidth)] }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, t("project.headerCreated")] })] }), _jsx(FocusableList, { items: props.projects, selectedIndex: props.selectedIndex, isFocused: isFocused, limit: 10, onSelect: props.onSelect, onActivate: props.onActivate, renderItem: (project, isSelected) => {
|
|
30
|
+
const count = loopCountFor(props.loops, project.id, props.defaultProjectId, props.knownProjectIds);
|
|
31
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
32
|
+
const countFg = isSelected ? theme.text.inverse : theme.text.muted;
|
|
33
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: project.color, children: "\u25CF" }), _jsxs(Text, { color: fg, children: [" ", project.name.padEnd(nameWidth - 1)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, String(count).padEnd(loopsWidth)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, project.createdAt] })] }));
|
|
34
|
+
} }), props.children] }));
|
|
35
|
+
}
|
|
36
|
+
export function ProjectsPage(props) {
|
|
37
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
38
|
+
const [subModal, setSubModal] = useState("none");
|
|
39
|
+
const defaultProject = props.projects.find((p) => p.isDefault) ?? null;
|
|
40
|
+
const defaultProjectId = defaultProject?.id ?? null;
|
|
41
|
+
const knownProjectIds = new Set(props.projects.map((p) => p.id));
|
|
42
|
+
const selected = props.projects[selectedIndex];
|
|
43
|
+
const loopCount = selected
|
|
44
|
+
? loopCountFor(props.loops, selected.id, defaultProjectId, knownProjectIds)
|
|
45
|
+
: 0;
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (subModal !== "none")
|
|
48
|
+
return;
|
|
49
|
+
if (key.escape) {
|
|
50
|
+
props.onClose();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (input === "n") {
|
|
54
|
+
if (props.onNavigateCreate) {
|
|
55
|
+
props.onNavigateCreate();
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (input === "e" && selected) {
|
|
60
|
+
if (props.onNavigateEdit) {
|
|
61
|
+
props.onNavigateEdit(selected);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (input === "d" && selected) {
|
|
66
|
+
setSubModal("delete");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
async function handleDelete() {
|
|
71
|
+
if (!selected)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
await deleteProject(selected.id);
|
|
75
|
+
props.onToast(t("project.toastDeleted", { name: selected.name }));
|
|
76
|
+
await props.onRefresh();
|
|
77
|
+
setSubModal("none");
|
|
78
|
+
setSelectedIndex(0);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
props.onToast(e.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (subModal === "delete" && selected) {
|
|
85
|
+
return (_jsx(DeleteConfirm, { project: selected, loopCount: loopCount, onConfirm: handleDelete, onCancel: () => setSubModal("none") }));
|
|
86
|
+
}
|
|
87
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Box, { width: "40%", flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderColor: theme.accent.project, paddingX: 1, children: _jsx(Text, { color: theme.accent.project, bold: true, children: t("project.projectsTitle") }) }), props.projects.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) })) : (_jsx(ListFocusWrapper, { projects: props.projects, loops: props.loops, defaultProjectId: defaultProjectId, knownProjectIds: knownProjectIds, selectedIndex: selectedIndex, onSelect: setSelectedIndex, onActivate: (idx) => {
|
|
88
|
+
setSelectedIndex(idx);
|
|
89
|
+
if (props.onNavigateEdit) {
|
|
90
|
+
const p = props.projects[idx];
|
|
91
|
+
if (p)
|
|
92
|
+
props.onNavigateEdit(p);
|
|
93
|
+
}
|
|
94
|
+
}, children: null }))] }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginLeft: 1, children: selected ? (_jsxs(React.Fragment, { children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.brand, padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: selected.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + selected.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "ID: " }), _jsx(Text, { color: theme.text.secondary, children: selected.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Created: " }), _jsx(Text, { color: theme.text.secondary, children: selected.createdAt })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Loops: " }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: loopCount }) })] }), selected.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.brand, onPress: () => {
|
|
95
|
+
if (props.onNavigateEdit && selected) {
|
|
96
|
+
props.onNavigateEdit(selected);
|
|
97
|
+
}
|
|
98
|
+
} }), _jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: () => setSubModal("delete") })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text.muted, children: `${t("project.keyNewHint")} | ${t("project.keyEditHint")} | ${t("project.keyDeleteHint")}` }) })] })] })) : (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) })) })] }) }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme, tabAccentColor } from "../theme.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { Inspector } from "./Inspector.js";
|
|
6
|
+
import { RunHistory } from "./RunHistory.js";
|
|
7
|
+
import { FocusableButton } from "./FocusableButton.js";
|
|
8
|
+
import { commandLine } from "../format.js";
|
|
9
|
+
const DIVIDER = "\u2500".repeat(40);
|
|
10
|
+
export function RightPanel(props) {
|
|
11
|
+
const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, } = props;
|
|
12
|
+
const borderColor = isFocused ? tabAccentColor(activeTab) : theme.border.default;
|
|
13
|
+
return (_jsx(Box, { flexDirection: "column", width: "40%", borderStyle: "single", borderColor: borderColor, children: activeTab === "projects" ? (_jsx(ProjectInspector, { project: selectedProject ?? null, loopCount: projectLoopCount ?? 0, onEdit: onProjectEdit, onDelete: onProjectDelete })) : activeTab === "tasks" ? (_jsx(TaskInspector, { task: selectedTask ?? null, allTasks: allTasks ?? [] })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive })] })) }));
|
|
14
|
+
}
|
|
15
|
+
function TaskInspector(props) {
|
|
16
|
+
const { task, allTasks } = props;
|
|
17
|
+
if (!task) {
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }), _jsx(Text, { color: theme.text.muted, children: DIVIDER }), _jsx(Text, { color: theme.text.muted, children: t("board.taskInspectorEmpty") })] }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Field, { label: t("board.taskFieldName"), children: _jsx(Text, { color: theme.accent.task, bold: true, children: task.name }) }), _jsx(Field, { label: t("board.taskFieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(Field, { label: t("board.taskFieldCommand"), children: _jsx(Text, { color: theme.text.primary, children: task.commandRaw
|
|
21
|
+
? task.commandRaw.split("\n").filter(Boolean).join(" ")
|
|
22
|
+
: commandLine(task.command, task.commandArgs) }) }), _jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) }), _jsxs(Field, { label: t("board.taskFieldChain"), children: [task.onSuccessTaskId ? (_jsx(Text, { color: theme.semantic.success, children: "\u2713 " + (allTasks.find((t) => t.id === task.onSuccessTaskId)?.name ?? task.onSuccessTaskId) })) : null, task.onFailureTaskId ? (_jsx(Text, { color: theme.semantic.danger, children: " \u2192 " + (allTasks.find((t) => t.id === task.onFailureTaskId)?.name ?? task.onFailureTaskId) })) : null, !task.onSuccessTaskId && !task.onFailureTaskId ? (_jsx(Text, { color: theme.text.muted, children: t("board.taskNone") })) : null] })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
|
|
23
|
+
}
|
|
24
|
+
const LABEL_WIDTH = 9;
|
|
25
|
+
function Field(props) {
|
|
26
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), props.children] }));
|
|
27
|
+
}
|
|
28
|
+
function ProjectInspector(props) {
|
|
29
|
+
const { project, loopCount, onEdit, onDelete } = props;
|
|
30
|
+
if (!project) {
|
|
31
|
+
return (_jsx(Box, { padding: 1, flexGrow: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.inspectorEmpty") }) }));
|
|
32
|
+
}
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: project.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + project.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldId") }), _jsx(Text, { color: theme.text.secondary, children: project.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldLoops") }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: String(loopCount) }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldCreated") }), _jsx(Text, { color: theme.text.secondary, children: project.createdAt.slice(0, 10) })] }), project.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) })), !project.isSystem && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [onEdit && (_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.project, onPress: onEdit })), onDelete && (_jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: onDelete }))] }))] }));
|
|
34
|
+
}
|