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
@@ -1,17 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useMemo, useRef } 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 { useLoopFormValidation } from "../../hooks/useLoopFormValidation.js";
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
7
  import { copyToClipboard } from "../../shared/clipboard.js";
8
- import { sanitizePaste } from "../utils/paste.js";
9
- function TextField({ value, hint, isActive, }) {
10
- const borderColor = isActive ? theme.accent.brand : theme.border.dim;
11
- const backgroundColor = isActive ? theme.bg.input : undefined;
12
- const showHint = value.length === 0;
13
- 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] }));
14
- }
8
+ import { sanitizePaste } from "../../shared/utils/paste.js";
9
+ import { TextField } from "./TextField.js";
15
10
  export function WizardForm(props) {
16
11
  const { title, steps, onComplete, onCancel, disabled = false } = props;
17
12
  const [values, setValues] = useState({});
@@ -0,0 +1,107 @@
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
+ export function useCreateSteps(params) {
7
+ const { initial, selectedTaskId, resolvedTaskName, commandValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
8
+ return useMemo(() => {
9
+ const list = [
10
+ {
11
+ key: "interval",
12
+ prompt: t("wizard.intervalPrompt"),
13
+ hint: t("wizard.intervalHint"),
14
+ required: true,
15
+ inputType: "text",
16
+ defaultValue: initial.interval ?? undefined,
17
+ },
18
+ {
19
+ key: "taskMode",
20
+ prompt: t("wizard.taskModePrompt"),
21
+ hint: t("board.hintTaskMode"),
22
+ required: true,
23
+ defaultValue: taskModeInitial,
24
+ onActivate: () => setOpenSelect("taskMode"),
25
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
26
+ fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
27
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
28
+ },
29
+ },
30
+ {
31
+ key: "taskId",
32
+ prompt: resolvedTaskName
33
+ ? t("board.selectedTask", { name: resolvedTaskName })
34
+ : t("board.chooseTask"),
35
+ hint: t("board.hintTask"),
36
+ required: true,
37
+ inputType: "text",
38
+ defaultValue: selectedTaskId ?? initial.taskId ?? undefined,
39
+ skip: (values) => !values.taskMode?.includes("Existing"),
40
+ onActivate: () => setTaskPickerOpen(true),
41
+ renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
42
+ },
43
+ {
44
+ key: "command",
45
+ prompt: t("wizard.commandPrompt"),
46
+ hint: t("wizard.commandHint"),
47
+ required: true,
48
+ inputType: "text",
49
+ defaultValue: commandValue || undefined,
50
+ skip: (values) => !!values.taskMode?.includes("Existing"),
51
+ onActivate: () => setCommandEditorOpen(true),
52
+ renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onActivate: () => setCommandEditorOpen(true) })),
53
+ },
54
+ {
55
+ key: "runNow",
56
+ prompt: t("wizard.runNowPrompt"),
57
+ hint: t("board.hintRunNow"),
58
+ required: true,
59
+ defaultValue: initial.runNow === "true" || initial.runNow === "yes"
60
+ ? t("wizard.runNowNow")
61
+ : t("wizard.runNowWait"),
62
+ onActivate: () => setOpenSelect("runNow"),
63
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
64
+ fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
65
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
66
+ },
67
+ },
68
+ {
69
+ key: "cwd",
70
+ prompt: t("wizard.cwdPrompt"),
71
+ hint: t("wizard.cwdHint"),
72
+ required: false,
73
+ inputType: "text",
74
+ defaultValue: initial.cwd ?? undefined,
75
+ },
76
+ {
77
+ key: "description",
78
+ prompt: t("wizard.descriptionPrompt"),
79
+ hint: t("wizard.descriptionHint"),
80
+ required: true,
81
+ inputType: "text",
82
+ defaultValue: initial.description ?? undefined,
83
+ },
84
+ {
85
+ key: "maxRuns",
86
+ prompt: t("wizard.maxRunsPrompt"),
87
+ hint: t("wizard.maxRunsHint"),
88
+ required: false,
89
+ inputType: "text",
90
+ defaultValue: initial.maxRuns ?? undefined,
91
+ },
92
+ {
93
+ key: "project",
94
+ prompt: t("wizard.projectPrompt"),
95
+ hint: t("wizard.projectHint"),
96
+ required: false,
97
+ defaultValue: projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? projects[0]?.name,
98
+ onActivate: () => setOpenSelect("project"),
99
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
100
+ fieldCallbacksRef.current.project = { value, onChange, onAdvance };
101
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
102
+ },
103
+ },
104
+ ];
105
+ return list;
106
+ }, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, projects]);
107
+ }
@@ -0,0 +1,74 @@
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
+ export function useHandleComplete(params) {
6
+ const { selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects, loopService, } = params;
7
+ return useCallback((values) => {
8
+ const intervalInput = values.interval ?? "";
9
+ if (!intervalInput.trim())
10
+ return;
11
+ let interval;
12
+ try {
13
+ interval = parseDuration(intervalInput.trim());
14
+ }
15
+ catch {
16
+ return;
17
+ }
18
+ const intervalHuman = intervalInput.trim();
19
+ const isExistingTask = !!values.taskMode?.includes("Existing");
20
+ if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
21
+ return;
22
+ const cmdValue = values.command ?? commandValue;
23
+ if (!isExistingTask && !cmdValue.trim())
24
+ return;
25
+ const cmd = isExistingTask
26
+ ? ""
27
+ : joinCommandLines(cmdValue);
28
+ let cmdOnly = "";
29
+ let args = [];
30
+ if (cmd.trim()) {
31
+ try {
32
+ const tokens = parseCommandLine(cmd.trim());
33
+ cmdOnly = tokens[0] ?? "";
34
+ args = tokens.slice(1);
35
+ }
36
+ catch {
37
+ return;
38
+ }
39
+ }
40
+ const runNowValue = values.runNow === t("wizard.runNowNow");
41
+ const projectName = values.project ?? "";
42
+ const project = projects.find((p) => p.name === projectName);
43
+ const projectId = project?.id ?? currentProjectId;
44
+ const options = {
45
+ interval,
46
+ taskId: isExistingTask
47
+ ? (selectedTaskId ?? values.taskId?.trim() ?? null)
48
+ : null,
49
+ command: cmdOnly,
50
+ commandArgs: args,
51
+ commandRaw: isExistingTask ? undefined : cmdValue,
52
+ cwd: (values.cwd ?? "").trim() || process.cwd(),
53
+ immediate: runNowValue,
54
+ maxRuns: (values.maxRuns ?? "").trim()
55
+ ? parseInt(values.maxRuns, 10)
56
+ : null,
57
+ verbose: false,
58
+ description: (values.description ?? "").trim(),
59
+ projectId,
60
+ offset: null,
61
+ };
62
+ const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
63
+ if (mode === "edit" && editId) {
64
+ loopService.update(editId, options, intervalHuman)
65
+ .then((id) => onDone(true, id, desc))
66
+ .catch(() => { });
67
+ }
68
+ else {
69
+ loopService.create(options, intervalHuman)
70
+ .then((id) => onDone(false, id, desc))
71
+ .catch(() => { });
72
+ }
73
+ }, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects]);
74
+ }
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useCallback, useState, useRef } from "react";
3
- import { createProject, updateProject } from "../daemon.js";
4
- import { t } from "../../i18n/index.js";
5
- import { WizardForm } from "./WizardForm.js";
6
- import { SelectModal, SelectValueField } from "./SelectModal.js";
7
- import { PROJECT_COLORS, PROJECT_COLOR_KEYS } from "../../config/constants.js";
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";
8
9
  export function ProjectFormView(props) {
9
10
  const { mode, editProject, onCancel, onDone } = props;
11
+ const projectService = useInject(TYPES.ProjectService);
10
12
  const colorKeyFor = (color) => {
11
13
  const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
12
14
  return found ?? "cyan";
@@ -51,12 +53,12 @@ export function ProjectFormView(props) {
51
53
  const colorKey = values.color ?? "cyan";
52
54
  const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
53
55
  if (mode === "edit" && editProject) {
54
- updateProject(editProject.id, name, color, values.directory?.trim() || undefined)
56
+ projectService.update(editProject.id, name, color, values.directory?.trim() || undefined)
55
57
  .then(() => onDone(true, name))
56
58
  .catch(() => { });
57
59
  }
58
60
  else {
59
- createProject(name, color, values.directory?.trim() || undefined)
61
+ projectService.create(name, color, values.directory?.trim() || undefined)
60
62
  .then(() => onDone(false, name))
61
63
  .catch(() => { });
62
64
  }
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { darkTheme as theme, statusColor } from "../theme.js";
4
- import { describeLoop, commandLine, timeAgo, timeUntil } from "../format.js";
5
- import { t } from "../../i18n/index.js";
6
- import { resolveEffectiveCwd } from "../../core/resolve-cwd.js";
3
+ import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
4
+ import { describeLoop, commandLine, timeAgo, timeUntil } from "../../shared/ui/format.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ import { resolveEffectiveCwd } from "../../core/command/resolve-cwd.js";
7
7
  const LABEL_WIDTH = 11;
8
8
  function Field(props) {
9
9
  return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { darkTheme as theme, tabAccentColor } from "../theme.js";
4
- import { t } from "../../i18n/index.js";
3
+ import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
4
+ import { t } from "../../shared/i18n/index.js";
5
5
  import { Inspector } from "./Inspector.js";
6
6
  import { RunHistory } from "./RunHistory.js";
7
- import { FocusableButton } from "./FocusableButton.js";
8
- import { commandLine } from "../format.js";
7
+ import { FocusableButton } from "../../shared/ui/FocusableButton.js";
8
+ import { commandLine } from "../../shared/ui/format.js";
9
9
  const DIVIDER = "\u2500".repeat(40);
10
10
  export function RightPanel(props) {
11
11
  const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, projects, } = props;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { ScrollList } from "ink-scroll-list";
4
- import { darkTheme as theme } from "../theme.js";
5
- import { formatRunTime, formatRunDuration, formatFileSize } from "../format.js";
6
- import { t } from "../../i18n/index.js";
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
7
  function runIcon(run) {
8
8
  if (run.status === "running")
9
9
  return "\u21bb";
@@ -14,6 +14,54 @@ function runIconColor(run) {
14
14
  return theme.accent.loop;
15
15
  return run.exitCode === 0 ? theme.semantic.success : theme.semantic.danger;
16
16
  }
17
+ const CHAIN_LABEL_MAX = 28;
18
+ function truncateChainLabel(label) {
19
+ if (label.length <= CHAIN_LABEL_MAX)
20
+ return label;
21
+ const ellipsis = "...";
22
+ const keep = CHAIN_LABEL_MAX - ellipsis.length;
23
+ return label.slice(0, keep) + ellipsis;
24
+ }
25
+ export function groupRunsByCycle(runs) {
26
+ const byRun = new Map();
27
+ for (const r of runs) {
28
+ const group = byRun.get(r.runNumber);
29
+ if (group)
30
+ group.push(r);
31
+ else
32
+ byRun.set(r.runNumber, [r]);
33
+ }
34
+ const result = [];
35
+ for (const group of byRun.values()) {
36
+ if (group.length === 1) {
37
+ result.push(group[0]);
38
+ continue;
39
+ }
40
+ const first = group[0];
41
+ const last = group[group.length - 1];
42
+ const anyRunning = group.some((r) => r.status === "running");
43
+ const totalDuration = group.reduce((sum, r) => sum + r.duration, 0);
44
+ const totalLogSize = group.reduce((sum, r) => sum + r.logSize, 0);
45
+ const chainNames = group
46
+ .map((r) => r.chainName)
47
+ .filter((n) => Boolean(n));
48
+ const chainLabel = chainNames.length > 0
49
+ ? truncateChainLabel(`\u2192 ${chainNames.join(" \u2192 ")}`)
50
+ : undefined;
51
+ result.push({
52
+ runNumber: first.runNumber,
53
+ startedAt: first.startedAt,
54
+ exitCode: anyRunning ? -1 : last.exitCode,
55
+ duration: totalDuration,
56
+ logSize: totalLogSize,
57
+ status: anyRunning ? "running" : "completed",
58
+ logOffset: first.logOffset,
59
+ chainGroupId: first.chainGroupId,
60
+ chainName: chainLabel,
61
+ });
62
+ }
63
+ return result;
64
+ }
17
65
  const SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
18
66
  function sparkline(durations) {
19
67
  if (durations.length === 0)
@@ -65,7 +113,7 @@ function computeTrends(runs) {
65
113
  const LIMIT = 15;
66
114
  export function RunHistory(props) {
67
115
  const { loop, selectedRunIndex, onSelectRun, onOpenRun, isFocused, navActive = true } = props;
68
- const runs = loop?.runHistory ?? [];
116
+ const runs = groupRunsByCycle(loop?.runHistory ?? []);
69
117
  const reversed = [...runs].reverse();
70
118
  const n = reversed.length;
71
119
  useInput((input, key) => {
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useCallback, useState, useRef } from "react";
3
- import { WizardForm } from "./WizardForm.js";
4
- import { SelectModal, SelectValueField } from "./SelectModal.js";
5
- import { CodeEditorPreview } from "./CodeEditorPreview.js";
6
- import { CodeEditorModal } from "./CodeEditorModal.js";
7
- import { createTask, updateTask, listTasks } from "../daemon.js";
3
+ import { WizardForm } from "../loop-form/WizardForm.js";
4
+ import { SelectModal, SelectValueField } from "../../shared/ui/SelectModal.js";
5
+ import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.js";
6
+ import { CodeEditorModal } from "../../features/code-editor/CodeEditorModal.js";
7
+ import { useInject } from "../../shared/hooks/useInject.js";
8
+ import { TYPES } from "../../shared/services/types.js";
8
9
  import crypto from "node:crypto";
9
- import { t } from "../../i18n/index.js";
10
+ import { t } from "../../shared/i18n/index.js";
10
11
  import { joinCommandLines, parseCommandLine } from "../../loop-config.js";
11
12
  function parseArgs(cmd) {
12
13
  const tokens = [];
@@ -22,6 +23,7 @@ function joinCommand(task) {
22
23
  }
23
24
  export function TaskForm(props) {
24
25
  const { mode, editTask, onCancel, onDone } = props;
26
+ const taskService = useInject(TYPES.TaskService);
25
27
  const [tasks, setTasks] = useState([]);
26
28
  const [commandValue, setCommandValue] = useState(editTask
27
29
  ? (editTask.steps?.length
@@ -29,7 +31,7 @@ export function TaskForm(props) {
29
31
  : (editTask.commandRaw ?? joinCommand(editTask)))
30
32
  : "");
31
33
  useEffect(() => {
32
- listTasks().then(setTasks).catch(() => setTasks([]));
34
+ taskService.list().then(setTasks).catch(() => setTasks([]));
33
35
  }, []);
34
36
  const chainOptions = useMemo(() => [t("wizard.chainNone"), ...tasks.map((task) => task.name)], [tasks]);
35
37
  const chainSelectOptions = useMemo(() => chainOptions.map((v) => ({ value: v, label: v })), [chainOptions]);
@@ -147,13 +149,13 @@ export function TaskForm(props) {
147
149
  };
148
150
  }
149
151
  if (mode === "edit" && editTask) {
150
- updateTask(editTask.id, payload)
152
+ taskService.update(editTask.id, payload)
151
153
  .then(() => onDone(true, editTask.id))
152
154
  .catch(() => { });
153
155
  }
154
156
  else {
155
157
  const id = crypto.randomUUID().slice(0, 8);
156
- createTask({ id, ...payload })
158
+ taskService.create({ id, ...payload })
157
159
  .then((result) => onDone(false, result.id))
158
160
  .catch(() => { });
159
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "2.1.4",
3
+ "version": "2.1.7",
4
4
  "description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,6 +56,7 @@
56
56
  "ink-select-input": "^6.2.0",
57
57
  "ink-spinner": "^5.0.0",
58
58
  "ink-text-input": "^6.0.0",
59
+ "inversify": "^8.1.1",
59
60
  "inversify-hooks": "^4.0.0",
60
61
  "ms": "^2.1.3",
61
62
  "react": "^19.2.7"