loop-task 2.1.4 → 2.1.7

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.
Files changed (148) hide show
  1. package/dist/app/App.js +119 -0
  2. package/dist/{tui → app}/index.js +4 -5
  3. package/dist/app/providers/index.js +1 -0
  4. package/dist/cli/import-writer.js +1 -1
  5. package/dist/cli.js +9 -9
  6. package/dist/client/cli-format.js +7 -0
  7. package/dist/client/commands.js +2 -164
  8. package/dist/client/ipc.js +3 -3
  9. package/dist/client/project-commands.js +159 -0
  10. package/dist/core/{command-runner.js → command/command-runner.js} +3 -3
  11. package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
  12. package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
  13. package/dist/core/loop/chain-executor.js +67 -0
  14. package/dist/core/loop/delay-utils.js +65 -0
  15. package/dist/core/loop/loop-controller.js +191 -0
  16. package/dist/core/loop/loop-runner.js +132 -0
  17. package/dist/core/loop/run-executor.js +90 -0
  18. package/dist/core/loop/types.js +1 -0
  19. package/dist/daemon/daemon-log.js +1 -1
  20. package/dist/daemon/http/helpers.js +81 -0
  21. package/dist/daemon/http/openapi.js +92 -0
  22. package/dist/daemon/http/route-loops.js +218 -0
  23. package/dist/daemon/http/route-misc.js +36 -0
  24. package/dist/daemon/http/route-projects.js +55 -0
  25. package/dist/daemon/http/route-tasks.js +53 -0
  26. package/dist/daemon/http/routes.js +17 -0
  27. package/dist/daemon/http/server.js +64 -0
  28. package/dist/daemon/http/sse.js +36 -0
  29. package/dist/daemon/index.js +7 -9
  30. package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
  31. package/dist/daemon/managers/loop-entry.js +24 -0
  32. package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
  33. package/dist/daemon/managers/loop-options.js +16 -0
  34. package/dist/daemon/managers/loop-serialization.js +43 -0
  35. package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
  36. package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
  37. package/dist/daemon/server/handlers/index.js +86 -0
  38. package/dist/daemon/server/handlers/log-handlers.js +130 -0
  39. package/dist/daemon/server/handlers/loop-handlers.js +54 -0
  40. package/dist/daemon/server/handlers/project-handlers.js +37 -0
  41. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  42. package/dist/daemon/server/index.js +113 -0
  43. package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
  44. package/dist/daemon/{state.js → state/index.js} +4 -4
  45. package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
  46. package/dist/duration.js +1 -1
  47. package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
  48. package/dist/entities/loops/index.js +1 -0
  49. package/dist/entities/projects/filters.js +69 -0
  50. package/dist/entities/projects/index.js +1 -0
  51. package/dist/entities/tasks/filters.js +41 -0
  52. package/dist/entities/tasks/index.js +1 -0
  53. package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
  54. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  55. package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
  56. package/dist/features/code-editor/useEditorKeyboard.js +147 -0
  57. package/dist/features/code-editor/useModalDimensions.js +9 -0
  58. package/dist/{tui → features/commands}/commands.js +2 -2
  59. package/dist/features/commands/useCommandHandlers.js +147 -0
  60. package/dist/features/commands/useContextualActions.js +75 -0
  61. package/dist/features/commands/useGlobalShortcuts.js +152 -0
  62. package/dist/features/forms/FormRouter.js +44 -0
  63. package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
  64. package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
  65. package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
  66. package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
  67. package/dist/features/overlays/OverlayStack.js +9 -0
  68. package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
  69. package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
  70. package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
  71. package/dist/features/overlays/useOverlayStack.js +51 -0
  72. package/dist/features/state/useAppState.js +150 -0
  73. package/dist/logger.js +0 -1
  74. package/dist/loop-config.js +9 -4
  75. package/dist/{config → shared/config}/constants.js +1 -1
  76. package/dist/shared/container/index.js +22 -0
  77. package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
  78. package/dist/shared/hooks/useInject.js +6 -0
  79. package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
  80. package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
  81. package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
  82. package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
  83. package/dist/shared/providers/InversifyProvider.js +6 -0
  84. package/dist/shared/services/export-service.js +43 -0
  85. package/dist/shared/services/log-service.js +28 -0
  86. package/dist/shared/services/loop-service.js +78 -0
  87. package/dist/shared/services/project-service.js +39 -0
  88. package/dist/shared/services/task-service.js +41 -0
  89. package/dist/shared/services/types.js +7 -0
  90. package/dist/{tui/components → shared/ui}/Button.js +1 -1
  91. package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
  92. package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
  93. package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
  94. package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
  95. package/dist/{tui/components → shared/ui}/Modal.js +1 -1
  96. package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
  97. package/dist/{tui/components → shared/ui}/Toast.js +1 -1
  98. package/dist/shared/ui/state.js +9 -0
  99. package/dist/{tui → shared}/utils/paste.js +2 -2
  100. package/dist/{tui → shared}/utils/validation.js +1 -1
  101. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  102. package/dist/widgets/command-input/CommandInput.js +129 -0
  103. package/dist/widgets/command-input/HintBar.js +9 -0
  104. package/dist/widgets/command-input/InputModes.js +68 -0
  105. package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
  106. package/dist/{tui/components → widgets/header}/Header.js +3 -3
  107. package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
  108. package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
  109. package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
  110. package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
  111. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  112. package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
  113. package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
  114. package/dist/widgets/loop-form/CreateForm.js +75 -0
  115. package/dist/widgets/loop-form/TextField.js +9 -0
  116. package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
  117. package/dist/widgets/loop-form/useCreateSteps.js +107 -0
  118. package/dist/widgets/loop-form/useHandleComplete.js +74 -0
  119. package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
  120. package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
  121. package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
  122. package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
  123. package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
  124. package/package.json +2 -1
  125. package/dist/core/loop-controller.js +0 -511
  126. package/dist/daemon/http-server.js +0 -599
  127. package/dist/daemon/server.js +0 -374
  128. package/dist/tui/App.js +0 -680
  129. package/dist/tui/components/CodeEditorModal.js +0 -244
  130. package/dist/tui/components/CommandInput.js +0 -273
  131. package/dist/tui/components/CreateForm.js +0 -224
  132. package/dist/tui/daemon.js +0 -142
  133. package/dist/tui/format.js +0 -111
  134. /package/dist/{tui/router.js → app/router/index.js} +0 -0
  135. /package/dist/{tui → app}/types.js +0 -0
  136. /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
  137. /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
  138. /package/dist/core/{template.js → context/template.js} +0 -0
  139. /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
  140. /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
  141. /package/dist/{ipc → daemon/ipc}/send.js +0 -0
  142. /package/dist/{config → shared/config}/paths.js +0 -0
  143. /package/dist/{i18n → shared/i18n}/en.json +0 -0
  144. /package/dist/{i18n → shared/i18n}/index.js +0 -0
  145. /package/dist/{board → shared/ui}/format.js +0 -0
  146. /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
  147. /package/dist/{tui → shared/ui}/theme.js +0 -0
  148. /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 "../../config/constants.js";
2
- import { CODE_EDITOR_WRAP_LENGTH } from "../../config/constants.js";
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 "../../i18n/index.js";
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 "../theme.js";
5
- import { t } from "../../i18n/index.js";
6
- import { buildTabCommands } from "../commands.js";
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 "../theme.js";
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 "../theme.js";
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 "../theme.js";
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 "./FocusableList.js";
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 "../theme.js";
6
- import { describeLoop, sinceLabel, statusLabel, timingLabel, truncate } from "../format.js";
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, useFocus } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
5
- import { FocusableList } from "./FocusableList.js";
6
- import { FocusableButton } from "./FocusableButton.js";
7
- import { Modal } from "./Modal.js";
8
- import { t } from "../../i18n/index.js";
9
- import { deleteProject } from "../daemon.js";
10
- function loopCountFor(loops, projectId, defaultProjectId, knownProjectIds) {
11
- const direct = loops.filter((l) => l.projectId === projectId).length;
12
- if (projectId === defaultProjectId) {
13
- const orphans = loops.filter((l) => !knownProjectIds.has(l.projectId)).length;
14
- return direct + orphans;
15
- }
16
- return direct;
17
- }
18
- function DeleteConfirm(props) {
19
- return (_jsxs(Modal, { title: t("project.deleteTitle"), onClose: props.onCancel, width: 50, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.text.primary, children: t("project.deleteConfirm", { name: props.project.name }) }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.deleteWarning", { count: props.loopCount }) }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(FocusableButton, { label: t("board.yes"), color: theme.semantic.danger, onPress: props.onConfirm, variant: "danger" }), _jsx(FocusableButton, { label: t("board.no"), color: theme.text.secondary, onPress: props.onCancel })] })] }));
20
- }
21
- function ListFocusWrapper(props) {
22
- const { isFocused } = useFocus();
23
- if (props.projects.length === 0) {
24
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
25
- }
26
- const nameWidth = 20;
27
- const loopsWidth = 12;
28
- const headerSeparator = " \u00B7 ";
29
- return (_jsxs(React.Fragment, { children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, children: [_jsx(Text, { color: theme.text.muted, children: " ".concat(t("project.headerName").padEnd(nameWidth)) }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, String(t("project.headerLoops")).padEnd(loopsWidth)] }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, t("project.headerCreated")] })] }), _jsx(FocusableList, { items: props.projects, selectedIndex: props.selectedIndex, isFocused: isFocused, limit: 10, onSelect: props.onSelect, onActivate: props.onActivate, renderItem: (project, isSelected) => {
30
- const count = loopCountFor(props.loops, project.id, props.defaultProjectId, props.knownProjectIds);
31
- const fg = isSelected ? theme.text.inverse : theme.text.primary;
32
- const countFg = isSelected ? theme.text.inverse : theme.text.muted;
33
- return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: project.color, children: "\u25CF" }), _jsxs(Text, { color: fg, children: [" ", project.name.padEnd(nameWidth - 1)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, String(count).padEnd(loopsWidth)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, project.createdAt] })] }));
34
- } }), props.children] }));
35
- }
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 deleteProject(selected.id);
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 "../theme.js";
6
- import { commandLine } from "../format.js";
7
- import { t } from "../../i18n/index.js";
8
- import { FocusableButton } from "./FocusableButton.js";
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 "../theme.js";
5
- import { t } from "../../i18n/index.js";
6
- import { streamRunLog } from "../daemon.js";
7
- import { formatDate } from "../format.js";
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
+ }