loop-task 2.1.4 → 2.1.5
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/dist/app/App.js +119 -0
- package/dist/{tui → app}/index.js +4 -5
- package/dist/app/providers/index.js +1 -0
- package/dist/cli/import-writer.js +1 -1
- package/dist/cli.js +9 -9
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +2 -164
- package/dist/client/ipc.js +3 -3
- package/dist/client/project-commands.js +159 -0
- package/dist/core/{command-runner.js → command/command-runner.js} +3 -3
- package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
- package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
- package/dist/core/loop/chain-executor.js +67 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +191 -0
- package/dist/core/loop/loop-runner.js +133 -0
- package/dist/core/loop/run-executor.js +90 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/daemon/daemon-log.js +1 -1
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +92 -0
- package/dist/daemon/http/route-loops.js +218 -0
- package/dist/daemon/http/route-misc.js +36 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +53 -0
- package/dist/daemon/http/routes.js +17 -0
- package/dist/daemon/http/server.js +64 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +7 -9
- package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
- package/dist/daemon/managers/loop-entry.js +24 -0
- package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
- package/dist/daemon/managers/loop-options.js +16 -0
- package/dist/daemon/managers/loop-serialization.js +43 -0
- package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
- package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
- package/dist/daemon/server/handlers/index.js +86 -0
- package/dist/daemon/server/handlers/log-handlers.js +130 -0
- package/dist/daemon/server/handlers/loop-handlers.js +54 -0
- package/dist/daemon/server/handlers/project-handlers.js +37 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/index.js +113 -0
- package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
- package/dist/daemon/{state.js → state/index.js} +4 -4
- package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
- package/dist/duration.js +1 -1
- package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
- package/dist/entities/loops/index.js +1 -0
- package/dist/entities/projects/filters.js +69 -0
- package/dist/entities/projects/index.js +1 -0
- package/dist/entities/tasks/filters.js +41 -0
- package/dist/entities/tasks/index.js +1 -0
- package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
- package/dist/features/code-editor/useEditorKeyboard.js +147 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/{tui → features/commands}/commands.js +2 -2
- package/dist/features/commands/useCommandHandlers.js +147 -0
- package/dist/features/commands/useContextualActions.js +75 -0
- package/dist/features/commands/useGlobalShortcuts.js +152 -0
- package/dist/features/forms/FormRouter.js +44 -0
- package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
- package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
- package/dist/features/overlays/OverlayStack.js +9 -0
- package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
- package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
- package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
- package/dist/features/overlays/useOverlayStack.js +51 -0
- package/dist/features/state/useAppState.js +150 -0
- package/dist/logger.js +0 -1
- package/dist/loop-config.js +9 -4
- package/dist/{config → shared/config}/constants.js +1 -1
- package/dist/shared/container/index.js +22 -0
- package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
- package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
- package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
- package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
- package/dist/shared/providers/InversifyProvider.js +6 -0
- package/dist/shared/services/export-service.js +43 -0
- package/dist/shared/services/log-service.js +28 -0
- package/dist/shared/services/loop-service.js +78 -0
- package/dist/shared/services/project-service.js +39 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +7 -0
- package/dist/{tui/components → shared/ui}/Button.js +1 -1
- package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
- package/dist/{tui/components → shared/ui}/Modal.js +1 -1
- package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
- package/dist/{tui/components → shared/ui}/Toast.js +1 -1
- package/dist/shared/ui/state.js +9 -0
- package/dist/{tui → shared}/utils/paste.js +2 -2
- package/dist/{tui → shared}/utils/validation.js +1 -1
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +129 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +68 -0
- package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
- package/dist/{tui/components → widgets/header}/Header.js +3 -3
- package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
- package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
- package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
- package/dist/widgets/loop-form/CreateForm.js +75 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
- package/dist/widgets/loop-form/useCreateSteps.js +107 -0
- package/dist/widgets/loop-form/useHandleComplete.js +74 -0
- package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
- package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
- package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
- package/package.json +2 -1
- package/dist/core/loop-controller.js +0 -511
- package/dist/daemon/http-server.js +0 -599
- package/dist/daemon/server.js +0 -374
- package/dist/tui/App.js +0 -680
- package/dist/tui/components/CodeEditorModal.js +0 -244
- package/dist/tui/components/CommandInput.js +0 -273
- package/dist/tui/components/CreateForm.js +0 -224
- package/dist/tui/daemon.js +0 -142
- package/dist/tui/format.js +0 -111
- /package/dist/{tui/router.js → app/router/index.js} +0 -0
- /package/dist/{tui → app}/types.js +0 -0
- /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
- /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
- /package/dist/core/{template.js → context/template.js} +0 -0
- /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
- /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
- /package/dist/{ipc → daemon/ipc}/send.js +0 -0
- /package/dist/{config → shared/config}/paths.js +0 -0
- /package/dist/{i18n → shared/i18n}/en.json +0 -0
- /package/dist/{i18n → shared/i18n}/index.js +0 -0
- /package/dist/{board → shared/ui}/format.js +0 -0
- /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
- /package/dist/{tui → shared/ui}/theme.js +0 -0
- /package/dist/{tui → shared}/utils/syntax.js +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { defaultFilters, applyLoopFilters, cycleSortMode, cycleStatusFilter } from "../../entities/loops/filters.js";
|
|
2
|
+
export { defaultProjectFilters, applyProjectFilters, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter } from "../../entities/projects/filters.js";
|
|
3
|
+
export function resolveInputOwner(state) {
|
|
4
|
+
if (state.modalOpen)
|
|
5
|
+
return "modal";
|
|
6
|
+
if (state.commandBarHasText || state.commandBarDropdownOpen)
|
|
7
|
+
return "commandBar";
|
|
8
|
+
return "panel";
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PASTE_MAX_CHARS } from "
|
|
2
|
-
import { CODE_EDITOR_WRAP_LENGTH } from "
|
|
1
|
+
import { PASTE_MAX_CHARS } from "../config/constants.js";
|
|
2
|
+
import { CODE_EDITOR_WRAP_LENGTH } from "../config/constants.js";
|
|
3
3
|
export function sanitizePaste(raw) {
|
|
4
4
|
return raw
|
|
5
5
|
.replace(/\x1b\[20[01]~/g, "")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { parseDuration } from "../../duration.js";
|
|
3
3
|
import { parseMaxRuns } from "../../loop-config.js";
|
|
4
|
-
import { t } from "
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
5
|
const validators = {
|
|
6
6
|
name(value) {
|
|
7
7
|
if (!value.trim())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
|
+
import { t } from "../../shared/i18n/index.js";
|
|
5
|
+
export function renderInputLine(value, cursorOffset) {
|
|
6
|
+
if (value.length === 0) {
|
|
7
|
+
return "\x1b[7m \x1b[27m";
|
|
8
|
+
}
|
|
9
|
+
let result = "";
|
|
10
|
+
for (let i = 0; i < value.length; i++) {
|
|
11
|
+
if (i === cursorOffset) {
|
|
12
|
+
result += "\x1b[7m" + value[i] + "\x1b[27m";
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
result += value[i];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (cursorOffset >= value.length) {
|
|
19
|
+
result += "\x1b[7m \x1b[27m";
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function renderHighlightedLabel(label, matchRanges, isFocused) {
|
|
24
|
+
if (matchRanges.length === 0) {
|
|
25
|
+
return isFocused
|
|
26
|
+
? `\x1b[38;2;251;191;36m${label}\x1b[39m`
|
|
27
|
+
: label;
|
|
28
|
+
}
|
|
29
|
+
let result = "";
|
|
30
|
+
let pos = 0;
|
|
31
|
+
for (const range of matchRanges) {
|
|
32
|
+
if (pos < range.start) {
|
|
33
|
+
const segment = label.slice(pos, range.start);
|
|
34
|
+
result += isFocused
|
|
35
|
+
? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
|
|
36
|
+
: segment;
|
|
37
|
+
}
|
|
38
|
+
const matched = label.slice(range.start, range.end);
|
|
39
|
+
result += `\x1b[1m\x1b[38;2;251;191;36m${matched}\x1b[39m\x1b[22m`;
|
|
40
|
+
pos = range.end;
|
|
41
|
+
}
|
|
42
|
+
if (pos < label.length) {
|
|
43
|
+
const segment = label.slice(pos);
|
|
44
|
+
result += isFocused
|
|
45
|
+
? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
|
|
46
|
+
: segment;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export function CommandDropdown({ state, rankedFiltered, }) {
|
|
51
|
+
if (!state.isOpen || state.isLoading || state.error)
|
|
52
|
+
return null;
|
|
53
|
+
const filtered = rankedFiltered;
|
|
54
|
+
const visibleOptions = filtered.slice(state.visibleFromIndex, state.visibleToIndex);
|
|
55
|
+
if (visibleOptions.length === 0) {
|
|
56
|
+
if (state.inputValue.length > 0) {
|
|
57
|
+
return (_jsx(Box, { paddingLeft: 3, position: "absolute", bottom: 3, borderStyle: "single", borderColor: theme.border.dim, children: _jsx(Text, { color: theme.text.muted, children: t("cmdInput.noMatches") }) }));
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const aboveCount = state.visibleFromIndex;
|
|
62
|
+
const belowCount = filtered.length - state.visibleToIndex;
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 3, position: "absolute", bottom: 3, children: [aboveCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2191 ${aboveCount} more` })), visibleOptions.map((match, i) => {
|
|
64
|
+
const actualIndex = state.visibleFromIndex + i;
|
|
65
|
+
const isFocused = actualIndex === state.focusedIndex;
|
|
66
|
+
const label = renderHighlightedLabel(match.option.label, match.matchRanges, isFocused);
|
|
67
|
+
return (_jsxs(Box, { backgroundColor: isFocused ? theme.bg.active : undefined, children: [_jsx(Text, { color: isFocused ? theme.text.inverse : theme.text.muted, children: isFocused ? "\u276f " : " " }), _jsx(Text, { color: isFocused ? theme.text.inverse : undefined, children: label })] }, match.option.value));
|
|
68
|
+
}), belowCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2193 ${belowCount} more` }))] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { useAutocompleteState, } from "ink-combobox";
|
|
5
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
6
|
+
import { t } from "../../shared/i18n/index.js";
|
|
7
|
+
import { buildCommands, rankCommands } from "../../features/commands/commands.js";
|
|
8
|
+
import { COMMAND_INPUT_DROPDOWN_MAX_VISIBLE, COMMAND_INPUT_HEIGHT, } from "../../shared/config/constants.js";
|
|
9
|
+
import { sanitizePaste } from "../../shared/utils/paste.js";
|
|
10
|
+
import { CommandDropdown, renderInputLine } from "./CommandDropdown.js";
|
|
11
|
+
import { HintBar, KeyHint } from "./HintBar.js";
|
|
12
|
+
import { ConfirmMode, SearchMode } from "./InputModes.js";
|
|
13
|
+
export { sanitizePaste } from "../../shared/utils/paste.js";
|
|
14
|
+
function CommandMode({ context, onCommand, onCopy, onPanelAction, disabled, navOwner, onInputStateChange, }) {
|
|
15
|
+
const commands = useMemo(() => buildCommands(context), [context]);
|
|
16
|
+
const options = useMemo(() => commands.map((cmd) => ({ label: cmd.label, value: cmd.value })), [commands]);
|
|
17
|
+
const { state, dispatch } = useAutocompleteState({
|
|
18
|
+
options,
|
|
19
|
+
visibleOptionCount: COMMAND_INPUT_DROPDOWN_MAX_VISIBLE,
|
|
20
|
+
onSelect: onCommand,
|
|
21
|
+
});
|
|
22
|
+
const rankedFiltered = useMemo(() => {
|
|
23
|
+
const fo = state.filteredOptions;
|
|
24
|
+
if (fo.length === 0 || state.inputValue.length === 0)
|
|
25
|
+
return fo;
|
|
26
|
+
const byValue = new Map();
|
|
27
|
+
for (const m of fo)
|
|
28
|
+
byValue.set(m.option.value, m);
|
|
29
|
+
const ranked = rankCommands(state.inputValue, fo.map((m) => ({ label: m.option.label, value: m.option.value })));
|
|
30
|
+
return ranked
|
|
31
|
+
.map((opt) => byValue.get(opt.value))
|
|
32
|
+
.filter((m) => m !== undefined);
|
|
33
|
+
}, [state.filteredOptions, state.inputValue]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onInputStateChange?.(state.inputValue.length > 0, state.isOpen);
|
|
36
|
+
}, [state.inputValue, state.isOpen, onInputStateChange]);
|
|
37
|
+
const insertText = useCallback((text) => {
|
|
38
|
+
for (const ch of text)
|
|
39
|
+
dispatch({ type: "INSERT_TEXT", text: ch });
|
|
40
|
+
}, [dispatch]);
|
|
41
|
+
const clearInput = useCallback(() => {
|
|
42
|
+
dispatch({ type: "MOVE_CURSOR_END" });
|
|
43
|
+
for (let i = 0; i < state.inputValue.length; i++)
|
|
44
|
+
dispatch({ type: "DELETE_BACKWARD" });
|
|
45
|
+
}, [dispatch, state.inputValue.length]);
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (input.includes("\x1b[200~")) {
|
|
48
|
+
insertText(sanitizePaste(input));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ((key.ctrl && input === "u") || input === "\x15") {
|
|
52
|
+
clearInput();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (key.ctrl)
|
|
56
|
+
return;
|
|
57
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
58
|
+
return;
|
|
59
|
+
if (navOwner === "panel" && state.inputValue.length === 0 && !state.isOpen) {
|
|
60
|
+
if (input === "j" || input === "k" || key.upArrow || key.downArrow) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (onCopy && input === "c" && !state.isOpen) {
|
|
65
|
+
onCopy();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.escape) {
|
|
69
|
+
dispatch({ type: "CLOSE" });
|
|
70
|
+
if (state.inputValue.length > 0) {
|
|
71
|
+
clearInput();
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.return) {
|
|
76
|
+
if (state.isOpen && rankedFiltered.length > 0 && state.focusedIndex < rankedFiltered.length) {
|
|
77
|
+
const focused = rankedFiltered[state.focusedIndex];
|
|
78
|
+
dispatch({ type: "CLOSE" });
|
|
79
|
+
for (let i = 0; i <= state.inputValue.length; i++) {
|
|
80
|
+
dispatch({ type: "DELETE_BACKWARD" });
|
|
81
|
+
}
|
|
82
|
+
onCommand(focused.option.value);
|
|
83
|
+
}
|
|
84
|
+
else if (state.inputValue.length === 0) {
|
|
85
|
+
onPanelAction?.();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.tab) {
|
|
90
|
+
dispatch({ type: "ACCEPT" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.downArrow) {
|
|
94
|
+
dispatch({ type: "FOCUS_NEXT" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.upArrow) {
|
|
98
|
+
dispatch({ type: "FOCUS_PREV" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.backspace || key.delete) {
|
|
102
|
+
dispatch({ type: "DELETE_BACKWARD" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.leftArrow) {
|
|
106
|
+
dispatch({ type: "MOVE_CURSOR_LEFT" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.rightArrow) {
|
|
110
|
+
dispatch({ type: "MOVE_CURSOR_RIGHT" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (input.length > 1 && !key.meta) {
|
|
114
|
+
insertText(sanitizePaste(input));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (input.length === 1 && !key.ctrl && !key.meta && !key.return && !key.tab && !key.escape && input >= " " && input <= "~") {
|
|
118
|
+
dispatch({ type: "INSERT_TEXT", text: input });
|
|
119
|
+
}
|
|
120
|
+
}, { isActive: !disabled });
|
|
121
|
+
const isEmpty = state.inputValue.length === 0;
|
|
122
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
123
|
+
const inputContent = isEmpty ? cursor : renderInputLine(state.inputValue, state.cursorOffset);
|
|
124
|
+
return (_jsxs(_Fragment, { children: [_jsx(CommandDropdown, { state: state, rankedFiltered: rankedFiltered }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent.brand, children: "│ " }), isEmpty ? (_jsx(Text, { color: theme.text.muted, children: t("cmdInput.placeholder") })) : (_jsx(Text, { children: inputContent }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsxs(Box, { children: [_jsx(KeyHint, { keyLabel: "enter", action: "edit/logs" }), _jsx(KeyHint, { keyLabel: "ctrl+u", action: "clear" }), _jsx(KeyHint, { keyLabel: "c", action: "copy" }), _jsx(KeyHint, { keyLabel: "tab", action: "panels" }), _jsx(KeyHint, { keyLabel: "ctrl+\u2190\u2192", action: "tabs" }), _jsx(KeyHint, { keyLabel: "ctrl+p", action: "commands" })] }) })] }));
|
|
125
|
+
}
|
|
126
|
+
export function CommandInput(props) {
|
|
127
|
+
const { context, onCommand, confirmState, searchState, searchValue, onSearchChange, onSearchSubmit, onSearchCancel, onConfirmYes, onConfirmCancel, onCopy, onPanelAction, navOwner, onInputStateChange, } = props;
|
|
128
|
+
return (_jsx(Box, { flexDirection: "column", height: COMMAND_INPUT_HEIGHT, borderStyle: "single", borderColor: theme.border.dim, paddingY: 1, children: searchState?.active ? (_jsx(SearchMode, { value: searchValue, onSearchChange: onSearchChange, onSearchSubmit: onSearchSubmit, onSearchCancel: onSearchCancel, disabled: props.disabled })) : confirmState === null ? (_jsx(CommandMode, { context: context, onCommand: onCommand, onCopy: onCopy, onPanelAction: onPanelAction, disabled: props.disabled, navOwner: navOwner, onInputStateChange: onInputStateChange })) : (_jsx(ConfirmMode, { confirmState: confirmState, onConfirmYes: onConfirmYes, onConfirmCancel: onConfirmCancel, disabled: props.disabled })) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
|
+
export function HintBar({ leftHint, rightHint, }) {
|
|
5
|
+
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Box, { children: leftHint }), _jsx(Box, { children: rightHint })] }));
|
|
6
|
+
}
|
|
7
|
+
export function KeyHint({ keyLabel, action }) {
|
|
8
|
+
return (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { bold: true, color: theme.text.primary, children: keyLabel }), _jsx(Text, { color: theme.text.muted, children: " " + action })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { HintBar, KeyHint } from "./HintBar.js";
|
|
7
|
+
export function ConfirmMode({ confirmState, onConfirmYes, onConfirmCancel, disabled, }) {
|
|
8
|
+
const [value, setValue] = useState("");
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (key.ctrl)
|
|
11
|
+
return;
|
|
12
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
13
|
+
return;
|
|
14
|
+
if (key.escape) {
|
|
15
|
+
setValue("");
|
|
16
|
+
onConfirmCancel();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (key.return) {
|
|
20
|
+
if (value.toLowerCase() === "yes") {
|
|
21
|
+
setValue("");
|
|
22
|
+
onConfirmYes();
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
setValue("");
|
|
26
|
+
onConfirmCancel();
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.backspace || key.delete) {
|
|
31
|
+
setValue((v) => v.slice(0, -1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
35
|
+
setValue((v) => v + input);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}, { isActive: !disabled });
|
|
39
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
40
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.semantic.danger, children: "│ " }), _jsx(Text, { color: theme.text.muted, children: confirmState.prompt + " " }), value.length > 0 ? (_jsx(Text, { children: value + cursor })) : (_jsx(Text, { children: cursor }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsx(KeyHint, { keyLabel: "enter", action: "confirm" }) })] }));
|
|
41
|
+
}
|
|
42
|
+
export function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, disabled, }) {
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (key.ctrl)
|
|
45
|
+
return;
|
|
46
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
47
|
+
return;
|
|
48
|
+
if (key.escape) {
|
|
49
|
+
onSearchCancel();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.return) {
|
|
53
|
+
onSearchSubmit();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.backspace || key.delete) {
|
|
57
|
+
onSearchChange(value.slice(0, -1));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
61
|
+
onSearchChange(value + input);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}, { isActive: !disabled });
|
|
65
|
+
const placeholder = t("cmdInput.searchPlaceholder");
|
|
66
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent.project, children: "│ " }), value.length > 0 ? (_jsx(Text, { children: value + cursor })) : (_jsx(Text, { color: theme.text.muted, children: placeholder }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsx(KeyHint, { keyLabel: "enter", action: "apply" }) })] }));
|
|
68
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useMemo } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import { t } from "../../i18n/index.js";
|
|
6
|
-
import { buildTabCommands } from "
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { buildTabCommands } from "../../features/commands/commands.js";
|
|
7
7
|
const CATEGORY_ORDER = ["global", "filters", "loop", "task", "project"];
|
|
8
8
|
function categoryLabel(cat) {
|
|
9
9
|
switch (cat) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import { HEADER_COMPACT_WIDTH } from "../../config/constants.js";
|
|
6
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { HEADER_COMPACT_WIDTH } from "../../shared/config/constants.js";
|
|
6
|
+
import { t } from "../../shared/i18n/index.js";
|
|
7
7
|
import { TabBar } from "./TabBar.js";
|
|
8
8
|
const VERSION = "v" + (process.env.npm_package_version ?? "dev");
|
|
9
9
|
function daemonSymbol(status) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { darkTheme as theme } from "
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
4
|
const TAB_DEFS = [
|
|
5
5
|
{ key: "loops", label: "Loops", color: theme.accent.loop },
|
|
6
6
|
{ key: "tasks", label: "Tasks", color: theme.accent.task },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
|
-
import { darkTheme as theme, tabAccentColor } from "
|
|
5
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
6
|
import { Navigator } from "./Navigator.js";
|
|
7
7
|
import { TaskNavigator } from "./TaskBrowser.js";
|
|
8
|
-
import { FocusableList } from "
|
|
8
|
+
import { FocusableList } from "../../shared/ui/FocusableList.js";
|
|
9
9
|
function Chip(props) {
|
|
10
10
|
const { label, value, highlighted } = props;
|
|
11
11
|
return (_jsxs(Text, { color: highlighted ? theme.text.primary : theme.text.muted, children: [label, " ", value] }));
|
|
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
4
|
import { ScrollList } from "ink-scroll-list";
|
|
5
|
-
import { darkTheme as theme, statusColor } from "
|
|
6
|
-
import { describeLoop, sinceLabel, statusLabel, timingLabel, truncate } from "
|
|
7
|
-
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
|
|
6
|
+
import { describeLoop, sinceLabel, statusLabel, timingLabel, truncate } from "../../shared/ui/format.js";
|
|
7
|
+
import { t } from "../../shared/i18n/index.js";
|
|
8
8
|
const DESC_WIDTH = 32;
|
|
9
9
|
const SINCE_WIDTH = 13;
|
|
10
10
|
const RUNS_WIDTH = 4;
|
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState } from "react";
|
|
3
|
-
import { Box, Text, useInput
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
}
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { FocusableButton } from "../../shared/ui/FocusableButton.js";
|
|
6
|
+
import { t } from "../../shared/i18n/index.js";
|
|
7
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
8
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
9
|
+
import { DeleteConfirm, ListFocusWrapper, loopCountFor } from "./ProjectsPageParts.js";
|
|
36
10
|
export function ProjectsPage(props) {
|
|
37
11
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
38
12
|
const [subModal, setSubModal] = useState("none");
|
|
13
|
+
const projectService = useInject(TYPES.ProjectService);
|
|
39
14
|
const defaultProject = props.projects.find((p) => p.isDefault) ?? null;
|
|
40
15
|
const defaultProjectId = defaultProject?.id ?? null;
|
|
41
16
|
const knownProjectIds = new Set(props.projects.map((p) => p.id));
|
|
@@ -71,7 +46,7 @@ export function ProjectsPage(props) {
|
|
|
71
46
|
if (!selected)
|
|
72
47
|
return;
|
|
73
48
|
try {
|
|
74
|
-
await
|
|
49
|
+
await projectService.delete(selected.id);
|
|
75
50
|
props.onToast(t("project.toastDeleted", { name: selected.name }));
|
|
76
51
|
await props.onRefresh();
|
|
77
52
|
setSubModal("none");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useFocus } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { FocusableList } from "../../shared/ui/FocusableList.js";
|
|
6
|
+
import { FocusableButton } from "../../shared/ui/FocusableButton.js";
|
|
7
|
+
import { Modal } from "../../shared/ui/Modal.js";
|
|
8
|
+
import { t } from "../../shared/i18n/index.js";
|
|
9
|
+
export function loopCountFor(loops, projectId, defaultProjectId, knownProjectIds) {
|
|
10
|
+
const direct = loops.filter((l) => l.projectId === projectId).length;
|
|
11
|
+
if (projectId === defaultProjectId) {
|
|
12
|
+
const orphans = loops.filter((l) => !knownProjectIds.has(l.projectId)).length;
|
|
13
|
+
return direct + orphans;
|
|
14
|
+
}
|
|
15
|
+
return direct;
|
|
16
|
+
}
|
|
17
|
+
export function DeleteConfirm(props) {
|
|
18
|
+
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 })] })] }));
|
|
19
|
+
}
|
|
20
|
+
export function ListFocusWrapper(props) {
|
|
21
|
+
const { isFocused } = useFocus();
|
|
22
|
+
if (props.projects.length === 0) {
|
|
23
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
|
|
24
|
+
}
|
|
25
|
+
const nameWidth = 20;
|
|
26
|
+
const loopsWidth = 12;
|
|
27
|
+
const headerSeparator = " \u00B7 ";
|
|
28
|
+
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) => {
|
|
29
|
+
const count = loopCountFor(props.loops, project.id, props.defaultProjectId, props.knownProjectIds);
|
|
30
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
31
|
+
const countFg = isSelected ? theme.text.inverse : theme.text.muted;
|
|
32
|
+
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] })] }));
|
|
33
|
+
} }), props.children] }));
|
|
34
|
+
}
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import { ScrollList } from "ink-scroll-list";
|
|
5
|
-
import { darkTheme as theme } from "
|
|
6
|
-
import { commandLine } from "
|
|
7
|
-
import { t } from "../../i18n/index.js";
|
|
8
|
-
import { FocusableButton } from "
|
|
5
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
6
|
+
import { commandLine } from "../../shared/ui/format.js";
|
|
7
|
+
import { t } from "../../shared/i18n/index.js";
|
|
8
|
+
import { FocusableButton } from "../../shared/ui/FocusableButton.js";
|
|
9
9
|
const NAME_WIDTH = 24;
|
|
10
10
|
const COMMAND_WIDTH = 32;
|
|
11
11
|
const LIMIT = 15;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import { t } from "../../i18n/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
7
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
8
|
+
import { formatDate } from "../../shared/ui/format.js";
|
|
8
9
|
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
9
10
|
const MAX_VISIBLE_LINES = 20;
|
|
10
11
|
function colorForLine(line, run) {
|
|
@@ -30,6 +31,7 @@ export function LogModal(props) {
|
|
|
30
31
|
const [searchQuery, setSearchQuery] = useState("");
|
|
31
32
|
const [follow, setFollow] = useState(true);
|
|
32
33
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
34
|
+
const logService = useInject(TYPES.LogService);
|
|
33
35
|
useEffect(() => {
|
|
34
36
|
setLines(props.logLines);
|
|
35
37
|
}, [props.logLines]);
|
|
@@ -39,11 +41,11 @@ export function LogModal(props) {
|
|
|
39
41
|
if (props.run.status !== "running")
|
|
40
42
|
return;
|
|
41
43
|
setStreaming(true);
|
|
42
|
-
const socket = streamRunLog(props.loopId, props.run.runNumber, (line) => setLines((prev) => [...prev, line]), () => setStreaming(false), () => setStreaming(false));
|
|
44
|
+
const socket = logService.streamRunLog(props.loopId, props.run.runNumber, (line) => setLines((prev) => [...prev, line]), () => setStreaming(false), () => setStreaming(false));
|
|
43
45
|
return () => {
|
|
44
46
|
socket.destroy();
|
|
45
47
|
};
|
|
46
|
-
}, [props.loopId, props.run.runNumber, props.run.status]);
|
|
48
|
+
}, [props.loopId, props.run.runNumber, props.run.status, logService]);
|
|
47
49
|
const filtered = searchQuery
|
|
48
50
|
? lines.filter((l) => l.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
49
51
|
: lines;
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { useInject } from "../../shared/hooks/useInject.js";
|
|
4
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { WizardForm } from "./WizardForm.js";
|
|
7
|
+
import { TaskPickerModal } from "../../features/overlays/TaskPickerModal.js";
|
|
8
|
+
import { SelectModal } from "../../shared/ui/SelectModal.js";
|
|
9
|
+
import { CodeEditorModal } from "../../features/code-editor/CodeEditorModal.js";
|
|
10
|
+
import { useCreateSteps } from "./useCreateSteps.js";
|
|
11
|
+
import { useHandleComplete } from "./useHandleComplete.js";
|
|
12
|
+
export function CreateView(props) {
|
|
13
|
+
const { mode, editId, initial, selectedTaskId, selectedTaskName, tasks, currentProjectId, onCancel, onDone, onChooseTask, } = props;
|
|
14
|
+
const loopService = useInject(TYPES.LoopService);
|
|
15
|
+
const [taskPickerOpen, setTaskPickerOpen] = useState(false);
|
|
16
|
+
const [openSelect, setOpenSelect] = useState(null);
|
|
17
|
+
const [commandEditorOpen, setCommandEditorOpen] = useState(false);
|
|
18
|
+
const [commandValue, setCommandValue] = useState(initial.command ?? "");
|
|
19
|
+
const fieldCallbacksRef = useRef({});
|
|
20
|
+
const taskModeInitial = initial.taskMode === "existing" ? "Existing task" : "Inline command";
|
|
21
|
+
const resolvedTaskName = useMemo(() => {
|
|
22
|
+
const tid = selectedTaskId ?? initial.taskId;
|
|
23
|
+
if (!tid)
|
|
24
|
+
return null;
|
|
25
|
+
const displayName = selectedTaskName ?? tasks.find((t) => t.id === tid)?.name;
|
|
26
|
+
if (displayName)
|
|
27
|
+
return `${displayName} (${tid.slice(0, 8)})`;
|
|
28
|
+
return `${tid.slice(0, 8)}`;
|
|
29
|
+
}, [selectedTaskName, selectedTaskId, initial.taskId, tasks]);
|
|
30
|
+
const steps = useCreateSteps({
|
|
31
|
+
initial,
|
|
32
|
+
selectedTaskId,
|
|
33
|
+
resolvedTaskName,
|
|
34
|
+
commandValue,
|
|
35
|
+
projects: props.projects,
|
|
36
|
+
setOpenSelect,
|
|
37
|
+
setTaskPickerOpen,
|
|
38
|
+
setCommandEditorOpen,
|
|
39
|
+
fieldCallbacksRef,
|
|
40
|
+
taskModeInitial,
|
|
41
|
+
});
|
|
42
|
+
const handleComplete = useHandleComplete({
|
|
43
|
+
selectedTaskId,
|
|
44
|
+
mode,
|
|
45
|
+
editId,
|
|
46
|
+
currentProjectId,
|
|
47
|
+
onDone,
|
|
48
|
+
commandValue,
|
|
49
|
+
projects: props.projects,
|
|
50
|
+
loopService,
|
|
51
|
+
});
|
|
52
|
+
const selectOptionsFor = useCallback((field) => {
|
|
53
|
+
if (field === "taskMode") {
|
|
54
|
+
return [t("wizard.taskModeInline"), t("wizard.taskModeExisting")].map((v) => ({ value: v, label: v }));
|
|
55
|
+
}
|
|
56
|
+
if (field === "runNow") {
|
|
57
|
+
return [t("wizard.runNowWait"), t("wizard.runNowNow")].map((v) => ({ value: v, label: v }));
|
|
58
|
+
}
|
|
59
|
+
return props.projects.map((p) => ({ value: p.name, label: p.name }));
|
|
60
|
+
}, [props.projects]);
|
|
61
|
+
const selectTitleFor = (field) => field === "taskMode" ? t("wizard.taskModePrompt")
|
|
62
|
+
: field === "runNow" ? t("wizard.runNowPrompt")
|
|
63
|
+
: t("wizard.projectPrompt");
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: mode === "edit" ? t("wizard.editLoop") : t("wizard.newLoop"), steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: taskPickerOpen || openSelect !== null || commandEditorOpen }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
|
|
65
|
+
onChooseTask({ id: task.id, name: task.name });
|
|
66
|
+
setTaskPickerOpen(false);
|
|
67
|
+
}, onClose: () => setTaskPickerOpen(false) })) : null, openSelect ? (_jsx(SelectModal, { title: selectTitleFor(openSelect), options: selectOptionsFor(openSelect), initialValue: fieldCallbacksRef.current[openSelect]?.value, onSelect: (option) => {
|
|
68
|
+
fieldCallbacksRef.current[openSelect]?.onChange(option.value);
|
|
69
|
+
fieldCallbacksRef.current[openSelect]?.onAdvance();
|
|
70
|
+
setOpenSelect(null);
|
|
71
|
+
}, onClose: () => setOpenSelect(null) })) : null, commandEditorOpen ? (_jsx(CodeEditorModal, { initialValue: commandValue, onSave: (v) => {
|
|
72
|
+
setCommandValue(v);
|
|
73
|
+
setCommandEditorOpen(false);
|
|
74
|
+
}, onCancel: () => setCommandEditorOpen(false) })) : null] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
|
+
export function TextField({ value, hint, isActive, }) {
|
|
5
|
+
const borderColor = isActive ? theme.accent.brand : theme.border.dim;
|
|
6
|
+
const backgroundColor = isActive ? theme.bg.input : undefined;
|
|
7
|
+
const showHint = value.length === 0;
|
|
8
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, paddingLeft: 1, overflow: "hidden", width: "100%", children: [_jsx(Text, { color: showHint ? theme.text.muted : theme.text.primary, children: showHint ? hint : value }), isActive ? _jsx(Text, { inverse: true, children: " " }) : null] }));
|
|
9
|
+
}
|