@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.
Files changed (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
@@ -0,0 +1,132 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { ScrollList } from "ink-scroll-list";
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
+ import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
10
+ const NAME_WIDTH = 24;
11
+ const COMMAND_WIDTH = 28;
12
+ const PROJECT_WIDTH = 10;
13
+ const LIMIT = 15;
14
+ export function TaskNavigator(props) {
15
+ const { visible, total, selectedIndex, isFocused, navActive = true, allTasks, projects, onSelect, onActivate } = props;
16
+ const title = t("board.taskBrowserTitle", {
17
+ visible: String(visible.length),
18
+ total: String(total),
19
+ });
20
+ useInput((input, key) => {
21
+ // Swallow SGR mouse sequences so they don't interfere
22
+ if (input.includes("[<")) {
23
+ return;
24
+ }
25
+ if (key.upArrow || input === "k") {
26
+ onSelect(Math.max(selectedIndex - 1, 0));
27
+ }
28
+ else if (key.downArrow || input === "j") {
29
+ onSelect(Math.min(selectedIndex + 1, visible.length - 1));
30
+ }
31
+ else if (key.ctrl && key.return) {
32
+ onActivate(selectedIndex);
33
+ }
34
+ }, { isActive: isFocused && navActive });
35
+ useMouseScroll({
36
+ onScrollUp: () => {
37
+ if (!(isFocused && navActive))
38
+ return;
39
+ onSelect(Math.max(selectedIndex - 1, 0));
40
+ },
41
+ onScrollDown: () => {
42
+ if (!(isFocused && navActive))
43
+ return;
44
+ onSelect(Math.min(selectedIndex + 1, visible.length - 1));
45
+ },
46
+ isActive: isFocused && navActive,
47
+ });
48
+ function resolveName(id) {
49
+ if (id === null)
50
+ return null;
51
+ return allTasks.find((t) => t.id === id)?.name ?? id;
52
+ }
53
+ function chainsLabel(task) {
54
+ const hasSuccess = task.onSuccessTaskId !== null;
55
+ const hasFailure = task.onFailureTaskId !== null;
56
+ if (!hasSuccess && !hasFailure)
57
+ return t("board.taskChainsNone");
58
+ const successName = hasSuccess ? resolveName(task.onSuccessTaskId) : null;
59
+ const failureName = hasFailure ? resolveName(task.onFailureTaskId) : null;
60
+ return t("board.taskChainsFormat", {
61
+ success: hasSuccess ? `\u2713${successName}` : "-",
62
+ failure: hasFailure ? `\u2192${failureName}` : "-",
63
+ });
64
+ }
65
+ function resolveProjectName(task) {
66
+ const proj = projects.find((p) => p.id === task.projectId);
67
+ return proj?.name ?? "";
68
+ }
69
+ function renderTask(task, index) {
70
+ const isSelected = index === selectedIndex;
71
+ const name = task.name.length > NAME_WIDTH
72
+ ? task.name.slice(0, NAME_WIDTH - 3) + "..."
73
+ : task.name.padEnd(NAME_WIDTH);
74
+ const cmd = commandLine(task.command, task.commandArgs);
75
+ const cmdDisplay = cmd.length > COMMAND_WIDTH
76
+ ? cmd.slice(0, COMMAND_WIDTH - 3) + "..."
77
+ : cmd.padEnd(COMMAND_WIDTH);
78
+ const chains = chainsLabel(task);
79
+ const silent = task.silentChain ? " [s]" : "";
80
+ const recipeBadge = task.isRecipeTask ? "R " : "";
81
+ const projectTag = task.isRecipeTask && task.projectId ? `${resolveProjectName(task)}: ` : "";
82
+ const projectDisplay = projectTag.length > PROJECT_WIDTH
83
+ ? projectTag.slice(0, PROJECT_WIDTH - 3) + "..."
84
+ : projectTag.padEnd(PROJECT_WIDTH);
85
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
86
+ const recipeFg = isSelected ? theme.text.inverse : theme.accent.brand;
87
+ return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.activeTask : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: recipeFg, children: recipeBadge }), _jsxs(Text, { color: fg, children: [projectDisplay, name] }), _jsx(Text, { color: fg, children: cmdDisplay }), _jsxs(Text, { color: fg, children: [chains, silent] })] }));
88
+ }
89
+ 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.taskBrowserEmpty") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderName").padEnd(NAME_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderCommand").padEnd(COMMAND_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderChains") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { height: LIMIT, selectedIndex: selectedIndex, scrollAlignment: "auto", children: visible.map((task, i) => (_jsx(React.Fragment, { children: renderTask(task, i) }, task.id))) }) })] }))] }));
90
+ }
91
+ const FIELD_LABEL_WIDTH = 13;
92
+ function InspectorField(props) {
93
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.secondary, children: props.label.padEnd(FIELD_LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
94
+ }
95
+ export function TaskInspector(props) {
96
+ const { task, allTasks } = props;
97
+ if (!task) {
98
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
99
+ }
100
+ const cmd = commandLine(task.command, task.commandArgs);
101
+ const resolveName = (id) => {
102
+ if (id === null)
103
+ return null;
104
+ return allTasks.find((t) => t.id === id)?.name ?? id;
105
+ };
106
+ const onSuccess = task.onSuccessTaskId ? resolveName(task.onSuccessTaskId) ?? task.onSuccessTaskId : t("board.taskNone");
107
+ const onFailure = task.onFailureTaskId ? resolveName(task.onFailureTaskId) ?? task.onFailureTaskId : t("board.taskNone");
108
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(InspectorField, { label: t("board.fieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(InspectorField, { label: t("board.taskLabelName") + ": ", children: _jsx(Text, { color: theme.text.primary, children: task.name }) }), _jsx(InspectorField, { label: t("board.taskLabelCommand") + ": ", children: _jsx(Text, { color: theme.text.primary, children: cmd }) }), _jsx(InspectorField, { label: t("board.taskLabelOnSuccess") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onSuccess }) }), _jsx(InspectorField, { label: t("board.taskLabelOnFailure") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onFailure }) }), _jsx(InspectorField, { label: t("board.taskFieldSilent") + ": ", children: _jsx(Text, { color: task.silentChain ? theme.semantic.warning : theme.text.muted, children: task.silentChain ? t("board.silentChainYes") : t("board.silentChainNo") }) })] })] }));
109
+ }
110
+ export function TaskActionButtons(props) {
111
+ const { task, selectable, onAction } = props;
112
+ const isRecipe = task?.isRecipeTask === true;
113
+ const actions = isRecipe
114
+ ? [] // Recipe tasks are immutable — no actions allowed
115
+ : selectable
116
+ ? [
117
+ { key: "select", label: t("board.taskActionSelect") },
118
+ { key: "edit", label: t("board.taskActionEdit") },
119
+ { key: "delete", label: t("board.taskActionDelete") },
120
+ ]
121
+ : [
122
+ { key: "edit", label: t("board.taskActionEdit") },
123
+ { key: "delete", label: t("board.taskActionDelete") },
124
+ ];
125
+ if (!task) {
126
+ return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "row", height: 3, backgroundColor: theme.bg.surface, children: _jsx(Text, { color: theme.text.muted, children: t("board.noActions") }) }));
127
+ }
128
+ if (isRecipe) {
129
+ return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "row", height: 3, backgroundColor: theme.bg.surface, children: _jsx(Text, { color: theme.text.muted, children: t("task.recipeImmutableShort") }) }));
130
+ }
131
+ return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "row", height: 3, backgroundColor: theme.bg.surface, children: actions.map((action) => (_jsx(FocusableButton, { label: action.label, color: action.key === "delete" ? theme.semantic.danger : theme.text.secondary, variant: action.key === "delete" ? "danger" : "default", onPress: () => onAction(action.key) }, action.key))) }));
132
+ }
@@ -0,0 +1,211 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput, useStdout } from "ink";
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";
9
+ import { copyToClipboard } from "../../shared/clipboard.js";
10
+ import { LOG_MODAL_LINES_MAX } from "../../shared/config/constants.js";
11
+ import { appendClamped, clampLines } from "../../shared/utils/log-lines.js";
12
+ import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
13
+ function colorForLine(line, run) {
14
+ if (line.includes("[Run #"))
15
+ return theme.accent.loop;
16
+ if (line.startsWith("$ "))
17
+ return "#f0abfc";
18
+ if (line.startsWith(" cwd:"))
19
+ return theme.text.muted;
20
+ if (line.includes("--- Chain:"))
21
+ return theme.accent.task;
22
+ if (line.trimStart().startsWith("[exit")) {
23
+ const match = /\[exit\s+(\d+)/.exec(line);
24
+ const code = match ? Number(match[1]) : run.exitCode;
25
+ return code === 0 ? theme.semantic.success : theme.semantic.danger;
26
+ }
27
+ return theme.text.primary;
28
+ }
29
+ const MAX_VALUE_LENGTH = 60;
30
+ function expandJsonLine(line) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
33
+ return null;
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(trimmed);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
42
+ return null;
43
+ const lines = ["context: {"];
44
+ for (const key of Object.keys(parsed)) {
45
+ const value = parsed[key];
46
+ let display;
47
+ if (typeof value === "string") {
48
+ display = value.length > MAX_VALUE_LENGTH
49
+ ? `"${value.slice(0, MAX_VALUE_LENGTH)}..."`
50
+ : `"${value}"`;
51
+ }
52
+ else if (typeof value === "number" || typeof value === "boolean") {
53
+ display = String(value);
54
+ }
55
+ else if (value === null) {
56
+ display = "null";
57
+ }
58
+ else {
59
+ display = JSON.stringify(value);
60
+ }
61
+ lines.push(` ${key}: ${display}`);
62
+ }
63
+ lines.push("}");
64
+ return lines;
65
+ }
66
+ export function LogModal(props) {
67
+ const [lines, setLines] = useState(() => clampLines(props.logLines, LOG_MODAL_LINES_MAX));
68
+ const [streaming, setStreaming] = useState(false);
69
+ const [searchMode, setSearchMode] = useState(false);
70
+ const [searchQuery, setSearchQuery] = useState("");
71
+ const [follow, setFollow] = useState(true);
72
+ const [scrollOffset, setScrollOffset] = useState(0);
73
+ const injectedLogService = useInject(TYPES.LogService);
74
+ const logServiceRef = useRef(injectedLogService);
75
+ const logService = logServiceRef.current;
76
+ const { stdout } = useStdout();
77
+ const terminalHeight = stdout?.rows ?? 24;
78
+ const MAX_VISIBLE_LINES = Math.max(1, Math.floor(terminalHeight * 0.7) - 7);
79
+ useEffect(() => {
80
+ setLines(clampLines(props.logLines, LOG_MODAL_LINES_MAX));
81
+ }, [props.logLines]);
82
+ useEffect(() => {
83
+ if (!props.loopId)
84
+ return;
85
+ if (props.run.status !== "running")
86
+ return;
87
+ setStreaming(true);
88
+ const socket = logService.streamRunLog(props.loopId, props.run.runNumber, (line) => setLines((prev) => appendClamped(prev, line, LOG_MODAL_LINES_MAX)), () => setStreaming(false), () => setStreaming(false));
89
+ return () => {
90
+ socket.destroy();
91
+ };
92
+ }, [props.loopId, props.run.runNumber, props.run.status, logService]);
93
+ const filtered = searchQuery
94
+ ? lines.filter((l) => l.toLowerCase().includes(searchQuery.toLowerCase()))
95
+ : lines;
96
+ const totalLines = filtered.length;
97
+ const startIdx = follow
98
+ ? Math.max(0, totalLines - MAX_VISIBLE_LINES)
99
+ : scrollOffset;
100
+ const endIdx = startIdx + MAX_VISIBLE_LINES;
101
+ const visible = filtered.slice(startIdx, endIdx);
102
+ useInput((input, key) => {
103
+ // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
104
+ // before the escape check the leading ESC trips key.escape and would
105
+ // close the modal before the paste is acknowledged. LogModal doesn't
106
+ // insert pastes, so just swallow the sequence.
107
+ if (input.includes("\x1b[200~")) {
108
+ return;
109
+ }
110
+ // Swallow SGR mouse sequences so they don't trigger key.escape
111
+ if (input.includes("[<")) {
112
+ return;
113
+ }
114
+ if (searchMode) {
115
+ if (key.escape || key.return) {
116
+ setSearchMode(false);
117
+ return;
118
+ }
119
+ if (key.backspace || key.delete) {
120
+ setSearchQuery((q) => q.slice(0, -1));
121
+ return;
122
+ }
123
+ if (input && !key.ctrl && input.length === 1 && input >= " " && input <= "~") {
124
+ setSearchQuery((q) => q + input);
125
+ return;
126
+ }
127
+ return;
128
+ }
129
+ if (key.escape) {
130
+ props.onClose();
131
+ return;
132
+ }
133
+ if (input === "/") {
134
+ setSearchMode(true);
135
+ setSearchQuery("");
136
+ return;
137
+ }
138
+ if (key.ctrl && input === "x") {
139
+ copyToClipboard(lines.join("\n"));
140
+ props.onCopy?.();
141
+ return;
142
+ }
143
+ if (key.ctrl && input === "t") {
144
+ setFollow(false);
145
+ setScrollOffset(0);
146
+ return;
147
+ }
148
+ if (key.ctrl && input === "b") {
149
+ setFollow(true);
150
+ setScrollOffset(0);
151
+ return;
152
+ }
153
+ if (key.downArrow) {
154
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
155
+ setFollow(false);
156
+ setScrollOffset(Math.min(base + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
157
+ return;
158
+ }
159
+ if (key.upArrow) {
160
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
161
+ setFollow(false);
162
+ setScrollOffset(Math.max(0, base - 1));
163
+ return;
164
+ }
165
+ });
166
+ useMouseScroll({
167
+ onScrollUp: () => {
168
+ if (searchMode)
169
+ return;
170
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
171
+ setFollow(false);
172
+ setScrollOffset(Math.max(0, base - 1));
173
+ },
174
+ onScrollDown: () => {
175
+ if (searchMode)
176
+ return;
177
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
178
+ setFollow(false);
179
+ setScrollOffset(Math.min(base + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
180
+ },
181
+ });
182
+ const isLoading = props.loading || streaming;
183
+ const statusIcon = props.run.status === "running"
184
+ ? "\u21bb"
185
+ : props.run.exitCode === 0
186
+ ? "\u2713"
187
+ : "\u2717";
188
+ const statusColor = props.run.status === "running"
189
+ ? theme.semantic.warning
190
+ : props.run.exitCode === 0
191
+ ? theme.semantic.success
192
+ : theme.semantic.danger;
193
+ return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.elevated, paddingX: 2, paddingY: 1, flexDirection: "column", width: "95%", height: "70%", children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: `Run #${props.run.runNumber} - ${formatDate(props.run.startedAt)}` }), _jsxs(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " ", props.run.status === "running"
194
+ ? t("board.logModalRunning")
195
+ : t("board.logModalExit", { code: props.run.exitCode })] }), props.run.duration > 0 ? (_jsxs(Text, { color: theme.text.muted, children: [" ", t("board.logModalDuration"), " ", props.run.duration, "ms"] })) : null, _jsx(Text, { color: theme.text.muted, children: searchMode
196
+ ? `/${searchQuery}`
197
+ : `[${startIdx}-${endIdx}/${totalLines}]` })] }), searchMode ? (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text.muted, children: "Search: " }), _jsxs(Text, { color: theme.text.primary, children: [searchQuery, "_"] })] })) : null, _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: isLoading
198
+ ? t("board.logModalLoading")
199
+ : searchQuery
200
+ ? t("board.logModalNoMatches")
201
+ : t("board.logModalEmpty") })) : (visible.map((line, i) => {
202
+ const realIdx = startIdx + i;
203
+ const expanded = expandJsonLine(line);
204
+ if (expanded) {
205
+ return (_jsx(React.Fragment, { children: expanded.map((sub, j) => (_jsx(Text, { color: j === 0 ? theme.accent.task : theme.text.muted, wrap: "truncate", children: sub }, `${realIdx}-${j}`))) }, realIdx));
206
+ }
207
+ return (_jsx(Text, { color: colorForLine(line, props.run), wrap: "truncate", children: line }, realIdx));
208
+ })) }), _jsx(Box, { marginTop: 1, justifyContent: "space-between", children: _jsx(Text, { color: theme.text.muted, children: searchMode
209
+ ? t("board.logModalSearchHint")
210
+ : t("board.logModalFooterHints") }) })] })] }) }));
211
+ }
@@ -0,0 +1,82 @@
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 [contextEditorOpen, setContextEditorOpen] = useState(false);
19
+ const [commandValue, setCommandValue] = useState(initial.command ?? "");
20
+ const [contextValue, setContextValue] = useState(initial.context ?? "");
21
+ const fieldCallbacksRef = useRef({});
22
+ const taskModeInitial = initial.taskMode === "existing" ? "Existing task" : "Inline command";
23
+ const resolvedTaskName = useMemo(() => {
24
+ const tid = selectedTaskId ?? initial.taskId;
25
+ if (!tid)
26
+ return null;
27
+ const displayName = selectedTaskName ?? tasks.find((t) => t.id === tid)?.name;
28
+ if (displayName)
29
+ return `${displayName} (${tid.slice(0, 8)})`;
30
+ return `${tid.slice(0, 8)}`;
31
+ }, [selectedTaskName, selectedTaskId, initial.taskId, tasks]);
32
+ const steps = useCreateSteps({
33
+ initial,
34
+ selectedTaskId,
35
+ resolvedTaskName,
36
+ commandValue,
37
+ contextValue,
38
+ projects: props.projects,
39
+ setOpenSelect,
40
+ setTaskPickerOpen,
41
+ setCommandEditorOpen,
42
+ setContextEditorOpen,
43
+ fieldCallbacksRef,
44
+ taskModeInitial,
45
+ });
46
+ const handleComplete = useHandleComplete({
47
+ selectedTaskId,
48
+ mode,
49
+ editId,
50
+ currentProjectId,
51
+ onDone,
52
+ commandValue,
53
+ projects: props.projects,
54
+ loopService,
55
+ });
56
+ const selectOptionsFor = useCallback((field) => {
57
+ if (field === "taskMode") {
58
+ return [t("wizard.taskModeInline"), t("wizard.taskModeExisting")].map((v) => ({ value: v, label: v }));
59
+ }
60
+ if (field === "runNow") {
61
+ return [t("wizard.runNowWait"), t("wizard.runNowNow")].map((v) => ({ value: v, label: v }));
62
+ }
63
+ return props.projects.map((p) => ({ value: p.name, label: p.name }));
64
+ }, [props.projects]);
65
+ const selectTitleFor = (field) => field === "taskMode" ? t("wizard.taskModePrompt")
66
+ : field === "runNow" ? t("wizard.runNowPrompt")
67
+ : t("wizard.projectPrompt");
68
+ 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 || contextEditorOpen }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
69
+ onChooseTask({ id: task.id, name: task.name });
70
+ setTaskPickerOpen(false);
71
+ }, onClose: () => setTaskPickerOpen(false) })) : null, openSelect ? (_jsx(SelectModal, { title: selectTitleFor(openSelect), options: selectOptionsFor(openSelect), initialValue: fieldCallbacksRef.current[openSelect]?.value, onSelect: (option) => {
72
+ fieldCallbacksRef.current[openSelect]?.onChange(option.value);
73
+ fieldCallbacksRef.current[openSelect]?.onAdvance();
74
+ setOpenSelect(null);
75
+ }, onClose: () => setOpenSelect(null) })) : null, commandEditorOpen ? (_jsx(CodeEditorModal, { initialValue: commandValue, onSave: (v) => {
76
+ setCommandValue(v);
77
+ setCommandEditorOpen(false);
78
+ }, onCancel: () => setCommandEditorOpen(false) })) : null, contextEditorOpen ? (_jsx(CodeEditorModal, { initialValue: contextValue, onSave: (v) => {
79
+ setContextValue(v);
80
+ setContextEditorOpen(false);
81
+ }, onCancel: () => setContextEditorOpen(false) })) : null] }));
82
+ }
@@ -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
+ }
@@ -0,0 +1,199 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useMemo, useRef } 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 { useLoopFormValidation } from "../../shared/hooks/useLoopFormValidation.js";
7
+ import { copyToClipboard } from "../../shared/clipboard.js";
8
+ import { sanitizePaste } from "../../shared/utils/paste.js";
9
+ import { TextField } from "./TextField.js";
10
+ export function WizardForm(props) {
11
+ const { title, steps, onComplete, onCancel, disabled = false } = props;
12
+ const [values, setValues] = useState({});
13
+ const [activeField, setActiveField] = useState(0);
14
+ const [validationErrors, setValidationErrors] = useState({});
15
+ const { validateField } = useLoopFormValidation();
16
+ const step = steps[activeField];
17
+ const valueFor = useCallback((s) => values[s.key] ?? s.defaultValue ?? "", [values]);
18
+ // Resolved values: raw values with defaults filled in.
19
+ // skip() functions must receive these so they work before the user touches a field.
20
+ const resolvedValues = useMemo(() => {
21
+ const result = {};
22
+ for (const s of steps)
23
+ result[s.key] = values[s.key] ?? s.defaultValue ?? "";
24
+ return result;
25
+ }, [steps, values]);
26
+ // Synchronous mirror of resolvedValues updated inside setValue so
27
+ // findNextField sees the new value immediately, before React re-renders.
28
+ // Without this, onChange("Existing task") + onAdvance() run in the same
29
+ // tick: setValues queues a state update, but findNextField reads stale
30
+ // resolvedValues where taskMode is still "" → taskId is wrongly skipped.
31
+ const computeResolved = (raw) => {
32
+ const result = {};
33
+ for (const s of steps)
34
+ result[s.key] = raw[s.key] ?? s.defaultValue ?? "";
35
+ return result;
36
+ };
37
+ const resolvedValuesRef = useRef(computeResolved({}));
38
+ const setValue = useCallback((key, next) => {
39
+ resolvedValuesRef.current = computeResolved({ ...resolvedValuesRef.current, [key]: next });
40
+ setValues((prev) => ({ ...prev, [key]: next }));
41
+ }, [steps]);
42
+ const clearError = useCallback((key) => {
43
+ setValidationErrors((prev) => {
44
+ if (!(key in prev))
45
+ return prev;
46
+ const next = { ...prev };
47
+ delete next[key];
48
+ return next;
49
+ });
50
+ }, []);
51
+ const setError = useCallback((key, msg) => {
52
+ setValidationErrors((prev) => ({ ...prev, [key]: msg }));
53
+ }, []);
54
+ const submit = useCallback(() => {
55
+ const errors = {};
56
+ for (const s of steps) {
57
+ if (s.skip && s.skip(resolvedValues))
58
+ continue;
59
+ const err = validateField(s.key, resolvedValues);
60
+ if (err)
61
+ errors[s.key] = err;
62
+ if (s.validate && !s.validate(valueFor(s))) {
63
+ errors[s.key] = s.validationError ?? t("wizard.validationError");
64
+ }
65
+ }
66
+ if (Object.keys(errors).length > 0) {
67
+ setValidationErrors(errors);
68
+ const firstErrorIdx = steps.findIndex((s) => s.key in errors);
69
+ if (firstErrorIdx >= 0)
70
+ setActiveField(firstErrorIdx);
71
+ return;
72
+ }
73
+ const missing = steps.find((s) => !s.skip?.(values) && s.required && !valueFor(s).trim());
74
+ if (missing) {
75
+ setActiveField(steps.indexOf(missing));
76
+ return;
77
+ }
78
+ const result = {};
79
+ for (const s of steps) {
80
+ if (s.skip && s.skip(resolvedValues))
81
+ continue;
82
+ result[s.key] = valueFor(s);
83
+ }
84
+ onComplete(result);
85
+ }, [steps, valueFor, onComplete, resolvedValues]);
86
+ const findNextField = useCallback((from, delta) => {
87
+ let next = from + delta;
88
+ const len = steps.length;
89
+ const vals = resolvedValuesRef.current;
90
+ while (next >= 0 && next < len) {
91
+ const candidate = steps[next];
92
+ if (!candidate.skip || !candidate.skip(vals))
93
+ return next;
94
+ next += delta;
95
+ }
96
+ return from;
97
+ }, [steps]);
98
+ const moveField = useCallback((delta) => {
99
+ if (step) {
100
+ const err = validateField(step.key, resolvedValues);
101
+ if (err) {
102
+ setError(step.key, err);
103
+ return;
104
+ }
105
+ if (step.validate) {
106
+ const value = valueFor(step);
107
+ if (!step.validate(value)) {
108
+ setError(step.key, step.validationError ?? t("wizard.validationError"));
109
+ return;
110
+ }
111
+ }
112
+ clearError(step.key);
113
+ }
114
+ const next = findNextField(activeField, delta);
115
+ if (next !== activeField)
116
+ setActiveField(next);
117
+ }, [step, valueFor, resolvedValues, activeField, findNextField, setError, clearError]);
118
+ const visibleSteps = steps.filter((s) => !s.skip || !s.skip(resolvedValues));
119
+ useInput((input, key) => {
120
+ if (input.includes("[<"))
121
+ return;
122
+ if (key.escape) {
123
+ onCancel();
124
+ return;
125
+ }
126
+ if (key.ctrl && input === "s") {
127
+ submit();
128
+ return;
129
+ }
130
+ if (key.ctrl && input === "y") {
131
+ if (step) {
132
+ copyToClipboard(valueFor(step));
133
+ }
134
+ return;
135
+ }
136
+ if (key.tab) {
137
+ moveField(key.shift ? -1 : 1);
138
+ return;
139
+ }
140
+ if (!step)
141
+ return;
142
+ if (step.renderCustom) {
143
+ // If onActivate is set, this is a "launch modal" field:
144
+ // Enter opens the modal, everything else is blocked.
145
+ if (step.onActivate) {
146
+ if (key.return)
147
+ step.onActivate();
148
+ }
149
+ // If no onActivate, the renderCustom component owns its own useInput
150
+ // for text editing (Enter, arrows, backspace). WizardForm only handles
151
+ // Esc / Ctrl+S / Tab which are already caught above.
152
+ return;
153
+ }
154
+ // Text field editing
155
+ if (key.return) {
156
+ moveField(1);
157
+ return;
158
+ }
159
+ if (key.delete || key.backspace) {
160
+ setValue(step.key, valueFor(step).slice(0, -1));
161
+ return;
162
+ }
163
+ // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
164
+ if (input.includes("\x1b[200~")) {
165
+ setValue(step.key, valueFor(step) + sanitizePaste(input));
166
+ return;
167
+ }
168
+ // Multi-char containing CR/LF with no bracketed markers ignore
169
+ if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
170
+ return;
171
+ // Multi-char printable input = unbracketed single-line paste (e.g. right-click)
172
+ if (input.length > 1 && !key.meta) {
173
+ setValue(step.key, valueFor(step) + sanitizePaste(input));
174
+ return;
175
+ }
176
+ if (input.length === 1 && input >= " " && input <= "~") {
177
+ setValue(step.key, valueFor(step) + input);
178
+ }
179
+ }, { isActive: !disabled });
180
+ const mid = Math.ceil(steps.length / 2);
181
+ const columns = [
182
+ [steps.slice(0, mid), 0],
183
+ [steps.slice(mid), mid],
184
+ ];
185
+ const filledCount = visibleSteps.filter((s) => valueFor(s).trim().length > 0).length;
186
+ function renderField(s, fieldIdx) {
187
+ const isActive = fieldIdx === activeField;
188
+ const val = valueFor(s);
189
+ if (s.skip && s.skip(resolvedValues))
190
+ return null;
191
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: isActive ? theme.accent.brand : theme.text.muted, bold: true, children: isActive ? "\u203a " : " " }), _jsx(Text, { color: isActive ? theme.accent.brand : theme.text.secondary, bold: true, children: s.prompt }), s.required ? _jsx(Text, { color: theme.semantic.danger, children: "*" }) : null] }), _jsx(Box, { paddingLeft: 2, width: "100%", children: s.renderCustom ? (s.renderCustom({
192
+ value: val,
193
+ isActive,
194
+ onChange: (next) => setValue(s.key, next),
195
+ onAdvance: () => moveField(1),
196
+ })) : (_jsx(TextField, { value: val, hint: s.hint, isActive: isActive })) }), validationErrors[s.key] ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: theme.semantic.danger, children: validationErrors[s.key] }) })) : null] }, s.key));
197
+ }
198
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: theme.bg.surface, paddingY: 1, paddingX: 2, flexGrow: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { color: theme.accent.loop, bold: true, children: title }), _jsxs(Text, { color: theme.text.muted, children: [t("wizard.filled", { filled: filledCount, total: visibleSteps.length }), " . ", t("wizard.footerHints")] })] }), _jsx(Box, { flexDirection: "row", gap: 4, children: columns.map(([col, offset]) => (_jsx(Box, { flexDirection: "column", width: "50%", children: col.map((s, i) => renderField(s, i + offset)) }, offset))) })] }));
199
+ }