@plainconceptsplatform/loop-task 2.6.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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/app/App.js +132 -0
- package/dist/app/BoardLayout.js +5 -0
- package/dist/app/index.js +48 -0
- package/dist/app/providers/index.js +1 -0
- package/dist/app/router/index.js +16 -0
- package/dist/app/types.js +1 -0
- package/dist/cli/import-validator.js +152 -0
- package/dist/cli/import-writer.js +81 -0
- package/dist/cli.js +573 -0
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +162 -0
- package/dist/client/ipc.js +96 -0
- package/dist/client/project-commands.js +164 -0
- package/dist/core/command/command-runner.js +300 -0
- package/dist/core/command/process-tree.js +73 -0
- package/dist/core/command/resolve-cwd.js +8 -0
- package/dist/core/command/stdout-capture-transform.js +73 -0
- package/dist/core/context/context-parser.js +65 -0
- package/dist/core/context/log-context.js +3 -0
- package/dist/core/context/template.js +20 -0
- package/dist/core/context/validate-context.js +19 -0
- package/dist/core/foreground/index.js +81 -0
- package/dist/core/logging/bounded-log-reader.js +368 -0
- package/dist/core/logging/log-follower.js +26 -0
- package/dist/core/logging/log-parser.js +29 -0
- package/dist/core/logging/log-rotator.js +34 -0
- package/dist/core/logging/rotating-log-stream.js +137 -0
- package/dist/core/loop/chain-executor.js +171 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +240 -0
- package/dist/core/loop/loop-runner.js +194 -0
- package/dist/core/loop/run-executor.js +144 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/core/scheduling/index.js +16 -0
- package/dist/daemon/daemon-log.js +11 -0
- package/dist/daemon/diagnostics.js +85 -0
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +119 -0
- package/dist/daemon/http/route-loops.js +311 -0
- package/dist/daemon/http/route-misc.js +60 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +141 -0
- package/dist/daemon/http/routes.js +19 -0
- package/dist/daemon/http/server.js +82 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +189 -0
- package/dist/daemon/ipc/send.js +5 -0
- package/dist/daemon/managers/loop-entry.js +27 -0
- package/dist/daemon/managers/loop-manager.js +346 -0
- package/dist/daemon/managers/loop-options.js +17 -0
- package/dist/daemon/managers/loop-serialization.js +53 -0
- package/dist/daemon/managers/project-manager.js +170 -0
- package/dist/daemon/managers/task-manager.js +94 -0
- package/dist/daemon/mcp/index.js +2 -0
- package/dist/daemon/mcp/openapi-sync.js +50 -0
- package/dist/daemon/mcp/server.js +167 -0
- package/dist/daemon/mcp/tools.js +382 -0
- package/dist/daemon/recipe/deferred-reload.js +32 -0
- package/dist/daemon/recipe/diagram-reader.js +26 -0
- package/dist/daemon/recipe/file-writer.js +52 -0
- package/dist/daemon/recipe/id-remapper.js +46 -0
- package/dist/daemon/recipe/runtime-state.js +23 -0
- package/dist/daemon/recipe/scanner.js +165 -0
- package/dist/daemon/recipe/task-store.js +33 -0
- package/dist/daemon/recipe/validator.js +35 -0
- package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
- package/dist/daemon/server/handlers/index.js +101 -0
- package/dist/daemon/server/handlers/log-handlers.js +129 -0
- package/dist/daemon/server/handlers/loop-handlers.js +64 -0
- package/dist/daemon/server/handlers/project-handlers.js +42 -0
- package/dist/daemon/server/handlers/settings-handlers.js +8 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
- package/dist/daemon/server/index.js +117 -0
- package/dist/daemon/settings-manager.js +73 -0
- package/dist/daemon/spawner/index.js +204 -0
- package/dist/daemon/state/index.js +209 -0
- package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
- package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
- package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
- package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
- package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
- package/dist/daemon/telemetry/index.js +6 -0
- package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
- package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
- package/dist/daemon/telemetry/telemetry-manager.js +147 -0
- package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
- package/dist/daemon/telemetry/telemetry-types.js +45 -0
- package/dist/daemon/telemetry/telemetry.js +1 -0
- package/dist/daemon/watcher/index.js +250 -0
- package/dist/duration.js +24 -0
- package/dist/entities/loops/filters.js +109 -0
- 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/entry.js +16 -0
- package/dist/esm-loader.js +39 -0
- package/dist/features/chain-editor/ChainEditor.js +116 -0
- package/dist/features/chain-editor/mermaidToAscii.js +375 -0
- package/dist/features/chain-editor/renderChainDiagram.js +106 -0
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/features/code-editor/CodeEditorPreview.js +14 -0
- package/dist/features/code-editor/useEditorKeyboard.js +150 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/features/commands/commands.js +132 -0
- package/dist/features/commands/useCommandHandlers.js +280 -0
- package/dist/features/commands/useContextualActions.js +78 -0
- package/dist/features/commands/useGlobalShortcuts.js +154 -0
- package/dist/features/forms/FormRouter.js +46 -0
- package/dist/features/overlays/ContextHelpModal.js +8 -0
- package/dist/features/overlays/DiagramModal.js +45 -0
- package/dist/features/overlays/ExportModal.js +51 -0
- package/dist/features/overlays/HelpGuideModal.js +23 -0
- package/dist/features/overlays/HelpModal.js +59 -0
- package/dist/features/overlays/OverlayStack.js +10 -0
- package/dist/features/overlays/ProjectsModal.js +42 -0
- package/dist/features/overlays/TaskPickerModal.js +62 -0
- package/dist/features/overlays/WelcomeScreen.js +74 -0
- package/dist/features/overlays/useOverlayStack.js +55 -0
- package/dist/features/state/useAppState.js +151 -0
- package/dist/logger.js +25 -0
- package/dist/loop-config.js +152 -0
- package/dist/shared/clipboard.js +111 -0
- package/dist/shared/config/constants.js +104 -0
- package/dist/shared/config/paths.js +67 -0
- package/dist/shared/container/index.js +24 -0
- package/dist/shared/fs-utils.js +44 -0
- package/dist/shared/hooks/useBreakpoint.js +11 -0
- package/dist/shared/hooks/useDaemonSettings.js +41 -0
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/shared/hooks/useLogStream.js +37 -0
- package/dist/shared/hooks/useLoopFormValidation.js +131 -0
- package/dist/shared/hooks/useLoopPolling.js +30 -0
- package/dist/shared/hooks/useMouseScroll.js +56 -0
- package/dist/shared/hooks/useUndoRedo.js +77 -0
- package/dist/shared/i18n/en.json +709 -0
- package/dist/shared/i18n/index.js +11 -0
- 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/settings-service.js +102 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +8 -0
- package/dist/shared/sleep.js +17 -0
- package/dist/shared/tail.js +4 -0
- package/dist/shared/ui/Button.js +18 -0
- package/dist/shared/ui/DebugPanel.js +9 -0
- package/dist/shared/ui/FocusableButton.js +18 -0
- package/dist/shared/ui/FocusableInput.js +72 -0
- package/dist/shared/ui/FocusableList.js +48 -0
- package/dist/shared/ui/Modal.js +13 -0
- package/dist/shared/ui/SelectModal.js +78 -0
- package/dist/shared/ui/Toast.js +37 -0
- package/dist/shared/ui/format.js +117 -0
- package/dist/shared/ui/hooks/useHoverState.js +11 -0
- package/dist/shared/ui/state.js +9 -0
- package/dist/shared/ui/theme.js +96 -0
- package/dist/shared/utils/log-lines.js +13 -0
- package/dist/shared/utils/paste.js +37 -0
- package/dist/shared/utils/syntax.js +128 -0
- package/dist/shared/utils/validation.js +56 -0
- package/dist/types.js +1 -0
- package/dist/visual-evidence/capture.js +51 -0
- package/dist/visual-evidence/cli.js +85 -0
- package/dist/visual-evidence/evidence-required.js +71 -0
- package/dist/visual-evidence/index.js +10 -0
- package/dist/visual-evidence/launch.js +61 -0
- package/dist/visual-evidence/manifest.js +32 -0
- package/dist/visual-evidence/openspec-resolver.js +45 -0
- package/dist/visual-evidence/publish.js +126 -0
- package/dist/visual-evidence/run.js +121 -0
- package/dist/visual-evidence/scenario-registry.js +132 -0
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +133 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +72 -0
- package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
- package/dist/widgets/header/Header.js +54 -0
- package/dist/widgets/header/TabBar.js +20 -0
- package/dist/widgets/left-panel/LeftPanel.js +34 -0
- package/dist/widgets/left-panel/Navigator.js +107 -0
- package/dist/widgets/left-panel/ProjectsPage.js +76 -0
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/widgets/left-panel/TaskBrowser.js +132 -0
- package/dist/widgets/log-modal/LogModal.js +211 -0
- package/dist/widgets/loop-form/CreateForm.js +82 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/widgets/loop-form/WizardForm.js +199 -0
- package/dist/widgets/loop-form/useCreateSteps.js +135 -0
- package/dist/widgets/loop-form/useHandleComplete.js +94 -0
- package/dist/widgets/project-form/ProjectForm.js +83 -0
- package/dist/widgets/right-panel/Inspector.js +41 -0
- package/dist/widgets/right-panel/RightPanel.js +38 -0
- package/dist/widgets/right-panel/RunHistory.js +187 -0
- package/dist/widgets/task-form/TaskForm.js +272 -0
- package/package.json +119 -0
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
// Swallow SGR mouse sequences — handled by useMouseScroll
|
|
52
|
+
if (input.includes("[<")) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if ((key.ctrl && input === "u") || input === "\x15") {
|
|
56
|
+
clearInput();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.ctrl)
|
|
60
|
+
return;
|
|
61
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
62
|
+
return;
|
|
63
|
+
if (navOwner === "panel" && state.inputValue.length === 0 && !state.isOpen) {
|
|
64
|
+
if (input === "j" || input === "k" || key.upArrow || key.downArrow) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (onCopy && input === "c" && !state.isOpen) {
|
|
69
|
+
onCopy();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.escape) {
|
|
73
|
+
dispatch({ type: "CLOSE" });
|
|
74
|
+
if (state.inputValue.length > 0) {
|
|
75
|
+
clearInput();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (key.return) {
|
|
80
|
+
if (state.isOpen && rankedFiltered.length > 0 && state.focusedIndex < rankedFiltered.length) {
|
|
81
|
+
const focused = rankedFiltered[state.focusedIndex];
|
|
82
|
+
dispatch({ type: "CLOSE" });
|
|
83
|
+
for (let i = 0; i <= state.inputValue.length; i++) {
|
|
84
|
+
dispatch({ type: "DELETE_BACKWARD" });
|
|
85
|
+
}
|
|
86
|
+
onCommand(focused.option.value);
|
|
87
|
+
}
|
|
88
|
+
else if (state.inputValue.length === 0) {
|
|
89
|
+
onPanelAction?.();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.tab) {
|
|
94
|
+
dispatch({ type: "ACCEPT" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.downArrow) {
|
|
98
|
+
dispatch({ type: "FOCUS_NEXT" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.upArrow) {
|
|
102
|
+
dispatch({ type: "FOCUS_PREV" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.backspace || key.delete) {
|
|
106
|
+
dispatch({ type: "DELETE_BACKWARD" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.leftArrow) {
|
|
110
|
+
dispatch({ type: "MOVE_CURSOR_LEFT" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.rightArrow) {
|
|
114
|
+
dispatch({ type: "MOVE_CURSOR_RIGHT" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (input.length > 1 && !key.meta) {
|
|
118
|
+
insertText(sanitizePaste(input));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (input.length === 1 && !key.ctrl && !key.meta && !key.return && !key.tab && !key.escape && input >= " " && input <= "~") {
|
|
122
|
+
dispatch({ type: "INSERT_TEXT", text: input });
|
|
123
|
+
}
|
|
124
|
+
}, { isActive: !disabled });
|
|
125
|
+
const isEmpty = state.inputValue.length === 0;
|
|
126
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
127
|
+
const inputContent = isEmpty ? cursor : renderInputLine(state.inputValue, state.cursorOffset);
|
|
128
|
+
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" })] }) })] }));
|
|
129
|
+
}
|
|
130
|
+
export function CommandInput(props) {
|
|
131
|
+
const { context, onCommand, confirmState, searchState, searchValue, onSearchChange, onSearchSubmit, onSearchCancel, onConfirmYes, onConfirmCancel, onCopy, onPanelAction, navOwner, onInputStateChange, } = props;
|
|
132
|
+
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 })) }));
|
|
133
|
+
}
|
|
@@ -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,72 @@
|
|
|
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 (input.includes("[<"))
|
|
11
|
+
return;
|
|
12
|
+
if (key.ctrl)
|
|
13
|
+
return;
|
|
14
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
15
|
+
return;
|
|
16
|
+
if (key.escape) {
|
|
17
|
+
setValue("");
|
|
18
|
+
onConfirmCancel();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (key.return) {
|
|
22
|
+
if (value.toLowerCase() === "yes") {
|
|
23
|
+
setValue("");
|
|
24
|
+
onConfirmYes();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
setValue("");
|
|
28
|
+
onConfirmCancel();
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.backspace || key.delete) {
|
|
33
|
+
setValue((v) => v.slice(0, -1));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
37
|
+
setValue((v) => v + input);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}, { isActive: !disabled });
|
|
41
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
42
|
+
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" }) })] }));
|
|
43
|
+
}
|
|
44
|
+
export function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, disabled, }) {
|
|
45
|
+
useInput((input, key) => {
|
|
46
|
+
if (input.includes("[<"))
|
|
47
|
+
return;
|
|
48
|
+
if (key.ctrl)
|
|
49
|
+
return;
|
|
50
|
+
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
51
|
+
return;
|
|
52
|
+
if (key.escape) {
|
|
53
|
+
onSearchCancel();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.return) {
|
|
57
|
+
onSearchSubmit();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.backspace || key.delete) {
|
|
61
|
+
onSearchChange(value.slice(0, -1));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
65
|
+
onSearchChange(value + input);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}, { isActive: !disabled });
|
|
69
|
+
const placeholder = t("cmdInput.searchPlaceholder");
|
|
70
|
+
const cursor = "\x1b[7m \x1b[27m";
|
|
71
|
+
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" }) })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } 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 { buildTabCommands } from "../../features/commands/commands.js";
|
|
7
|
+
import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
|
|
8
|
+
const CATEGORY_ORDER = ["global", "filters", "loop", "task", "project"];
|
|
9
|
+
function categoryLabel(cat) {
|
|
10
|
+
switch (cat) {
|
|
11
|
+
case "global": return t("cmdsBrowser.groupGlobal");
|
|
12
|
+
case "filters": return t("cmdsBrowser.groupFilters");
|
|
13
|
+
case "loop": return t("cmdsBrowser.groupLoop");
|
|
14
|
+
case "task": return t("cmdsBrowser.groupTask");
|
|
15
|
+
case "project": return t("cmdsBrowser.groupProject");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const MAX_VISIBLE = 16;
|
|
19
|
+
export function CommandsBrowserModal(props) {
|
|
20
|
+
const allCommands = useMemo(() => buildTabCommands(props.context), [props.context]);
|
|
21
|
+
const flatItems = useMemo(() => {
|
|
22
|
+
const items = [];
|
|
23
|
+
for (const cat of CATEGORY_ORDER) {
|
|
24
|
+
const cmds = allCommands.filter((c) => c.category === cat).sort((a, b) => a.label.localeCompare(b.label));
|
|
25
|
+
if (cmds.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
items.push({ command: { label: categoryLabel(cat), value: `__header_${cat}`, hint: "", tier: "global", category: cat }, isHeader: true, category: cat });
|
|
28
|
+
for (const cmd of cmds) {
|
|
29
|
+
items.push({ command: cmd, isHeader: false, category: cat });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return items;
|
|
33
|
+
}, [allCommands]);
|
|
34
|
+
const selectableIndices = useMemo(() => {
|
|
35
|
+
return flatItems
|
|
36
|
+
.map((item, i) => ({ item, i }))
|
|
37
|
+
.filter(({ item }) => !item.isHeader)
|
|
38
|
+
.map(({ i }) => i);
|
|
39
|
+
}, [flatItems]);
|
|
40
|
+
const [cursor, setCursor] = useState(0);
|
|
41
|
+
const currentFlatIdx = selectableIndices[cursor] ?? 0;
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (input.includes("[<")) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.escape) {
|
|
47
|
+
props.onClose();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.upArrow) {
|
|
51
|
+
setCursor((prev) => prev <= 0 ? Math.max(0, selectableIndices.length - 1) : prev - 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.downArrow) {
|
|
55
|
+
setCursor((prev) => prev >= selectableIndices.length - 1 ? 0 : prev + 1);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.return) {
|
|
59
|
+
const flatIdx = selectableIndices[cursor];
|
|
60
|
+
if (flatIdx !== undefined) {
|
|
61
|
+
const item = flatItems[flatIdx];
|
|
62
|
+
if (item && !item.isHeader) {
|
|
63
|
+
props.onExecute(item.command.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
useMouseScroll({
|
|
70
|
+
onScrollUp: () => setCursor((prev) => prev <= 0 ? Math.max(0, selectableIndices.length - 1) : prev - 1),
|
|
71
|
+
onScrollDown: () => setCursor((prev) => prev >= selectableIndices.length - 1 ? 0 : prev + 1),
|
|
72
|
+
});
|
|
73
|
+
const windowStart = Math.max(0, currentFlatIdx - Math.floor(MAX_VISIBLE / 2));
|
|
74
|
+
const windowEnd = Math.min(flatItems.length, windowStart + MAX_VISIBLE);
|
|
75
|
+
const visibleItems = flatItems.slice(windowStart, windowEnd);
|
|
76
|
+
return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: 56, flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.border.dim, paddingX: 1, paddingY: 0, children: [_jsxs(Box, { justifyContent: "space-between", paddingY: 0, children: [_jsx(Text, { bold: true, color: theme.text.primary, children: t("cmdsBrowser.title") }), _jsx(Text, { color: theme.text.muted, children: "esc" })] }), _jsx(Box, { flexDirection: "column", children: visibleItems.map((item, visIdx) => {
|
|
77
|
+
const realIdx = windowStart + visIdx;
|
|
78
|
+
const isSelected = realIdx === currentFlatIdx;
|
|
79
|
+
if (item.isHeader) {
|
|
80
|
+
return (_jsx(Box, { marginTop: realIdx === 0 ? 0 : 1, children: _jsx(Text, { color: theme.accent.brand, bold: true, children: item.command.label }) }, item.command.value));
|
|
81
|
+
}
|
|
82
|
+
const shortcut = item.command.shortcut ?? "";
|
|
83
|
+
return (_jsxs(Box, { backgroundColor: isSelected ? theme.bg.active : undefined, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: " " + item.command.label }), item.command.hint.length > 0 ? (_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: " " + item.command.hint.slice(0, 20) })) : null] }), shortcut.length > 0 ? (_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: shortcut })) : null] }, `${item.command.value}-${realIdx}`));
|
|
84
|
+
}) }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: theme.text.muted, children: t("cmdsBrowser.hint") }) })] }) }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, 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 "../../shared/ui/theme.js";
|
|
5
|
+
import { HEADER_COMPACT_WIDTH } from "../../shared/config/constants.js";
|
|
6
|
+
import { t } from "../../shared/i18n/index.js";
|
|
7
|
+
import { TabBar } from "./TabBar.js";
|
|
8
|
+
const VERSION = "v" + (process.env.npm_package_version ?? "dev");
|
|
9
|
+
function daemonSymbol(status) {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case "connected": return "\u25CF";
|
|
12
|
+
case "starting": return "\u25CB";
|
|
13
|
+
case "error": return "\u2717";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function daemonColor(status) {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case "connected": return theme.semantic.success;
|
|
19
|
+
case "starting": return theme.semantic.warning;
|
|
20
|
+
case "error": return theme.semantic.danger;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function daemonText(status) {
|
|
24
|
+
switch (status) {
|
|
25
|
+
case "connected": return "connected";
|
|
26
|
+
case "starting": return "starting";
|
|
27
|
+
case "error": return "offline";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function ServiceStatus({ label, enabled, online }) {
|
|
31
|
+
// Unknown until the daemon is reachable and reports the setting.
|
|
32
|
+
const known = online && enabled !== undefined;
|
|
33
|
+
const on = known && enabled === true;
|
|
34
|
+
const color = !known
|
|
35
|
+
? theme.text.muted
|
|
36
|
+
: on
|
|
37
|
+
? theme.semantic.success
|
|
38
|
+
: theme.semantic.danger;
|
|
39
|
+
const symbol = !known ? "\u25CB" : on ? "\u25CF" : "\u25CB";
|
|
40
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: label }), _jsx(Text, { color: color, children: symbol })] }));
|
|
41
|
+
}
|
|
42
|
+
export function Header(props) {
|
|
43
|
+
const { stdout } = useStdout();
|
|
44
|
+
const width = stdout?.columns ?? 80;
|
|
45
|
+
const compact = width < HEADER_COMPACT_WIDTH;
|
|
46
|
+
const online = props.daemonStatus === "connected";
|
|
47
|
+
const entries = [
|
|
48
|
+
{ label: t("board.runningLabel"), value: props.counts.running, color: theme.semantic.success },
|
|
49
|
+
{ label: t("board.waitingLabel"), value: props.counts.waiting, color: theme.accent.loop },
|
|
50
|
+
{ label: t("board.pausedLabel"), value: props.counts.paused, color: theme.semantic.warning },
|
|
51
|
+
{ label: t("board.idleLabel"), value: props.counts.idle, color: theme.semantic.idle },
|
|
52
|
+
];
|
|
53
|
+
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: theme.text.muted, children: VERSION }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), _jsx(ServiceStatus, { label: "api", enabled: props.httpApiEnabled, online: online }), _jsx(ServiceStatus, { label: "mcp", enabled: props.mcpApiEnabled, online: online }), _jsx(ServiceStatus, { label: "otel", enabled: props.telemetryEnabled, online: online }), !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) }) })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
|
+
const TAB_DEFS = [
|
|
5
|
+
{ key: "loops", label: "Loops", color: theme.accent.loop },
|
|
6
|
+
{ key: "tasks", label: "Tasks", color: theme.accent.task },
|
|
7
|
+
{ key: "projects", label: "Projects", color: theme.accent.project },
|
|
8
|
+
];
|
|
9
|
+
export function tabColor(tab) {
|
|
10
|
+
return TAB_DEFS.find((t) => t.key === tab)?.color ?? theme.accent.brand;
|
|
11
|
+
}
|
|
12
|
+
export function TabBar(props) {
|
|
13
|
+
const { activeTab, counts } = props;
|
|
14
|
+
return (_jsx(Box, { gap: 1, children: TAB_DEFS.map((tab) => {
|
|
15
|
+
const isActive = tab.key === activeTab;
|
|
16
|
+
const count = counts?.[tab.key];
|
|
17
|
+
const label = count !== undefined ? `${tab.label} ${count}` : tab.label;
|
|
18
|
+
return (_jsx(Box, { children: _jsx(Text, { color: isActive ? theme.text.inverse : theme.text.muted, backgroundColor: isActive ? tab.color : undefined, bold: isActive, children: ` ${label} ` }) }, tab.key));
|
|
19
|
+
}) }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { Navigator } from "./Navigator.js";
|
|
7
|
+
import { TaskNavigator } from "./TaskBrowser.js";
|
|
8
|
+
import { FocusableList } from "../../shared/ui/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
|
+
const panelWidth = breakpoint === "wide" ? "60%" : "100%";
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", width: panelWidth, 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, projects: projects })) : (_jsx(ProjectNavigator, { projects: projects, loops: projectLoops ?? loops, selectedIndex: projectSelectedIndex, onSelect: onProjectSelect ?? (() => { }), onActivate: onProjectActivate ?? (() => { }), isFocused: isFocused, navActive: navActive, loopCountFor: loopCountFor }))] }));
|
|
21
|
+
}
|
|
22
|
+
function ProjectNavigator(props) {
|
|
23
|
+
const { projects, selectedIndex, onSelect, onActivate, isFocused, navActive = true, loopCountFor } = props;
|
|
24
|
+
if (projects.length === 0) {
|
|
25
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(FocusableList, { items: projects, selectedIndex: selectedIndex, isFocused: isFocused, navActive: navActive, limit: 15, onSelect: onSelect, onActivate: onActivate, renderItem: (project, isSelected) => {
|
|
28
|
+
const count = loopCountFor(project.id);
|
|
29
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
30
|
+
const countFg = isSelected ? theme.text.inverse : theme.text.muted;
|
|
31
|
+
const createdFg = 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(18)] }), _jsxs(Text, { color: countFg, children: [String(count).padStart(3), " "] }), _jsx(Text, { color: createdFg, children: project.createdAt.slice(0, 10) })] }));
|
|
33
|
+
} }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 "../../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
|
+
import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
|
|
9
|
+
const DESC_WIDTH_WIDE = 32;
|
|
10
|
+
const DESC_WIDTH_COMPACT = 20;
|
|
11
|
+
const DESC_WIDTH_MINIMAL = 14;
|
|
12
|
+
const SINCE_WIDTH = 13;
|
|
13
|
+
const RUNS_WIDTH = 4;
|
|
14
|
+
const STATUS_WIDTH = 8;
|
|
15
|
+
const COL_GAP = 1;
|
|
16
|
+
const LIMIT = 15;
|
|
17
|
+
function descWidth(bp) {
|
|
18
|
+
if (bp === "wide")
|
|
19
|
+
return DESC_WIDTH_WIDE;
|
|
20
|
+
if (bp === "compact")
|
|
21
|
+
return DESC_WIDTH_COMPACT;
|
|
22
|
+
return DESC_WIDTH_MINIMAL;
|
|
23
|
+
}
|
|
24
|
+
export function Navigator(props) {
|
|
25
|
+
const { visible, total, selectedIndex, projects, onSelect, onActivate, isFocused, navActive = true, breakpoint = "wide" } = props;
|
|
26
|
+
const n = visible.length;
|
|
27
|
+
const dw = descWidth(breakpoint);
|
|
28
|
+
const showSince = breakpoint === "wide";
|
|
29
|
+
const showTiming = breakpoint !== "minimal";
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
if (n === 0)
|
|
32
|
+
return;
|
|
33
|
+
// Swallow SGR mouse sequences so they don't interfere
|
|
34
|
+
if (input.includes("[<")) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.upArrow || input === "k") {
|
|
38
|
+
const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
|
|
39
|
+
onSelect(next);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.downArrow || input === "j") {
|
|
43
|
+
const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
|
|
44
|
+
onSelect(next);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (key.ctrl && key.return) {
|
|
48
|
+
onActivate(selectedIndex);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}, { isActive: isFocused && navActive });
|
|
52
|
+
useMouseScroll({
|
|
53
|
+
onScrollUp: () => {
|
|
54
|
+
if (n === 0 || !(isFocused && navActive))
|
|
55
|
+
return;
|
|
56
|
+
const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
|
|
57
|
+
onSelect(next);
|
|
58
|
+
},
|
|
59
|
+
onScrollDown: () => {
|
|
60
|
+
if (n === 0 || !(isFocused && navActive))
|
|
61
|
+
return;
|
|
62
|
+
const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
|
|
63
|
+
onSelect(next);
|
|
64
|
+
},
|
|
65
|
+
isActive: isFocused && navActive,
|
|
66
|
+
});
|
|
67
|
+
const title = t("board.navigatorCount", {
|
|
68
|
+
visible: String(visible.length),
|
|
69
|
+
total: String(total),
|
|
70
|
+
});
|
|
71
|
+
function projectColor(loop) {
|
|
72
|
+
const proj = projects.find((p) => p.id === loop.projectId);
|
|
73
|
+
return proj?.color ?? theme.text.muted;
|
|
74
|
+
}
|
|
75
|
+
function projectName(loop) {
|
|
76
|
+
const proj = projects.find((p) => p.id === loop.projectId);
|
|
77
|
+
return proj?.name ?? "Default";
|
|
78
|
+
}
|
|
79
|
+
function isFailed(loop) {
|
|
80
|
+
return loop.lastExitCode !== null && loop.lastExitCode !== 0;
|
|
81
|
+
}
|
|
82
|
+
function renderLoop(loop, isSelected) {
|
|
83
|
+
const rawDesc = describeLoop(loop);
|
|
84
|
+
const prefix = breakpoint === "minimal" ? "" : `${projectName(loop)}: `;
|
|
85
|
+
const desc = truncate(`${prefix}${rawDesc}`, dw);
|
|
86
|
+
const since = sinceLabel(loop);
|
|
87
|
+
const timing = timingLabel(loop);
|
|
88
|
+
const failed = isFailed(loop);
|
|
89
|
+
const sColor = failed ? theme.semantic.danger : statusColor(loop.status);
|
|
90
|
+
const sLabel = statusLabel(loop.status);
|
|
91
|
+
const recipeBadge = loop.isRecipe ? "R " : "";
|
|
92
|
+
const statusText = `${recipeBadge}${sLabel}`;
|
|
93
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
94
|
+
const dotChar = failed ? "\u2717 " : "\u25cf ";
|
|
95
|
+
const dotColor = failed
|
|
96
|
+
? theme.semantic.danger
|
|
97
|
+
: isSelected
|
|
98
|
+
? theme.text.inverse
|
|
99
|
+
: projectColor(loop);
|
|
100
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: dotColor, children: dotChar }), _jsx(Text, { color: fg, children: desc.padEnd(dw + COL_GAP) }), showSince ? _jsx(Text, { color: fg, children: since.padEnd(SINCE_WIDTH + COL_GAP) }) : null, _jsx(Text, { color: fg, children: String(loop.runCount).padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: isSelected ? theme.text.inverse : sColor, children: statusText.padEnd(STATUS_WIDTH + 1 + COL_GAP) }), showTiming ? _jsx(Text, { color: fg, children: timing }) : null, loop.status === "running" ? (_jsxs(Text, { color: theme.semantic.success, children: [" ", _jsx(Spinner, { type: "dots" })] })) : null] }));
|
|
101
|
+
}
|
|
102
|
+
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(dw + COL_GAP) }), showSince ? _jsx(Text, { color: theme.text.muted, children: t("board.headerSince").padEnd(SINCE_WIDTH + COL_GAP) }) : null, _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.headerStatus").padEnd(STATUS_WIDTH + COL_GAP) }), showTiming ? _jsx(Text, { color: theme.text.muted, children: t("board.headerTiming") }) : null] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { selectedIndex: selectedIndex, height: LIMIT, children: visible.map((loop, i) => {
|
|
103
|
+
const isSelected = i === selectedIndex;
|
|
104
|
+
const indicator = isSelected ? "\u203a " : " ";
|
|
105
|
+
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));
|
|
106
|
+
}) }) })] }))] }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
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";
|
|
10
|
+
export function ProjectsPage(props) {
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const [subModal, setSubModal] = useState("none");
|
|
13
|
+
const projectService = useInject(TYPES.ProjectService);
|
|
14
|
+
const defaultProject = props.projects.find((p) => p.isDefault) ?? null;
|
|
15
|
+
const defaultProjectId = defaultProject?.id ?? null;
|
|
16
|
+
const knownProjectIds = new Set(props.projects.map((p) => p.id));
|
|
17
|
+
const selected = props.projects[selectedIndex];
|
|
18
|
+
const loopCount = selected
|
|
19
|
+
? loopCountFor(props.loops, selected.id, defaultProjectId, knownProjectIds)
|
|
20
|
+
: 0;
|
|
21
|
+
useInput((input, key) => {
|
|
22
|
+
if (subModal !== "none")
|
|
23
|
+
return;
|
|
24
|
+
if (input.includes("[<"))
|
|
25
|
+
return;
|
|
26
|
+
if (key.escape) {
|
|
27
|
+
props.onClose();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (input === "n") {
|
|
31
|
+
if (props.onNavigateCreate) {
|
|
32
|
+
props.onNavigateCreate();
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (input === "e" && selected) {
|
|
37
|
+
if (props.onNavigateEdit) {
|
|
38
|
+
props.onNavigateEdit(selected);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (input === "d" && selected) {
|
|
43
|
+
setSubModal("delete");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
async function handleDelete() {
|
|
48
|
+
if (!selected)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
await projectService.delete(selected.id);
|
|
52
|
+
props.onToast(t("project.toastDeleted", { name: selected.name }));
|
|
53
|
+
await props.onRefresh();
|
|
54
|
+
setSubModal("none");
|
|
55
|
+
setSelectedIndex(0);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
props.onToast(e.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (subModal === "delete" && selected) {
|
|
62
|
+
return (_jsx(DeleteConfirm, { project: selected, loopCount: loopCount, onConfirm: handleDelete, onCancel: () => setSubModal("none") }));
|
|
63
|
+
}
|
|
64
|
+
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) => {
|
|
65
|
+
setSelectedIndex(idx);
|
|
66
|
+
if (props.onNavigateEdit) {
|
|
67
|
+
const p = props.projects[idx];
|
|
68
|
+
if (p)
|
|
69
|
+
props.onNavigateEdit(p);
|
|
70
|
+
}
|
|
71
|
+
}, 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.directory ? (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldDirectory") }), _jsx(Text, { color: theme.text.secondary, children: selected.directory })] })) : null, selected.directory ? (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Dir: " }), _jsx(Text, { color: theme.text.secondary, children: selected.directory })] })) : null, 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: () => {
|
|
72
|
+
if (props.onNavigateEdit && selected) {
|
|
73
|
+
props.onNavigateEdit(selected);
|
|
74
|
+
}
|
|
75
|
+
} }), _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") }) })) })] }) }));
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|