loop-task 1.5.5 → 2.0.0
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 +141 -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,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } 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
|
+
import { EXPORT_MAX_PREVIEW_LINES } from "../../config/constants.js";
|
|
7
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
8
|
+
const VISIBLE_LINES = 20;
|
|
9
|
+
export function ExportModal(props) {
|
|
10
|
+
const lines = props.json.split("\n");
|
|
11
|
+
const totalLines = lines.length;
|
|
12
|
+
const truncated = totalLines > EXPORT_MAX_PREVIEW_LINES;
|
|
13
|
+
const displayLines = truncated ? lines.slice(0, EXPORT_MAX_PREVIEW_LINES) : lines;
|
|
14
|
+
const maxScroll = Math.max(0, displayLines.length - VISIBLE_LINES);
|
|
15
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
if (key.escape) {
|
|
18
|
+
props.onClose();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (key.downArrow) {
|
|
22
|
+
setScrollOffset((o) => Math.min(o + 1, maxScroll));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (key.upArrow) {
|
|
26
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (input === "c" && !key.ctrl) {
|
|
30
|
+
copyToClipboard(props.json);
|
|
31
|
+
props.onCopy?.();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const visible = displayLines.slice(scrollOffset, scrollOffset + VISIBLE_LINES);
|
|
36
|
+
return (_jsxs(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.accent.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, bold: true, children: t("export.modalTitle") }) }), props.error ? (_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: theme.semantic.danger, children: t("export.error", { message: props.error }) }) })) : (_jsxs(_Fragment, { children: [props.filePath ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, children: t("export.filePath", { path: props.filePath }) }) })) : null, _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visible.map((line, i) => (_jsx(Text, { color: theme.text.primary, wrap: "truncate", children: line }, scrollOffset + i))), truncated ? (_jsx(Text, { color: theme.text.muted, children: t("export.truncated", { total: String(totalLines) }) })) : null] })] })), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: t("export.hint") }), _jsxs(Text, { color: theme.text.muted, children: ["[", scrollOffset, "-", scrollOffset + visible.length, "/", totalLines, "]"] })] })] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { FocusableButton } from "./FocusableButton.js";
|
|
6
|
+
import { FocusableInput } from "./FocusableInput.js";
|
|
7
|
+
export function FilterBar(props) {
|
|
8
|
+
const { filters, sort, onStatusCycle, onSortCycle, onSelectProject, currentProjectName, onQueryChange, onSearchDismiss, } = props;
|
|
9
|
+
const statusLabel = `${t("board.statusFilterTitle")} ${filters.status}`;
|
|
10
|
+
const sortLabel = `${t("board.sortTitle")} ${sort}`;
|
|
11
|
+
const projectLabel = currentProjectName ?? t("project.showAll");
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", height: 4, children: [_jsxs(Box, { children: [_jsx(Box, { width: currentProjectName ? 36 : 48, children: _jsx(FocusableInput, { value: filters.query, onChange: onQueryChange, onSubmit: onSearchDismiss, placeholder: t("board.searchPlaceholder") }) }), onSelectProject ? (_jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: projectLabel, color: theme.accent.project, onPress: onSelectProject }) })) : null, _jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: statusLabel, color: theme.accent.focus, onPress: onStatusCycle }) }), _jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: sortLabel, color: theme.accent.focus, onPress: onSortCycle }) })] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("board.searchEmpty") }) })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useFocus, useInput } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
export function FocusableButton(props) {
|
|
5
|
+
const { label, color, onPress, variant = "default" } = props;
|
|
6
|
+
const { isFocused } = useFocus();
|
|
7
|
+
useInput((_input, key) => {
|
|
8
|
+
if (key.return) {
|
|
9
|
+
onPress?.();
|
|
10
|
+
}
|
|
11
|
+
}, { isActive: isFocused });
|
|
12
|
+
const accent = variant === "danger" ? theme.semantic.danger : color;
|
|
13
|
+
const textColor = isFocused ? theme.text.inverse : accent;
|
|
14
|
+
const indicator = isFocused ? "› " : " ";
|
|
15
|
+
return (_jsx(Box, { backgroundColor: isFocused ? theme.bg.active : undefined, paddingX: 1, marginRight: 1, flexShrink: 0, children: _jsx(Text, { color: textColor, bold: true, children: indicator + label }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useFocus, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { sanitizePaste } from "../utils/paste.js";
|
|
6
|
+
export function FocusableInput(props) {
|
|
7
|
+
const { value, onChange, onSubmit } = props;
|
|
8
|
+
const placeholder = props.placeholder ?? "type here...";
|
|
9
|
+
const { isFocused } = useFocus();
|
|
10
|
+
const [cursorOffset, setCursorOffset] = useState(value.length);
|
|
11
|
+
const insertText = useCallback((text) => {
|
|
12
|
+
const before = value.slice(0, cursorOffset);
|
|
13
|
+
const after = value.slice(cursorOffset);
|
|
14
|
+
const next = before + text + after;
|
|
15
|
+
onChange(next);
|
|
16
|
+
setCursorOffset(before.length + text.length);
|
|
17
|
+
}, [value, cursorOffset, onChange]);
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
|
|
20
|
+
if (input.includes("\x1b[200~")) {
|
|
21
|
+
insertText(sanitizePaste(input));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (key.ctrl || key.escape)
|
|
25
|
+
return;
|
|
26
|
+
// Multi-char containing CR/LF with no bracketed markers — ignore
|
|
27
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
28
|
+
return;
|
|
29
|
+
if (key.return) {
|
|
30
|
+
onSubmit?.(value);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.backspace || key.delete) {
|
|
34
|
+
if (cursorOffset > 0) {
|
|
35
|
+
const next = value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);
|
|
36
|
+
onChange(next);
|
|
37
|
+
setCursorOffset((c) => c - 1);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (key.leftArrow) {
|
|
42
|
+
if (cursorOffset > 0)
|
|
43
|
+
setCursorOffset((c) => c - 1);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.rightArrow) {
|
|
47
|
+
if (cursorOffset < value.length)
|
|
48
|
+
setCursorOffset((c) => c + 1);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Multi-char printable input = unbracketed single-line paste (e.g. right-click)
|
|
52
|
+
if (input.length > 1 && !key.meta) {
|
|
53
|
+
insertText(sanitizePaste(input));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Single printable char
|
|
57
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
58
|
+
insertText(input);
|
|
59
|
+
}
|
|
60
|
+
}, { isActive: isFocused });
|
|
61
|
+
const borderColor = isFocused ? theme.accent.brand : theme.border.dim;
|
|
62
|
+
const backgroundColor = isFocused ? theme.bg.input : undefined;
|
|
63
|
+
const showPlaceholder = value.length === 0;
|
|
64
|
+
// Render value with cursor
|
|
65
|
+
const before = value.slice(0, cursorOffset);
|
|
66
|
+
const cursorChar = cursorOffset < value.length ? value[cursorOffset] : " ";
|
|
67
|
+
const after = cursorOffset < value.length ? value.slice(cursorOffset + 1) : "";
|
|
68
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, paddingLeft: 1, overflow: "hidden", children: [showPlaceholder ? (_jsx(Text, { color: theme.text.muted, children: placeholder })) : (_jsxs(Text, { color: theme.text.primary, children: [before, _jsx(Text, { inverse: true, children: cursorChar }), after] })), isFocused && showPlaceholder ? _jsx(Text, { inverse: true, children: " " }) : null] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
export function FocusableList(props) {
|
|
6
|
+
const { items, renderItem, onSelect, onActivate, selectedIndex, isFocused, navActive = true, limit, } = props;
|
|
7
|
+
const n = items.length;
|
|
8
|
+
useInput((input, key) => {
|
|
9
|
+
if (n === 0)
|
|
10
|
+
return;
|
|
11
|
+
if (key.upArrow || input === "k") {
|
|
12
|
+
const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
|
|
13
|
+
onSelect(next);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (key.downArrow || input === "j") {
|
|
17
|
+
const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
|
|
18
|
+
onSelect(next);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (key.return) {
|
|
22
|
+
onActivate(selectedIndex);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}, { isActive: isFocused && navActive });
|
|
26
|
+
const { visible, localSelected } = useMemo(() => {
|
|
27
|
+
if (n === 0)
|
|
28
|
+
return { visible: [], localSelected: 0 };
|
|
29
|
+
const eff = Math.min(limit, n);
|
|
30
|
+
if (n <= eff) {
|
|
31
|
+
return { visible: items, localSelected: selectedIndex };
|
|
32
|
+
}
|
|
33
|
+
const offset = Math.floor(eff / 2);
|
|
34
|
+
const r = (((selectedIndex - offset) % n) + n) % n;
|
|
35
|
+
const rotated = [...items.slice(r), ...items.slice(0, r)];
|
|
36
|
+
return { visible: rotated.slice(0, eff), localSelected: offset };
|
|
37
|
+
}, [items, selectedIndex, limit, n]);
|
|
38
|
+
if (n === 0) {
|
|
39
|
+
throw new Error("FocusableList requires at least one item");
|
|
40
|
+
}
|
|
41
|
+
return (_jsx(Box, { flexDirection: "column", children: visible.map((item, i) => {
|
|
42
|
+
const isSelected = i === localSelected;
|
|
43
|
+
const indicator = isSelected ? "\u203a " : " ";
|
|
44
|
+
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 }), renderItem(item, isSelected)] }, i));
|
|
45
|
+
}) }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useFocus, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
const MAX_VISIBLE = 6;
|
|
6
|
+
export function FocusableSearchSelect(props) {
|
|
7
|
+
const { options, value, onChange } = props;
|
|
8
|
+
const placeholder = props.placeholder ?? "type to filter...";
|
|
9
|
+
const { isFocused } = useFocus();
|
|
10
|
+
const [filter, setFilter] = useState("");
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const filtered = useMemo(() => {
|
|
13
|
+
if (!filter)
|
|
14
|
+
return options;
|
|
15
|
+
const q = filter.toLowerCase();
|
|
16
|
+
return options.filter((o) => o.name.toLowerCase().includes(q) ||
|
|
17
|
+
o.value.toLowerCase().includes(q));
|
|
18
|
+
}, [options, filter]);
|
|
19
|
+
const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
20
|
+
const visibleStart = clampedIndex < MAX_VISIBLE
|
|
21
|
+
? 0
|
|
22
|
+
: clampedIndex - MAX_VISIBLE + 1;
|
|
23
|
+
const visible = filtered.slice(visibleStart, visibleStart + MAX_VISIBLE);
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (key.upArrow) {
|
|
26
|
+
setSelectedIndex((prev) => prev <= 0 ? filtered.length - 1 : prev - 1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (key.downArrow) {
|
|
30
|
+
setSelectedIndex((prev) => prev >= filtered.length - 1 ? 0 : prev + 1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.return) {
|
|
34
|
+
const option = filtered[clampedIndex];
|
|
35
|
+
if (option) {
|
|
36
|
+
onChange(option.value);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (key.escape) {
|
|
41
|
+
setFilter("");
|
|
42
|
+
setSelectedIndex(0);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (key.backspace || key.delete) {
|
|
46
|
+
setFilter((f) => f.slice(0, -1));
|
|
47
|
+
setSelectedIndex(0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (input &&
|
|
51
|
+
!key.ctrl &&
|
|
52
|
+
!key.meta &&
|
|
53
|
+
input.length === 1 &&
|
|
54
|
+
/[a-z0-9 _\-./]/i.test(input)) {
|
|
55
|
+
setFilter((f) => f + input);
|
|
56
|
+
setSelectedIndex(0);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}, { isActive: isFocused });
|
|
60
|
+
const borderColor = isFocused ? theme.accent.focus : theme.border.default;
|
|
61
|
+
const backgroundColor = isFocused
|
|
62
|
+
? theme.bg.input
|
|
63
|
+
: theme.bg.surface;
|
|
64
|
+
const selectedOption = options.find((o) => o.value === value);
|
|
65
|
+
const displayLabel = selectedOption?.name ?? placeholder;
|
|
66
|
+
if (!isFocused) {
|
|
67
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.dim, paddingLeft: 1, children: _jsx(Text, { color: value ? theme.text.primary : theme.text.muted, children: displayLabel }) }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: "/ " }), _jsx(Text, { color: filter ? theme.text.primary : theme.text.muted, children: filter || placeholder })] }), filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", children: visible.map((option, i) => {
|
|
70
|
+
const realIndex = visibleStart + i;
|
|
71
|
+
const isSelected = realIndex === clampedIndex;
|
|
72
|
+
const isActive = option.value === value;
|
|
73
|
+
const prefix = isSelected ? "\u203a " : " ";
|
|
74
|
+
const bg = isSelected ? theme.bg.active : undefined;
|
|
75
|
+
const fg = isSelected
|
|
76
|
+
? theme.text.inverse
|
|
77
|
+
: isActive
|
|
78
|
+
? theme.accent.focus
|
|
79
|
+
: option.color ?? theme.text.secondary;
|
|
80
|
+
return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, option.name] }) }, option.value));
|
|
81
|
+
}) })) : null] }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
function modeBadgeColor(mode) {
|
|
5
|
+
switch (mode) {
|
|
6
|
+
case "normal": return "#38bdf8";
|
|
7
|
+
case "search": return "#38bdf8";
|
|
8
|
+
case "create": return "#38bdf8";
|
|
9
|
+
case "task": return "#a78bfa";
|
|
10
|
+
case "help": return "#38bdf8";
|
|
11
|
+
case "confirm": return "#f87171";
|
|
12
|
+
case "projects": return "#34d399";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function modeLabelText(mode) {
|
|
16
|
+
switch (mode) {
|
|
17
|
+
case "normal": return "Loops";
|
|
18
|
+
case "search": return "Search";
|
|
19
|
+
case "create": return "Create";
|
|
20
|
+
case "task": return "Tasks";
|
|
21
|
+
case "help": return "Help";
|
|
22
|
+
case "confirm": return "Confirm";
|
|
23
|
+
case "projects": return "Projects";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function modeHints(mode) {
|
|
27
|
+
switch (mode) {
|
|
28
|
+
case "normal": return "Tab:panel /:search h:help Ctrl+C:quit";
|
|
29
|
+
case "search": return "Enter:apply Esc:cancel";
|
|
30
|
+
case "create": return "Tab:next Enter:save Esc:cancel";
|
|
31
|
+
case "task": return "Tab:next Enter:save Esc:cancel";
|
|
32
|
+
case "help": return "Esc:close";
|
|
33
|
+
case "confirm": return "arrows:choose Enter:confirm y/n Esc:cancel";
|
|
34
|
+
case "projects": return "Tab:navigate n:new e:edit d:delete Esc:back";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function Footer(props) {
|
|
38
|
+
const badgeColor = modeBadgeColor(props.mode);
|
|
39
|
+
const label = modeLabelText(props.mode);
|
|
40
|
+
const hints = modeHints(props.mode);
|
|
41
|
+
return (_jsxs(Box, { height: 1, justifyContent: "space-between", children: [_jsx(Box, { children: _jsxs(Text, { backgroundColor: badgeColor, color: theme.bg.base, bold: true, children: [" ", label, " "] }) }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: hints }) })] }));
|
|
42
|
+
}
|
|
@@ -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, useStdout } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { HEADER_COMPACT_WIDTH } from "../../config/constants.js";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
import { TabBar } from "./TabBar.js";
|
|
8
|
+
function daemonSymbol(status) {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case "connected": return "\u25CF";
|
|
11
|
+
case "starting": return "\u25CB";
|
|
12
|
+
case "error": return "\u2717";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function daemonColor(status) {
|
|
16
|
+
switch (status) {
|
|
17
|
+
case "connected": return theme.semantic.success;
|
|
18
|
+
case "starting": return theme.semantic.warning;
|
|
19
|
+
case "error": return theme.semantic.danger;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function daemonText(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case "connected": return "connected";
|
|
25
|
+
case "starting": return "starting";
|
|
26
|
+
case "error": return "offline";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function Header(props) {
|
|
30
|
+
const { stdout } = useStdout();
|
|
31
|
+
const width = stdout?.columns ?? 80;
|
|
32
|
+
const compact = width < HEADER_COMPACT_WIDTH;
|
|
33
|
+
const entries = [
|
|
34
|
+
{ label: t("board.runningLabel"), value: props.counts.running, color: theme.semantic.success },
|
|
35
|
+
{ label: t("board.waitingLabel"), value: props.counts.waiting, color: theme.accent.loop },
|
|
36
|
+
{ label: t("board.pausedLabel"), value: props.counts.paused, color: theme.semantic.warning },
|
|
37
|
+
{ label: t("board.idleLabel"), value: props.counts.idle, color: theme.semantic.idle },
|
|
38
|
+
];
|
|
39
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 { Modal } from "./Modal.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
export function HelpGuideModal(props) {
|
|
7
|
+
useInput((_input, key) => {
|
|
8
|
+
if (key.escape || _input === "q") {
|
|
9
|
+
props.onClose();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
const sections = [
|
|
13
|
+
{ title: "", body: t("helpGuide.intro") },
|
|
14
|
+
{ title: t("helpGuide.commands"), body: t("helpGuide.examples") },
|
|
15
|
+
{ title: t("helpGuide.allCommands"), body: t("helpGuide.allCommands") },
|
|
16
|
+
{ title: t("helpGuide.confirm"), body: t("helpGuide.confirm") },
|
|
17
|
+
{ title: t("helpGuide.navigation"), body: t("helpGuide.navigation") },
|
|
18
|
+
{ title: t("helpGuide.ctrl"), body: t("helpGuide.ctrl") },
|
|
19
|
+
];
|
|
20
|
+
return (_jsxs(Modal, { title: t("helpGuide.title"), onClose: props.onClose, width: "70%", children: [sections.map((section, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [section.title.length > 0 ? (_jsx(Text, { color: theme.accent.brand, bold: true, children: section.title })) : null, _jsx(Text, { color: theme.text.primary, children: section.body })] }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text.muted, children: t("helpGuide.esc") }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
import { Modal } from "./Modal.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
function helpEntries(view) {
|
|
7
|
+
switch (view) {
|
|
8
|
+
case "board":
|
|
9
|
+
return [
|
|
10
|
+
{ key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
|
|
11
|
+
{ key: t("board.helpKeyEnter"), desc: t("board.helpToggleDetail") },
|
|
12
|
+
{ key: t("board.helpKeyN"), desc: t("board.helpCreate") },
|
|
13
|
+
{ key: t("board.helpKeyE"), desc: t("board.helpEdit") },
|
|
14
|
+
{ key: t("board.helpKeyP"), desc: t("board.helpPauseResume") },
|
|
15
|
+
{ key: t("board.helpKeyX"), desc: t("board.helpForceRun") },
|
|
16
|
+
{ key: t("board.helpKeyD"), desc: t("board.helpDelete") },
|
|
17
|
+
{ key: t("board.helpKeySlash"), desc: t("board.helpSearch") },
|
|
18
|
+
{ key: t("board.helpKeyF"), desc: t("board.helpCycleFilter") },
|
|
19
|
+
{ key: t("board.helpKeyS"), desc: t("board.helpCycleSort") },
|
|
20
|
+
{ key: t("board.helpKeyLeftRight"), desc: t("board.helpSwitchPanel") },
|
|
21
|
+
{ key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
|
|
22
|
+
{ key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
|
|
23
|
+
{ key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
|
|
24
|
+
];
|
|
25
|
+
case "tasks":
|
|
26
|
+
return [
|
|
27
|
+
{ key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
|
|
28
|
+
{ key: t("board.helpKeyEnter"), desc: t("board.taskActionSelect") },
|
|
29
|
+
{ key: t("board.helpKeyN"), desc: t("board.helpCreateTask") },
|
|
30
|
+
{ key: t("board.helpKeyE"), desc: t("board.taskActionEdit") },
|
|
31
|
+
{ key: t("board.helpKeyD"), desc: t("board.actionDelete") },
|
|
32
|
+
{ key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
|
|
33
|
+
{ key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
|
|
34
|
+
{ key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
|
|
35
|
+
];
|
|
36
|
+
case "projects":
|
|
37
|
+
return [
|
|
38
|
+
{ key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
|
|
39
|
+
{ key: t("board.helpKeyEnter"), desc: t("project.editProjectLabel") },
|
|
40
|
+
{ key: t("board.helpKeyN"), desc: t("project.keyNewHint") },
|
|
41
|
+
{ key: t("board.helpKeyE"), desc: t("project.keyEditHint") },
|
|
42
|
+
{ key: t("board.helpKeyD"), desc: t("project.keyDeleteHint") },
|
|
43
|
+
{ key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
|
|
44
|
+
{ key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
|
|
45
|
+
{ key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
|
|
46
|
+
];
|
|
47
|
+
default:
|
|
48
|
+
return [
|
|
49
|
+
{ key: t("board.hintKeyTab"), desc: t("board.hintNextField") },
|
|
50
|
+
{ key: t("board.helpKeyEnter"), desc: t("board.hintApply") },
|
|
51
|
+
{ key: t("board.helpKeyEsc"), desc: t("board.hintCancel") },
|
|
52
|
+
{ key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function HelpModal(props) {
|
|
57
|
+
const entries = helpEntries(props.view);
|
|
58
|
+
return (_jsx(Modal, { title: "Shortcuts", onClose: props.onClose, width: "60%", children: entries.map((entry, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent.brand, children: entry.key.padEnd(16) }), _jsx(Text, { color: theme.text.primary, children: entry.desc })] }, i))) }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } 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
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
7
|
+
import { sanitizePaste } from "../utils/paste.js";
|
|
8
|
+
const MAX_VISIBLE = 8;
|
|
9
|
+
export function InlineCommandEditor({ value, hint, isActive, onChange, }) {
|
|
10
|
+
const [lines, setLines] = useState(() => value ? value.split("\n") : [""]);
|
|
11
|
+
const [cursorRow, setCursorRow] = useState(0);
|
|
12
|
+
const [cursorCol, setCursorCol] = useState(0);
|
|
13
|
+
const emit = useCallback((nextLines) => {
|
|
14
|
+
onChange(nextLines.join("\n"));
|
|
15
|
+
}, [onChange]);
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
// Ctrl+Y: copy full command text
|
|
18
|
+
if (key.ctrl && input === "y") {
|
|
19
|
+
copyToClipboard(value);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Let WizardForm handle these
|
|
23
|
+
if (key.ctrl || key.escape || key.tab)
|
|
24
|
+
return;
|
|
25
|
+
if (key.return) {
|
|
26
|
+
const next = [...lines];
|
|
27
|
+
const line = next[cursorRow] ?? "";
|
|
28
|
+
next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
|
|
29
|
+
setLines(next);
|
|
30
|
+
setCursorRow((r) => r + 1);
|
|
31
|
+
setCursorCol(0);
|
|
32
|
+
emit(next);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.upArrow) {
|
|
36
|
+
if (cursorRow > 0) {
|
|
37
|
+
setCursorRow((r) => r - 1);
|
|
38
|
+
setCursorCol((c) => Math.min(c, (lines[cursorRow - 1] ?? "").length));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.downArrow) {
|
|
43
|
+
if (cursorRow < lines.length - 1) {
|
|
44
|
+
setCursorRow((r) => r + 1);
|
|
45
|
+
setCursorCol((c) => Math.min(c, (lines[cursorRow + 1] ?? "").length));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (key.leftArrow) {
|
|
50
|
+
if (cursorCol > 0)
|
|
51
|
+
setCursorCol((c) => c - 1);
|
|
52
|
+
else if (cursorRow > 0) {
|
|
53
|
+
setCursorRow((r) => r - 1);
|
|
54
|
+
setCursorCol((lines[cursorRow - 1] ?? "").length);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.rightArrow) {
|
|
59
|
+
const curLen = (lines[cursorRow] ?? "").length;
|
|
60
|
+
if (cursorCol < curLen)
|
|
61
|
+
setCursorCol((c) => c + 1);
|
|
62
|
+
else if (cursorRow < lines.length - 1) {
|
|
63
|
+
setCursorRow((r) => r + 1);
|
|
64
|
+
setCursorCol(0);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.backspace || key.delete) {
|
|
69
|
+
if (cursorCol > 0) {
|
|
70
|
+
const next = [...lines];
|
|
71
|
+
const line = next[cursorRow] ?? "";
|
|
72
|
+
next[cursorRow] = line.slice(0, cursorCol - 1) + line.slice(cursorCol);
|
|
73
|
+
setLines(next);
|
|
74
|
+
setCursorCol((c) => c - 1);
|
|
75
|
+
emit(next);
|
|
76
|
+
}
|
|
77
|
+
else if (cursorRow > 0) {
|
|
78
|
+
const next = [...lines];
|
|
79
|
+
const prev = next[cursorRow - 1] ?? "";
|
|
80
|
+
const cur = next[cursorRow] ?? "";
|
|
81
|
+
next.splice(cursorRow - 1, 2, prev + cur);
|
|
82
|
+
setLines(next);
|
|
83
|
+
setCursorRow((r) => r - 1);
|
|
84
|
+
setCursorCol(prev.length);
|
|
85
|
+
emit(next);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
|
|
90
|
+
if (input.includes("\x1b[200~")) {
|
|
91
|
+
const pasted = sanitizePaste(input);
|
|
92
|
+
if (pasted) {
|
|
93
|
+
const next = [...lines];
|
|
94
|
+
const line = next[cursorRow] ?? "";
|
|
95
|
+
next[cursorRow] = line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
96
|
+
setLines(next);
|
|
97
|
+
setCursorCol((c) => c + pasted.length);
|
|
98
|
+
emit(next);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Multi-char containing CR/LF with no bracketed markers — ignore
|
|
103
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
104
|
+
return;
|
|
105
|
+
// Multi-char printable input = unbracketed single-line paste (e.g. right-click)
|
|
106
|
+
if (input.length > 1 && !key.meta) {
|
|
107
|
+
const pasted = sanitizePaste(input);
|
|
108
|
+
if (pasted) {
|
|
109
|
+
const next = [...lines];
|
|
110
|
+
const line = next[cursorRow] ?? "";
|
|
111
|
+
next[cursorRow] = line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
112
|
+
setLines(next);
|
|
113
|
+
setCursorCol((c) => c + pasted.length);
|
|
114
|
+
emit(next);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
119
|
+
const next = [...lines];
|
|
120
|
+
const line = next[cursorRow] ?? "";
|
|
121
|
+
next[cursorRow] = line.slice(0, cursorCol) + input + line.slice(cursorCol);
|
|
122
|
+
setLines(next);
|
|
123
|
+
setCursorCol((c) => c + 1);
|
|
124
|
+
emit(next);
|
|
125
|
+
}
|
|
126
|
+
}, { isActive });
|
|
127
|
+
const scrollStart = Math.max(0, cursorRow - MAX_VISIBLE + 1);
|
|
128
|
+
const visible = lines.slice(scrollStart, scrollStart + MAX_VISIBLE);
|
|
129
|
+
const lineNumWidth = String(lines.length).length;
|
|
130
|
+
const isEmpty = lines.length === 1 && lines[0] === "";
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: isActive ? theme.accent.brand : theme.border.dim, backgroundColor: isActive ? theme.bg.input : undefined, width: "100%", paddingX: 1, children: [isEmpty && !isActive ? (_jsx(Text, { color: theme.text.muted, children: hint })) : (visible.map((line, visIdx) => {
|
|
132
|
+
const rowIdx = scrollStart + visIdx;
|
|
133
|
+
const isCursor = isActive && rowIdx === cursorRow;
|
|
134
|
+
const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
|
|
135
|
+
if (isCursor) {
|
|
136
|
+
const before = line.slice(0, cursorCol);
|
|
137
|
+
const cur = cursorCol < line.length ? line[cursorCol] : " ";
|
|
138
|
+
const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
|
|
139
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.primary, children: before }), _jsx(Text, { inverse: true, children: cur }), _jsx(Text, { color: theme.text.primary, children: after })] }, rowIdx));
|
|
140
|
+
}
|
|
141
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.secondary, children: line })] }, rowIdx));
|
|
142
|
+
})), isActive ? (_jsx(Box, { marginTop: 0, children: _jsx(Text, { color: theme.text.muted, children: t("cmdEditor.inlineHint") }) })) : null] }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme, statusColor } from "../theme.js";
|
|
4
|
+
import { describeLoop, commandLine, timeAgo, timeUntil } from "../format.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
const LABEL_WIDTH = 11;
|
|
7
|
+
function Field(props) {
|
|
8
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
|
|
9
|
+
}
|
|
10
|
+
const DIVIDER = "\u2500".repeat(40);
|
|
11
|
+
/** Muted-label field for identity block (de-emphasized). */
|
|
12
|
+
function MutedField(props) {
|
|
13
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: props.children })] }));
|
|
14
|
+
}
|
|
15
|
+
export function Inspector(props) {
|
|
16
|
+
const { loop } = props;
|
|
17
|
+
if (!loop) {
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
|
|
19
|
+
}
|
|
20
|
+
const sColor = statusColor(loop.status);
|
|
21
|
+
const maxRunsLabel = loop.maxRuns ? String(loop.maxRuns) : t("board.unlimited");
|
|
22
|
+
const lastRun = loop.lastRunAt ? timeAgo(loop.lastRunAt) : t("format.dash");
|
|
23
|
+
const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
|
|
24
|
+
const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
|
|
25
|
+
const pid = loop.pid ? String(loop.pid) : t("format.dash");
|
|
26
|
+
// Dedupe: when description matches the full command line, skip Command field
|
|
27
|
+
const fullCmd = commandLine(loop.command, loop.commandArgs);
|
|
28
|
+
const desc = describeLoop(loop);
|
|
29
|
+
const showCommand = desc !== fullCmd;
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldDir"), children: _jsx(Text, { color: theme.text.primary, children: loop.cwd }) }), _jsx(MutedField, { label: t("board.fieldId"), children: loop.id }), _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), showCommand && _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd }), _jsx(MutedField, { label: t("board.fieldTask"), children: loop.taskId ?? t("format.dash") }), _jsx(MutedField, { label: t("board.fieldPid"), children: pid })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
|
|
31
|
+
}
|