@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,135 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { t } from "../../shared/i18n/index.js";
4
+ import { SelectValueField } from "../../shared/ui/SelectModal.js";
5
+ import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.js";
6
+ import { validateContext } from "../../core/context/validate-context.js";
7
+ export function useCreateSteps(params) {
8
+ const { initial, selectedTaskId, resolvedTaskName, commandValue, contextValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, setContextEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
9
+ return useMemo(() => {
10
+ const list = [
11
+ {
12
+ key: "interval",
13
+ prompt: t("wizard.intervalPrompt"),
14
+ hint: t("wizard.intervalHint"),
15
+ required: true,
16
+ inputType: "text",
17
+ defaultValue: initial.interval ?? undefined,
18
+ },
19
+ {
20
+ key: "taskMode",
21
+ prompt: t("wizard.taskModePrompt"),
22
+ hint: t("board.hintTaskMode"),
23
+ required: true,
24
+ defaultValue: taskModeInitial,
25
+ onActivate: () => setOpenSelect("taskMode"),
26
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
27
+ fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
28
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
29
+ },
30
+ },
31
+ {
32
+ key: "taskId",
33
+ prompt: resolvedTaskName
34
+ ? t("board.selectedTask", { name: resolvedTaskName })
35
+ : t("board.chooseTask"),
36
+ hint: t("board.hintTask"),
37
+ required: true,
38
+ inputType: "text",
39
+ defaultValue: selectedTaskId ?? initial.taskId ?? undefined,
40
+ skip: (values) => !values.taskMode?.includes("Existing"),
41
+ onActivate: () => setTaskPickerOpen(true),
42
+ renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
43
+ },
44
+ {
45
+ key: "command",
46
+ prompt: t("wizard.commandPrompt"),
47
+ hint: t("wizard.commandHint"),
48
+ required: true,
49
+ inputType: "text",
50
+ defaultValue: commandValue || undefined,
51
+ skip: (values) => !!values.taskMode?.includes("Existing"),
52
+ onActivate: () => setCommandEditorOpen(true),
53
+ renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onActivate: () => setCommandEditorOpen(true) })),
54
+ },
55
+ {
56
+ key: "runNow",
57
+ prompt: t("wizard.runNowPrompt"),
58
+ hint: t("board.hintRunNow"),
59
+ required: true,
60
+ defaultValue: initial.runNow === "true" || initial.runNow === "yes"
61
+ ? t("wizard.runNowNow")
62
+ : t("wizard.runNowWait"),
63
+ skip: (values) => {
64
+ const v = (values.interval ?? "").trim().toLowerCase();
65
+ return v === "manual" || v === "0";
66
+ },
67
+ onActivate: () => setOpenSelect("runNow"),
68
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
69
+ fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
70
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
71
+ },
72
+ },
73
+ {
74
+ key: "cwd",
75
+ prompt: t("wizard.cwdPrompt"),
76
+ hint: t("wizard.cwdHint"),
77
+ required: false,
78
+ inputType: "text",
79
+ defaultValue: initial.cwd ?? undefined,
80
+ },
81
+ {
82
+ key: "description",
83
+ prompt: t("wizard.descriptionPrompt"),
84
+ hint: t("wizard.descriptionHint"),
85
+ required: true,
86
+ inputType: "text",
87
+ defaultValue: initial.description ?? undefined,
88
+ },
89
+ {
90
+ key: "maxRuns",
91
+ prompt: t("wizard.maxRunsPrompt"),
92
+ hint: t("wizard.maxRunsHint"),
93
+ required: false,
94
+ inputType: "text",
95
+ defaultValue: initial.maxRuns ?? undefined,
96
+ },
97
+ {
98
+ key: "project",
99
+ prompt: t("wizard.projectPrompt"),
100
+ hint: t("wizard.projectHint"),
101
+ required: false,
102
+ defaultValue: projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? projects[0]?.name,
103
+ onActivate: () => setOpenSelect("project"),
104
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
105
+ fieldCallbacksRef.current.project = { value, onChange, onAdvance };
106
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
107
+ },
108
+ },
109
+ {
110
+ key: "context",
111
+ prompt: t("wizard.contextPrompt"),
112
+ hint: t("wizard.contextHint"),
113
+ required: false,
114
+ inputType: "text",
115
+ defaultValue: contextValue || undefined,
116
+ onActivate: () => setContextEditorOpen(true),
117
+ validate: (value) => {
118
+ if (!value?.trim())
119
+ return true;
120
+ try {
121
+ const parsed = JSON.parse(value);
122
+ const result = validateContext(parsed);
123
+ return result.valid;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ },
129
+ validationError: t("wizard.contextInvalid"),
130
+ renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: contextValue, hint: t("wizard.contextHint"), isActive: isActive, onActivate: () => setContextEditorOpen(true) })),
131
+ },
132
+ ];
133
+ return list;
134
+ }, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, contextValue, projects]);
135
+ }
@@ -0,0 +1,94 @@
1
+ import { useCallback } from "react";
2
+ import { t } from "../../shared/i18n/index.js";
3
+ import { parseDuration } from "../../duration.js";
4
+ import { parseCommandLine, joinCommandLines } from "../../loop-config.js";
5
+ import { validateContext } from "../../core/context/validate-context.js";
6
+ export function useHandleComplete(params) {
7
+ const { selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects, loopService, } = params;
8
+ return useCallback((values) => {
9
+ const intervalInput = values.interval ?? "";
10
+ if (!intervalInput.trim())
11
+ return;
12
+ let interval;
13
+ try {
14
+ interval = parseDuration(intervalInput.trim());
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ const isManual = interval === 0;
20
+ const intervalHuman = isManual ? "manual" : intervalInput.trim();
21
+ const isExistingTask = !!values.taskMode?.includes("Existing");
22
+ if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
23
+ return;
24
+ const cmdValue = values.command ?? commandValue;
25
+ if (!isExistingTask && !cmdValue.trim())
26
+ return;
27
+ const cmd = isExistingTask
28
+ ? ""
29
+ : joinCommandLines(cmdValue);
30
+ let cmdOnly = "";
31
+ let args = [];
32
+ if (cmd.trim()) {
33
+ try {
34
+ const tokens = parseCommandLine(cmd.trim());
35
+ cmdOnly = tokens[0] ?? "";
36
+ args = tokens.slice(1);
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ }
42
+ const runNowValue = values.runNow === t("wizard.runNowNow");
43
+ const projectName = values.project ?? "";
44
+ const project = projects.find((p) => p.name === projectName);
45
+ const projectId = project?.id ?? currentProjectId;
46
+ let context;
47
+ const contextStr = (values.context ?? "").trim();
48
+ if (contextStr) {
49
+ try {
50
+ const parsed = JSON.parse(contextStr);
51
+ const result = validateContext(parsed);
52
+ if (result.valid) {
53
+ context = result.context;
54
+ }
55
+ }
56
+ catch {
57
+ // invalid context, skip
58
+ }
59
+ }
60
+ const cwdInput = (values.cwd ?? "").trim();
61
+ const options = {
62
+ interval,
63
+ taskId: isExistingTask
64
+ ? (selectedTaskId ?? values.taskId?.trim() ?? null)
65
+ : null,
66
+ command: cmdOnly,
67
+ commandArgs: args,
68
+ commandRaw: isExistingTask ? undefined : cmdValue,
69
+ // On create, default an empty cwd to the current directory. On edit,
70
+ // preserve an empty cwd, the user deliberately cleared/kept it empty.
71
+ cwd: cwdInput || (mode === "edit" ? "" : process.cwd()),
72
+ immediate: isManual ? false : runNowValue,
73
+ maxRuns: (values.maxRuns ?? "").trim()
74
+ ? parseInt(values.maxRuns, 10)
75
+ : null,
76
+ verbose: false,
77
+ description: (values.description ?? "").trim(),
78
+ projectId,
79
+ offset: null,
80
+ context,
81
+ };
82
+ const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
83
+ if (mode === "edit" && editId) {
84
+ loopService.update(editId, options, intervalHuman)
85
+ .then((id) => onDone(true, id, desc))
86
+ .catch(() => { });
87
+ }
88
+ else {
89
+ loopService.create(options, intervalHuman)
90
+ .then((id) => onDone(false, id, desc))
91
+ .catch(() => { });
92
+ }
93
+ }, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects]);
94
+ }
@@ -0,0 +1,83 @@
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 "../loop-form/WizardForm.js";
7
+ import { SelectModal, SelectValueField } from "../../shared/ui/SelectModal.js";
8
+ import { PROJECT_COLORS, PROJECT_COLOR_KEYS } from "../../shared/config/constants.js";
9
+ export function ProjectFormView(props) {
10
+ const { mode, editProject, onCancel, onDone } = props;
11
+ const projectService = useInject(TYPES.ProjectService);
12
+ const colorKeyFor = (color) => {
13
+ const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
14
+ return found ?? "cyan";
15
+ };
16
+ const [colorPickerOpen, setColorPickerOpen] = useState(false);
17
+ const colorFieldRef = useRef(null);
18
+ const colorOptions = useMemo(() => PROJECT_COLOR_KEYS.map((k) => ({ value: k, label: k })), []);
19
+ const steps = useMemo(() => [
20
+ {
21
+ key: "name",
22
+ prompt: t("project.wizard.namePrompt"),
23
+ hint: t("project.wizard.nameHint"),
24
+ required: true,
25
+ inputType: "text",
26
+ defaultValue: editProject?.name ?? undefined,
27
+ },
28
+ {
29
+ key: "color",
30
+ prompt: t("project.wizard.colorPrompt"),
31
+ hint: t("project.wizard.colorHint"),
32
+ required: true,
33
+ defaultValue: colorKeyFor(editProject?.color),
34
+ onActivate: () => setColorPickerOpen(true),
35
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
36
+ colorFieldRef.current = { value, onChange, onAdvance };
37
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("project.wizard.colorPrompt"), isActive: isActive }));
38
+ },
39
+ },
40
+ {
41
+ key: "directory",
42
+ prompt: t("project.wizard.directoryPrompt"),
43
+ hint: t("project.wizard.directoryHint"),
44
+ required: false,
45
+ inputType: "text",
46
+ defaultValue: mode === "create" ? process.cwd() : (editProject?.directory || undefined),
47
+ },
48
+ {
49
+ key: "githubSource",
50
+ prompt: t("project.wizard.githubSourcePrompt"),
51
+ hint: t("project.wizard.githubSourceHint"),
52
+ required: false,
53
+ inputType: "text",
54
+ defaultValue: editProject?.githubSource || undefined,
55
+ },
56
+ ], [editProject, mode]);
57
+ const handleComplete = useCallback((values) => {
58
+ const name = (values.name ?? "").trim();
59
+ if (!name)
60
+ return;
61
+ const colorKey = values.color ?? "cyan";
62
+ const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
63
+ const githubSource = values.githubSource?.trim() || undefined;
64
+ if (mode === "edit" && editProject) {
65
+ projectService.update(editProject.id, name, color, values.directory?.trim() || undefined, githubSource)
66
+ .then(() => onDone(true, name))
67
+ .catch(() => { });
68
+ }
69
+ else {
70
+ projectService.create(name, color, values.directory?.trim() || undefined, githubSource)
71
+ .then(() => onDone(false, name))
72
+ .catch(() => { });
73
+ }
74
+ }, [mode, editProject, onDone]);
75
+ const title = mode === "edit"
76
+ ? t("project.editTitle")
77
+ : t("project.createTitle");
78
+ return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: colorPickerOpen }), colorPickerOpen ? (_jsx(SelectModal, { title: t("project.wizard.colorPrompt"), options: colorOptions, initialValue: colorFieldRef.current?.value, onSelect: (option) => {
79
+ colorFieldRef.current?.onChange(option.value);
80
+ colorFieldRef.current?.onAdvance();
81
+ setColorPickerOpen(false);
82
+ }, onClose: () => setColorPickerOpen(false) })) : null] }));
83
+ }
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
4
+ import { describeLoop, commandLine, timeAgo, timeUntil, truncate } from "../../shared/ui/format.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ function labelWidth(bp) {
7
+ return bp === "wide" ? 11 : 8;
8
+ }
9
+ function dividerLen(bp) {
10
+ if (bp === "wide")
11
+ return 40;
12
+ if (bp === "compact")
13
+ return 28;
14
+ return 18;
15
+ }
16
+ function Field(props) {
17
+ return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(props.lw) }), _jsx(Text, { color: theme.text.primary, wrap: "truncate", children: props.children })] }));
18
+ }
19
+ function MutedField(props) {
20
+ return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(props.lw) }), _jsx(Text, { color: theme.text.muted, wrap: "truncate", children: props.children })] }));
21
+ }
22
+ export function Inspector(props) {
23
+ const { loop, breakpoint = "wide" } = props;
24
+ const lw = labelWidth(breakpoint);
25
+ const divLen = dividerLen(breakpoint);
26
+ const DIVIDER = "\u2500".repeat(divLen);
27
+ const truncLen = breakpoint === "wide" ? 38 : breakpoint === "compact" ? 25 : 15;
28
+ if (!loop) {
29
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
30
+ }
31
+ const sColor = statusColor(loop.status);
32
+ const maxRunsLabel = loop.maxRuns ? String(loop.maxRuns) : t("board.unlimited");
33
+ const lastRun = loop.lastRunAt ? timeAgo(loop.lastRunAt) : t("format.dash");
34
+ const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
35
+ const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
36
+ const fullCmd = truncate(commandLine(loop.command, loop.commandArgs), truncLen);
37
+ const desc = truncate(describeLoop(loop), truncLen);
38
+ // Fields omitted in compact/minimal to save vertical space
39
+ const showOptional = breakpoint === "wide";
40
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [loop.isRecipe ? (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.semantic.warning, children: "Recipe".padEnd(lw) }), _jsx(Text, { color: theme.semantic.warning, children: loop.recipeFile ?? "" })] })) : null, _jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(lw) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldRuns"), lw: lw, children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldLastExit"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), showOptional ? (_jsx(Field, { label: t("board.fieldLastRun"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: lastRun }) })) : null, showOptional ? (_jsx(Field, { label: t("board.fieldNextRun"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: nextRun }) })) : null, (loop.silentChainCount ?? 0) > 0 ? (_jsx(Field, { label: t("board.fieldSilentChains"), lw: lw, children: _jsx(Text, { color: theme.text.muted, children: t("board.silentChainCount", { count: (loop.silentChainCount ?? 0).toLocaleString() }) }) })) : null, _jsx(MutedField, { label: t("board.fieldDesc"), lw: lw, children: desc }), _jsx(MutedField, { label: t("board.fieldCommand"), lw: lw, children: fullCmd })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
41
+ }
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
4
+ import { t } from "../../shared/i18n/index.js";
5
+ import { Inspector } from "./Inspector.js";
6
+ import { RunHistory } from "./RunHistory.js";
7
+ import { FocusableButton } from "../../shared/ui/FocusableButton.js";
8
+ import { commandLine } from "../../shared/ui/format.js";
9
+ export function RightPanel(props) {
10
+ const { isFocused, navActive = true, activeTab, breakpoint = "wide", loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, projects, } = props;
11
+ const borderColor = isFocused ? tabAccentColor(activeTab) : theme.border.default;
12
+ const { stdout } = useStdout();
13
+ const panelHeight = (stdout?.rows ?? 24) - 8;
14
+ const panelWidth = breakpoint === "wide" ? "40%" : "100%";
15
+ return (_jsx(Box, { flexDirection: "column", width: panelWidth, height: breakpoint === "wide" ? panelHeight : undefined, borderStyle: "single", borderColor: borderColor, children: activeTab === "projects" ? (_jsx(ProjectInspector, { project: selectedProject ?? null, loopCount: projectLoopCount ?? 0, onEdit: onProjectEdit, onDelete: onProjectDelete })) : activeTab === "tasks" ? (_jsx(TaskInspector, { task: selectedTask ?? null, allTasks: allTasks ?? [], breakpoint: breakpoint })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop, projects: projects, breakpoint: breakpoint }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive, breakpoint: breakpoint })] })) }));
16
+ }
17
+ function TaskInspector(props) {
18
+ const { task, allTasks, breakpoint = "wide" } = props;
19
+ const divLen = breakpoint === "wide" ? 40 : breakpoint === "compact" ? 30 : 20;
20
+ const taskDivider = "\u2500".repeat(divLen);
21
+ if (!task) {
22
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }), _jsx(Text, { color: theme.text.muted, children: taskDivider }), _jsx(Text, { color: theme.text.muted, children: t("board.taskInspectorEmpty") })] }));
23
+ }
24
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: taskDivider }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Field, { label: t("board.taskFieldName"), children: _jsx(Text, { color: theme.accent.task, bold: true, children: task.name }) }), _jsx(Field, { label: t("board.taskFieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), breakpoint !== "minimal" ? (_jsx(Field, { label: t("board.taskFieldCommand"), children: _jsx(Text, { color: theme.text.primary, children: task.commandRaw
25
+ ? task.commandRaw.split("\n").filter(Boolean).join(" ")
26
+ : commandLine(task.command, task.commandArgs) }) })) : null, breakpoint === "wide" ? (_jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) })) : null, _jsxs(Field, { label: t("board.taskFieldChain"), children: [task.onSuccessTaskId ? (_jsx(Text, { color: theme.semantic.success, children: "\u2713 " + (allTasks.find((t) => t.id === task.onSuccessTaskId)?.name ?? task.onSuccessTaskId) })) : null, task.onFailureTaskId ? (_jsx(Text, { color: theme.semantic.danger, children: " \u2192 " + (allTasks.find((t) => t.id === task.onFailureTaskId)?.name ?? task.onFailureTaskId) })) : null, !task.onSuccessTaskId && !task.onFailureTaskId ? (_jsx(Text, { color: theme.text.muted, children: t("board.taskNone") })) : null] }), _jsx(Field, { 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") }) })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: taskDivider }) })] }));
27
+ }
28
+ const LABEL_WIDTH = 9;
29
+ function Field(props) {
30
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), props.children] }));
31
+ }
32
+ function ProjectInspector(props) {
33
+ const { project, loopCount, onEdit, onDelete } = props;
34
+ if (!project) {
35
+ return (_jsx(Box, { padding: 1, flexGrow: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.inspectorEmpty") }) }));
36
+ }
37
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: project.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + project.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldId") }), _jsx(Text, { color: theme.text.secondary, children: project.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldLoops") }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: String(loopCount) }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldCreated") }), _jsx(Text, { color: theme.text.secondary, children: project.createdAt.slice(0, 10) })] }), project.directory && (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldDirectory") }), _jsx(Text, { color: theme.text.secondary, children: project.directory })] })), project.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) })), !project.isSystem && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [onEdit && (_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.project, onPress: onEdit })), onDelete && (_jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: onDelete }))] }))] }));
38
+ }
@@ -0,0 +1,187 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useInput, useStdout } from "ink";
3
+ import { ScrollList } from "ink-scroll-list";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { formatRunTime, formatRunDuration, formatFileSize } from "../../shared/ui/format.js";
6
+ import { t } from "../../shared/i18n/index.js";
7
+ import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
8
+ function runIcon(run) {
9
+ if (run.status === "running")
10
+ return "\u21bb";
11
+ return run.exitCode === 0 ? "\u2713" : "\u2717";
12
+ }
13
+ function runIconColor(run) {
14
+ if (run.status === "running")
15
+ return theme.accent.loop;
16
+ return run.exitCode === 0 ? theme.semantic.success : theme.semantic.danger;
17
+ }
18
+ function chainLabelMax(bp) {
19
+ if (bp === "wide")
20
+ return 28;
21
+ if (bp === "compact")
22
+ return 16;
23
+ return 8;
24
+ }
25
+ function truncateChainLabel(label, maxLen) {
26
+ if (label.length <= maxLen)
27
+ return label;
28
+ const ellipsis = "...";
29
+ const keep = maxLen - ellipsis.length;
30
+ return label.slice(0, keep) + ellipsis;
31
+ }
32
+ export function groupRunsByCycle(runs, bp = "wide") {
33
+ const byRun = new Map();
34
+ for (const r of runs) {
35
+ const group = byRun.get(r.runNumber);
36
+ if (group)
37
+ group.push(r);
38
+ else
39
+ byRun.set(r.runNumber, [r]);
40
+ }
41
+ const result = [];
42
+ for (const group of byRun.values()) {
43
+ if (group.length === 1) {
44
+ result.push(group[0]);
45
+ continue;
46
+ }
47
+ const first = group[0];
48
+ const last = group[group.length - 1];
49
+ const anyRunning = group.some((r) => r.status === "running");
50
+ const totalDuration = group.reduce((sum, r) => sum + r.duration, 0);
51
+ const totalLogSize = group.reduce((sum, r) => sum + r.logSize, 0);
52
+ const chainNames = group
53
+ .map((r) => r.chainName)
54
+ .filter((n) => Boolean(n));
55
+ const chainLabel = chainNames.length > 0
56
+ ? truncateChainLabel(`\u2192 ${chainNames.join(" \u2192 ")}`, chainLabelMax(bp))
57
+ : undefined;
58
+ result.push({
59
+ runNumber: first.runNumber,
60
+ startedAt: first.startedAt,
61
+ exitCode: anyRunning ? -1 : last.exitCode,
62
+ duration: totalDuration,
63
+ logSize: totalLogSize,
64
+ status: anyRunning ? "running" : "completed",
65
+ logOffset: first.logOffset,
66
+ chainGroupId: first.chainGroupId,
67
+ chainName: chainLabel,
68
+ });
69
+ }
70
+ return result;
71
+ }
72
+ const SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
73
+ function sparkline(durations) {
74
+ if (durations.length === 0)
75
+ return "";
76
+ const max = Math.max(...durations, 1);
77
+ const min = Math.min(...durations, 0);
78
+ const range = max - min || 1;
79
+ return durations.map((d) => {
80
+ const level = Math.round(((d - min) / range) * (SPARK_CHARS.length - 1));
81
+ return SPARK_CHARS[level] ?? SPARK_CHARS[0];
82
+ }).join("");
83
+ }
84
+ function computeTrends(runs) {
85
+ if (runs.length === 0) {
86
+ return { sparkline: "", avgDuration: 0, successStreak: 0, failureStreak: 0, lastFailureAgo: null };
87
+ }
88
+ const recent = runs.slice(-20);
89
+ const durations = recent.map((r) => r.duration).filter((d) => d > 0);
90
+ const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
91
+ let successStreak = 0;
92
+ let failureStreak = 0;
93
+ for (let i = runs.length - 1; i >= 0; i--) {
94
+ const r = runs[i];
95
+ if (r.exitCode === 0 && failureStreak === 0)
96
+ successStreak++;
97
+ else if (r.exitCode !== 0 && successStreak === 0)
98
+ failureStreak++;
99
+ else
100
+ break;
101
+ }
102
+ let lastFailureAgo = null;
103
+ for (let i = runs.length - 1; i >= 0; i--) {
104
+ if (runs[i].exitCode !== 0) {
105
+ const diff = Date.now() - new Date(runs[i].startedAt).getTime();
106
+ const hours = Math.floor(diff / 3600000);
107
+ const mins = Math.floor(diff / 60000);
108
+ lastFailureAgo = hours > 0 ? `${hours}h ago` : `${mins}m ago`;
109
+ break;
110
+ }
111
+ }
112
+ return {
113
+ sparkline: sparkline(durations),
114
+ avgDuration: Math.round(avgDuration),
115
+ successStreak,
116
+ failureStreak,
117
+ lastFailureAgo,
118
+ };
119
+ }
120
+ export function RunHistory(props) {
121
+ const { loop, selectedRunIndex, onSelectRun, onOpenRun, isFocused, navActive = true, breakpoint = "wide" } = props;
122
+ const { stdout } = useStdout();
123
+ const terminalHeight = stdout?.rows ?? 24;
124
+ const LIMIT = Math.max(3, terminalHeight - 22);
125
+ const runs = groupRunsByCycle(loop?.runHistory ?? [], breakpoint);
126
+ const reversed = [...runs].reverse();
127
+ const n = reversed.length;
128
+ const showSize = breakpoint === "wide";
129
+ const timePad = breakpoint === "wide" ? 10 : 8;
130
+ const durPad = breakpoint === "wide" ? 10 : 7;
131
+ useInput((input, key) => {
132
+ if (n === 0)
133
+ return;
134
+ // Swallow SGR mouse sequences so they don't interfere
135
+ if (input.includes("[<")) {
136
+ return;
137
+ }
138
+ if (key.upArrow || input === "k") {
139
+ const next = selectedRunIndex <= 0 ? n - 1 : selectedRunIndex - 1;
140
+ onSelectRun(next);
141
+ return;
142
+ }
143
+ if (key.downArrow || input === "j") {
144
+ const next = selectedRunIndex >= n - 1 ? 0 : selectedRunIndex + 1;
145
+ onSelectRun(next);
146
+ return;
147
+ }
148
+ if (key.ctrl && key.return) {
149
+ const run = reversed[selectedRunIndex];
150
+ if (run)
151
+ onOpenRun(run);
152
+ return;
153
+ }
154
+ }, { isActive: isFocused && navActive });
155
+ useMouseScroll({
156
+ onScrollUp: () => {
157
+ if (n === 0 || !(isFocused && navActive))
158
+ return;
159
+ const next = selectedRunIndex <= 0 ? n - 1 : selectedRunIndex - 1;
160
+ onSelectRun(next);
161
+ },
162
+ onScrollDown: () => {
163
+ if (n === 0 || !(isFocused && navActive))
164
+ return;
165
+ const next = selectedRunIndex >= n - 1 ? 0 : selectedRunIndex + 1;
166
+ onSelectRun(next);
167
+ },
168
+ isActive: isFocused && navActive,
169
+ });
170
+ const title = isFocused ? t("board.runHistoryTitleHint") : t("board.runHistoryTitle");
171
+ const trends = computeTrends(runs);
172
+ function renderRun(run, isSelected) {
173
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
174
+ const time = formatRunTime(run.startedAt);
175
+ const duration = formatRunDuration(run.duration);
176
+ const size = formatFileSize(run.logSize);
177
+ const icon = runIcon(run);
178
+ const iconColor = runIconColor(run);
179
+ const chain = run.chainName ?? "";
180
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: fg, children: time.padEnd(timePad) }), _jsxs(Text, { color: isSelected ? theme.text.inverse : iconColor, children: [icon, " "] }), _jsx(Text, { color: fg, children: duration.padEnd(durPad) }), showSize ? _jsx(Text, { color: fg, children: size.padEnd(8) }) : null, _jsx(Text, { color: fg, children: chain })] }));
181
+ }
182
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: title }), (trends.failureStreak > 0 || trends.lastFailureAgo) ? (_jsxs(Text, { color: theme.semantic.warning, children: [trends.failureStreak > 0 ? t("board.runHistoryFailStreak", { count: String(trends.failureStreak) }) : "", trends.failureStreak > 0 && trends.lastFailureAgo ? " " : "", trends.lastFailureAgo ? t("board.runHistoryLastFail", { ago: trends.lastFailureAgo }) : ""] })) : null] }), reversed.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryEmpty") }) })) : (_jsxs(Box, { flexDirection: "column", children: [trends.sparkline ? (_jsx(Box, { paddingLeft: 1, marginBottom: 0, children: breakpoint !== "minimal" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: "Durations: " }), _jsx(Text, { color: theme.accent.loop, children: trends.sparkline }), _jsxs(Text, { color: theme.text.muted, children: [" avg:", formatRunDuration(trends.avgDuration), " "] }), trends.successStreak > 0 ? (_jsxs(Text, { color: theme.semantic.success, children: ["streak:", trends.successStreak, " ok"] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent.loop, children: trends.sparkline }), _jsxs(Text, { color: theme.text.muted, children: [" avg:", formatRunDuration(trends.avgDuration)] })] })) })) : null, _jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: t("board.runHistoryTime").padEnd(timePad) }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryDuration").padEnd(durPad) }), showSize ? _jsx(Text, { color: theme.text.muted, children: t("board.runHistorySize").padEnd(8) }) : null, _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryDesc") })] }), _jsx(ScrollList, { selectedIndex: selectedRunIndex, height: LIMIT, children: reversed.map((run, i) => {
183
+ const isSelected = i === selectedRunIndex;
184
+ const indicator = isSelected ? "\u203a " : " ";
185
+ 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 }), renderRun(run, isSelected)] }, i));
186
+ }) })] }))] }));
187
+ }