loop-task 1.5.5 → 2.0.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 (105) hide show
  1. package/README.md +141 -21
  2. package/dist/board/App.js +1 -1
  3. package/dist/board/components/ActionButtons.js +1 -1
  4. package/dist/board/components/BoardButton.js +1 -1
  5. package/dist/board/components/ConfirmModal.js +1 -1
  6. package/dist/board/components/ContextHelpModal.js +1 -1
  7. package/dist/board/components/CreateForm.js +1 -1
  8. package/dist/board/components/CreateProjectModal.js +1 -1
  9. package/dist/board/components/DeleteProjectConfirm.js +1 -1
  10. package/dist/board/components/DetailView.js +1 -1
  11. package/dist/board/components/EditProjectModal.js +1 -1
  12. package/dist/board/components/FilterBar.js +1 -1
  13. package/dist/board/components/Footer.js +1 -1
  14. package/dist/board/components/Header.js +1 -1
  15. package/dist/board/components/HelpModal.js +1 -1
  16. package/dist/board/components/Inspector.js +1 -1
  17. package/dist/board/components/LogModal.js +1 -1
  18. package/dist/board/components/Navigator.js +1 -1
  19. package/dist/board/components/ProjectsModal.js +1 -1
  20. package/dist/board/components/ProjectsPage.js +1 -1
  21. package/dist/board/components/RunHistory.js +1 -1
  22. package/dist/board/components/SearchBox.js +1 -1
  23. package/dist/board/components/SearchSelect.js +1 -1
  24. package/dist/board/components/TaskBrowser.js +1 -1
  25. package/dist/board/components/TaskFilterBar.js +1 -1
  26. package/dist/board/components/TaskForm.js +1 -1
  27. package/dist/board/components/Timeline.js +1 -1
  28. package/dist/board/focus-context.js +1 -1
  29. package/dist/board/format.js +15 -6
  30. package/dist/board/index.js +1 -1
  31. package/dist/board/toast.js +1 -1
  32. package/dist/cli/import-validator.js +151 -0
  33. package/dist/cli/import-writer.js +77 -0
  34. package/dist/cli.js +111 -17
  35. package/dist/config/constants.js +52 -0
  36. package/dist/daemon/file-watcher.js +171 -0
  37. package/dist/daemon/http-server.js +605 -0
  38. package/dist/daemon/index.js +36 -6
  39. package/dist/daemon/manager.js +94 -13
  40. package/dist/daemon/projects.js +10 -0
  41. package/dist/daemon/server.js +21 -0
  42. package/dist/daemon/task-manager.js +7 -0
  43. package/dist/esm-loader.js +4 -1
  44. package/dist/hooks/useLoopFormValidation.js +140 -0
  45. package/dist/i18n/en.json +237 -17
  46. package/dist/shared/fs-utils.js +34 -1
  47. package/dist/tui/app.js +675 -151
  48. package/dist/tui/commands.js +114 -0
  49. package/dist/tui/components/ActionButtons.js +49 -0
  50. package/dist/tui/components/Button.js +18 -0
  51. package/dist/tui/components/ChainEditor.js +112 -0
  52. package/dist/tui/components/CommandBuilderField.js +143 -0
  53. package/dist/tui/components/CommandEditorModal.js +159 -0
  54. package/dist/tui/components/CommandInput.js +303 -0
  55. package/dist/tui/components/CommandsBrowserModal.js +77 -0
  56. package/dist/tui/components/ConfirmModal.js +9 -0
  57. package/dist/tui/components/ContextHelpModal.js +8 -0
  58. package/dist/tui/components/CreateForm.js +223 -0
  59. package/dist/tui/components/DebugPanel.js +8 -0
  60. package/dist/tui/components/ExportModal.js +37 -0
  61. package/dist/tui/components/FilterBar.js +13 -0
  62. package/dist/tui/components/FocusableButton.js +16 -0
  63. package/dist/tui/components/FocusableInput.js +69 -0
  64. package/dist/tui/components/FocusableList.js +46 -0
  65. package/dist/tui/components/FocusableSearchSelect.js +82 -0
  66. package/dist/tui/components/Footer.js +42 -0
  67. package/dist/tui/components/Header.js +40 -0
  68. package/dist/tui/components/HelpGuideModal.js +21 -0
  69. package/dist/tui/components/HelpModal.js +59 -0
  70. package/dist/tui/components/InlineCommandEditor.js +143 -0
  71. package/dist/tui/components/Inspector.js +31 -0
  72. package/dist/tui/components/LeftPanel.js +33 -0
  73. package/dist/tui/components/LogModal.js +121 -0
  74. package/dist/tui/components/Modal.js +11 -0
  75. package/dist/tui/components/Navigator.js +68 -0
  76. package/dist/tui/components/PatchEditForm.js +78 -0
  77. package/dist/tui/components/ProjectForm.js +64 -0
  78. package/dist/tui/components/ProjectsModal.js +40 -0
  79. package/dist/tui/components/ProjectsPage.js +99 -0
  80. package/dist/tui/components/RightPanel.js +34 -0
  81. package/dist/tui/components/RunHistory.js +108 -0
  82. package/dist/tui/components/SearchSelect.js +67 -0
  83. package/dist/tui/components/SelectModal.js +76 -0
  84. package/dist/tui/components/TabBar.js +20 -0
  85. package/dist/tui/components/TaskBrowser.js +96 -0
  86. package/dist/tui/components/TaskFilterBar.js +9 -0
  87. package/dist/tui/components/TaskForm.js +134 -0
  88. package/dist/tui/components/TaskPickerModal.js +60 -0
  89. package/dist/tui/components/Toast.js +37 -0
  90. package/dist/tui/components/WelcomeScreen.js +70 -0
  91. package/dist/tui/components/WizardForm.js +178 -0
  92. package/dist/tui/daemon.js +142 -0
  93. package/dist/tui/format.js +111 -0
  94. package/dist/tui/hooks/useBreakpoint.js +7 -0
  95. package/dist/tui/hooks/useHoverState.js +11 -0
  96. package/dist/tui/hooks/useLogStream.js +32 -0
  97. package/dist/tui/hooks/useLoopPolling.js +28 -0
  98. package/dist/tui/index.js +24 -0
  99. package/dist/tui/router.js +16 -0
  100. package/dist/tui/state.js +175 -0
  101. package/dist/tui/theme.js +96 -0
  102. package/dist/tui/types.js +1 -0
  103. package/dist/tui/utils/paste.js +10 -0
  104. package/dist/tui/utils/validation.js +56 -0
  105. package/package.json +13 -8
@@ -0,0 +1,33 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { darkTheme as theme, tabAccentColor } from "../theme.js";
5
+ import { t } from "../../i18n/index.js";
6
+ import { Navigator } from "./Navigator.js";
7
+ import { TaskNavigator } from "./TaskBrowser.js";
8
+ import { FocusableList } from "./FocusableList.js";
9
+ function Chip(props) {
10
+ const { label, value, highlighted } = props;
11
+ return (_jsxs(Text, { color: highlighted ? theme.text.primary : theme.text.muted, children: [label, " ", value] }));
12
+ }
13
+ export function LeftPanel(props) {
14
+ const { isFocused, navActive = true, activeTab, query, loops, selectedIndex, filters, sort, breakpoint, projects, onSelect, onActivate, tasks, taskSelectedIndex, onTaskSelect, onTaskActivate, currentProjectName, projectFilters, projectSelectedIndex = 0, onProjectSelect, onProjectActivate, projectLoops, } = props;
15
+ const accentColor = tabAccentColor(activeTab);
16
+ const borderColor = isFocused ? accentColor : theme.border.default;
17
+ const hasFilter = query.length > 0;
18
+ const loopCountFor = (projectId) => (projectLoops ?? loops).filter((l) => l.projectId === projectId).length;
19
+ return (_jsxs(Box, { flexDirection: "column", width: "60%", flexShrink: 0, borderStyle: "single", borderColor: borderColor, children: [_jsxs(Box, { paddingLeft: 1, gap: 1, children: [hasFilter ? (_jsx(Text, { color: accentColor, children: t("board.chipFilter", { query }) })) : null, activeTab === "loops" ? (_jsxs(_Fragment, { children: [_jsx(Chip, { label: t("board.chipProject"), value: currentProjectName ?? t("board.chipAll"), highlighted: currentProjectName != null }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipStatus"), value: filters.status, highlighted: filters.status !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipSort"), value: sort, highlighted: sort !== "description" })] })) : null, activeTab === "projects" && projectFilters ? (_jsxs(_Fragment, { children: [projectFilters.query.length > 0 ? (_jsx(Text, { color: accentColor, children: t("board.chipSearch", { query: projectFilters.query }) })) : null, _jsx(Chip, { label: t("board.chipLoops"), value: projectFilters.hasLoops, highlighted: projectFilters.hasLoops !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipType"), value: projectFilters.isSystem, highlighted: projectFilters.isSystem !== "all" }), _jsx(Text, { color: theme.text.muted, children: "\u00B7" }), _jsx(Chip, { label: t("board.chipSort"), value: projectFilters.sort, highlighted: projectFilters.sort !== "name" })] })) : null] }), activeTab === "loops" ? (_jsx(Navigator, { visible: loops, total: loops.length, selectedIndex: selectedIndex, breakpoint: breakpoint, projects: projects, onSelect: onSelect, onActivate: onActivate, isFocused: isFocused, navActive: navActive })) : activeTab === "tasks" ? (_jsx(TaskNavigator, { visible: tasks, total: tasks.length, selectedIndex: taskSelectedIndex, query: query, onSelect: onTaskSelect, onActivate: onTaskActivate, isFocused: isFocused, navActive: navActive, allTasks: tasks })) : (_jsx(ProjectNavigator, { projects: projects, loops: projectLoops ?? loops, selectedIndex: projectSelectedIndex, onSelect: onProjectSelect ?? (() => { }), onActivate: onProjectActivate ?? (() => { }), isFocused: isFocused, navActive: navActive, loopCountFor: loopCountFor }))] }));
20
+ }
21
+ function ProjectNavigator(props) {
22
+ const { projects, selectedIndex, onSelect, onActivate, isFocused, navActive = true, loopCountFor } = props;
23
+ if (projects.length === 0) {
24
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
25
+ }
26
+ return (_jsx(FocusableList, { items: projects, selectedIndex: selectedIndex, isFocused: isFocused, navActive: navActive, limit: 15, onSelect: onSelect, onActivate: onActivate, renderItem: (project, isSelected) => {
27
+ const count = loopCountFor(project.id);
28
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
29
+ const countFg = isSelected ? theme.text.inverse : theme.text.muted;
30
+ const createdFg = isSelected ? theme.text.inverse : theme.text.muted;
31
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: project.color, children: "\u25CF" }), _jsxs(Text, { color: fg, children: [" ", project.name.padEnd(18)] }), _jsxs(Text, { color: countFg, children: [String(count).padStart(3), " "] }), _jsx(Text, { color: createdFg, children: project.createdAt.slice(0, 10) })] }));
32
+ } }));
33
+ }
@@ -0,0 +1,121 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../theme.js";
5
+ import { Modal } from "./Modal.js";
6
+ import { t } from "../../i18n/index.js";
7
+ import { streamRunLog } from "../daemon.js";
8
+ import { formatDate } from "../format.js";
9
+ import { copyToClipboard } from "../../shared/clipboard.js";
10
+ const MAX_VISIBLE_LINES = 20;
11
+ function colorForLine(line, run) {
12
+ if (line.includes("[Run #"))
13
+ return theme.accent.loop;
14
+ if (line.includes("--- Chain:"))
15
+ return theme.accent.task;
16
+ if (line.trimStart().startsWith("[exit")) {
17
+ const match = /\[exit\s+(\d+)/.exec(line);
18
+ const code = match ? Number(match[1]) : run.exitCode;
19
+ return code === 0 ? theme.semantic.success : theme.semantic.danger;
20
+ }
21
+ return theme.text.primary;
22
+ }
23
+ export function LogModal(props) {
24
+ const [lines, setLines] = useState(props.logLines);
25
+ const [streaming, setStreaming] = useState(false);
26
+ const [searchMode, setSearchMode] = useState(false);
27
+ const [searchQuery, setSearchQuery] = useState("");
28
+ const [follow, setFollow] = useState(true);
29
+ const [scrollOffset, setScrollOffset] = useState(0);
30
+ useEffect(() => {
31
+ setLines(props.logLines);
32
+ }, [props.logLines]);
33
+ useEffect(() => {
34
+ if (!props.loopId)
35
+ return;
36
+ if (props.run.status !== "running")
37
+ return;
38
+ setStreaming(true);
39
+ const socket = streamRunLog(props.loopId, props.run.runNumber, (line) => setLines((prev) => [...prev, line]), () => setStreaming(false), () => setStreaming(false));
40
+ return () => {
41
+ socket.destroy();
42
+ };
43
+ }, [props.loopId, props.run.runNumber, props.run.status]);
44
+ const filtered = searchQuery
45
+ ? lines.filter((l) => l.toLowerCase().includes(searchQuery.toLowerCase()))
46
+ : lines;
47
+ const totalLines = filtered.length;
48
+ const startIdx = follow
49
+ ? Math.max(0, totalLines - MAX_VISIBLE_LINES)
50
+ : scrollOffset;
51
+ const endIdx = startIdx + MAX_VISIBLE_LINES;
52
+ const visible = filtered.slice(startIdx, endIdx);
53
+ useInput((input, key) => {
54
+ if (searchMode) {
55
+ if (key.escape || key.return) {
56
+ setSearchMode(false);
57
+ return;
58
+ }
59
+ if (key.backspace || key.delete) {
60
+ setSearchQuery((q) => q.slice(0, -1));
61
+ return;
62
+ }
63
+ if (input && !key.ctrl && input.length === 1 && input >= " " && input <= "~") {
64
+ setSearchQuery((q) => q + input);
65
+ return;
66
+ }
67
+ return;
68
+ }
69
+ if (key.escape) {
70
+ // Escape handled by App's global popLayer(); no-op here to avoid double-fire
71
+ return;
72
+ }
73
+ if (input === "/") {
74
+ setSearchMode(true);
75
+ setSearchQuery("");
76
+ return;
77
+ }
78
+ if (input === "c" && !key.ctrl) {
79
+ copyToClipboard(lines.join("\n"));
80
+ props.onCopy?.();
81
+ return;
82
+ }
83
+ if (key.downArrow) {
84
+ setFollow(false);
85
+ setScrollOffset((o) => Math.min(o + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
86
+ return;
87
+ }
88
+ if (key.upArrow) {
89
+ setFollow(false);
90
+ setScrollOffset((o) => Math.max(0, o - 1));
91
+ return;
92
+ }
93
+ });
94
+ const isLoading = props.loading || streaming;
95
+ const statusIcon = props.run.status === "running"
96
+ ? "\u21bb"
97
+ : props.run.exitCode === 0
98
+ ? "\u2713"
99
+ : "\u2717";
100
+ const statusColor = props.run.status === "running"
101
+ ? theme.semantic.warning
102
+ : props.run.exitCode === 0
103
+ ? theme.semantic.success
104
+ : theme.semantic.danger;
105
+ return (_jsxs(Modal, { title: `Run #${props.run.runNumber} - ${formatDate(props.run.startedAt)}`, onClose: props.onClose, width: "95%", height: "70%", children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " ", props.run.status === "running"
106
+ ? t("board.logModalRunning")
107
+ : 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
108
+ ? `/${searchQuery}`
109
+ : follow
110
+ ? "f follow"
111
+ : `[${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", children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: isLoading
112
+ ? t("board.logModalLoading")
113
+ : searchQuery
114
+ ? t("board.logModalNoMatches")
115
+ : t("board.logModalEmpty") })) : (visible.map((line, i) => {
116
+ const realIdx = startIdx + i;
117
+ return (_jsx(Text, { color: colorForLine(line, props.run), wrap: "truncate", children: line }, realIdx));
118
+ })) }), _jsx(Box, { marginTop: 1, justifyContent: "space-between", children: _jsx(Text, { color: theme.text.muted, children: searchMode
119
+ ? t("board.logModalSearchHint")
120
+ : t("board.logModalFooterHints") }) })] }));
121
+ }
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ export function Modal(props) {
5
+ useInput((_input, key) => {
6
+ if (key.escape) {
7
+ props.onClose();
8
+ }
9
+ });
10
+ 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: props.width ?? "60%", height: props.height, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: props.title }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, children: props.children })] }) }));
11
+ }
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import { ScrollList } from "ink-scroll-list";
5
+ import { darkTheme as theme, statusColor } from "../theme.js";
6
+ import { describeLoop, sinceLabel, statusLabel, timingLabel, truncate } from "../format.js";
7
+ import { t } from "../../i18n/index.js";
8
+ const DESC_WIDTH = 32;
9
+ const SINCE_WIDTH = 13;
10
+ const RUNS_WIDTH = 4;
11
+ const SKIPPED_WIDTH = 4;
12
+ const STATUS_WIDTH = 8;
13
+ const COL_GAP = 1;
14
+ const LIMIT = 15;
15
+ export function Navigator(props) {
16
+ const { visible, total, selectedIndex, projects, onSelect, onActivate, isFocused, navActive = true } = props;
17
+ const n = visible.length;
18
+ useInput((input, key) => {
19
+ if (n === 0)
20
+ return;
21
+ if (key.upArrow || input === "k") {
22
+ const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
23
+ onSelect(next);
24
+ return;
25
+ }
26
+ if (key.downArrow || input === "j") {
27
+ const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
28
+ onSelect(next);
29
+ return;
30
+ }
31
+ if (key.ctrl && key.return) {
32
+ onActivate(selectedIndex);
33
+ return;
34
+ }
35
+ }, { isActive: isFocused && navActive });
36
+ const title = t("board.navigatorCount", {
37
+ visible: String(visible.length),
38
+ total: String(total),
39
+ });
40
+ function projectColor(loop) {
41
+ const proj = projects.find((p) => p.id === loop.projectId);
42
+ return proj?.color ?? theme.text.muted;
43
+ }
44
+ function isFailed(loop) {
45
+ return loop.lastExitCode !== null && loop.lastExitCode !== 0;
46
+ }
47
+ function renderLoop(loop, isSelected) {
48
+ const desc = truncate(describeLoop(loop), DESC_WIDTH);
49
+ const since = sinceLabel(loop);
50
+ const timing = timingLabel(loop);
51
+ const failed = isFailed(loop);
52
+ const sColor = failed ? theme.semantic.danger : statusColor(loop.status);
53
+ const sLabel = statusLabel(loop.status);
54
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
55
+ const dotChar = failed ? "\u2717 " : "\u25cf ";
56
+ const dotColor = failed
57
+ ? theme.semantic.danger
58
+ : isSelected
59
+ ? theme.text.inverse
60
+ : projectColor(loop);
61
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: dotColor, children: dotChar }), _jsx(Text, { color: fg, children: desc.padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: since.padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.runCount).padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.skippedCount).padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: isSelected ? theme.text.inverse : sColor, children: sLabel.padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: timing }), loop.status === "running" ? (_jsxs(Text, { color: theme.semantic.success, children: [" ", _jsx(Spinner, { type: "dots" })] })) : null] }));
62
+ }
63
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.noMatch") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.headerDescription").padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSince").padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerRuns").padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSkipped").padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerStatus").padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerTiming") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { selectedIndex: selectedIndex, height: LIMIT, children: visible.map((loop, i) => {
64
+ const isSelected = i === selectedIndex;
65
+ const indicator = isSelected ? "\u203a " : " ";
66
+ return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.active : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: indicator }), renderLoop(loop, isSelected)] }, i));
67
+ }) }) })] }))] }));
68
+ }
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ import { FocusableInput } from "./FocusableInput.js";
5
+ import { t } from "../../i18n/index.js";
6
+ import { copyToClipboard } from "../../shared/clipboard.js";
7
+ const LABEL_WIDTH = 15;
8
+ const DIVIDER_CHAR = "\u2500";
9
+ const FOCUSED_MARKER = "\u203A";
10
+ export function PatchEditForm(props) {
11
+ const { title, fields, activeField, activeFieldValue, onActiveFieldChange, onActiveFieldCommit, onActiveFieldCancel, onActiveFieldActivate, pendingChanges, focusedRowIndex, onFocusedRowChange, validationErrors, onSave, onCancel, onCopy, } = props;
12
+ useInput((_input, key) => {
13
+ if (activeField === null) {
14
+ if (key.upArrow) {
15
+ onFocusedRowChange(Math.max(0, focusedRowIndex - 1));
16
+ return;
17
+ }
18
+ if (key.downArrow) {
19
+ onFocusedRowChange(Math.min(fields.length - 1, focusedRowIndex + 1));
20
+ return;
21
+ }
22
+ if (key.return) {
23
+ const field = fields[focusedRowIndex];
24
+ if (field) {
25
+ const shown = displayValue(field.key, field.value);
26
+ onActiveFieldActivate(field.key, shown);
27
+ }
28
+ return;
29
+ }
30
+ if (key.ctrl && _input === "y") {
31
+ const field = fields[focusedRowIndex];
32
+ if (field) {
33
+ const shown = displayValue(field.key, field.value);
34
+ copyToClipboard(shown);
35
+ onCopy(shown);
36
+ }
37
+ return;
38
+ }
39
+ if (key.ctrl && _input === "s") {
40
+ onSave();
41
+ return;
42
+ }
43
+ if (key.escape) {
44
+ onCancel();
45
+ return;
46
+ }
47
+ }
48
+ else {
49
+ if (key.escape) {
50
+ onActiveFieldCancel();
51
+ return;
52
+ }
53
+ }
54
+ });
55
+ const pendingCount = Object.keys(pendingChanges).length;
56
+ const divider = DIVIDER_CHAR.repeat(50);
57
+ function displayValue(fieldKey, originalValue) {
58
+ if (fieldKey in pendingChanges) {
59
+ return pendingChanges[fieldKey];
60
+ }
61
+ return originalValue;
62
+ }
63
+ const hintText = activeField !== null
64
+ ? t("patch.hintEditing")
65
+ : pendingCount > 0
66
+ ? `${t("patch.hintNavPending")} . Ctrl+Y ${t("patch.hintCopy").toLowerCase()}`
67
+ : `${t("patch.hintNavIdle")}`;
68
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { bold: true, color: theme.accent.loop, children: title }) }), pendingCount > 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("patch.pendingChanges", { count: String(pendingCount) }) }) })) : (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("patch.noChanges") }) })), _jsx(Box, { children: _jsx(Text, { color: theme.border.dim, children: divider }) }), fields.map((field, index) => {
69
+ const isActive = activeField === field.key;
70
+ const hasPending = field.key in pendingChanges;
71
+ const shownValue = displayValue(field.key, field.value);
72
+ const isFocused = activeField === null && index === focusedRowIndex;
73
+ const marker = isFocused ? FOCUSED_MARKER : " ";
74
+ const markerColor = isFocused ? theme.accent.brand : theme.text.muted;
75
+ const fieldError = validationErrors[field.key];
76
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: markerColor, children: marker }) }), _jsx(Box, { width: LABEL_WIDTH, flexShrink: 0, children: _jsx(Text, { color: theme.text.muted, children: field.label }) }), _jsx(Box, { flexGrow: 1, children: isActive ? (_jsx(FocusableInput, { value: activeFieldValue, onChange: onActiveFieldChange, onSubmit: onActiveFieldCommit })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.text.primary, children: shownValue }), hasPending ? (_jsx(Text, { color: theme.semantic.warning, children: " \u25CF" })) : null] })) })] }), fieldError ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: LABEL_WIDTH, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: theme.semantic.danger, children: fieldError }) })] })) : null] }, field.key));
77
+ }), _jsx(Box, { children: _jsx(Text, { color: theme.border.dim, children: divider }) }), pendingCount > 0 ? (_jsxs(Box, { paddingLeft: 1, flexDirection: "row", children: [_jsx(Text, { color: theme.accent.brand, children: "[Save] Ctrl+S " }), _jsx(Text, { color: theme.text.muted, children: "[Cancel] Esc" })] })) : null, _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: hintText }) })] }));
78
+ }
@@ -0,0 +1,64 @@
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 { 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";
8
+ export function ProjectFormView(props) {
9
+ const { mode, editProject, onCancel, onDone } = props;
10
+ const colorKeyFor = (color) => {
11
+ const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
12
+ return found ?? "cyan";
13
+ };
14
+ const [colorPickerOpen, setColorPickerOpen] = useState(false);
15
+ const colorFieldRef = useRef(null);
16
+ const colorOptions = useMemo(() => PROJECT_COLOR_KEYS.map((k) => ({ value: k, label: k })), []);
17
+ const steps = useMemo(() => [
18
+ {
19
+ key: "name",
20
+ prompt: t("project.wizard.namePrompt"),
21
+ hint: t("project.wizard.nameHint"),
22
+ required: true,
23
+ inputType: "text",
24
+ defaultValue: editProject?.name ?? undefined,
25
+ },
26
+ {
27
+ key: "color",
28
+ prompt: t("project.wizard.colorPrompt"),
29
+ hint: t("project.wizard.colorHint"),
30
+ required: true,
31
+ defaultValue: colorKeyFor(editProject?.color),
32
+ onActivate: () => setColorPickerOpen(true),
33
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
34
+ colorFieldRef.current = { value, onChange, onAdvance };
35
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("project.wizard.colorPrompt"), isActive: isActive }));
36
+ },
37
+ },
38
+ ], [editProject]);
39
+ const handleComplete = useCallback((values) => {
40
+ const name = (values.name ?? "").trim();
41
+ if (!name)
42
+ return;
43
+ const colorKey = values.color ?? "cyan";
44
+ const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
45
+ if (mode === "edit" && editProject) {
46
+ updateProject(editProject.id, name, color)
47
+ .then(() => onDone(true, name))
48
+ .catch(() => { });
49
+ }
50
+ else {
51
+ createProject(name, color)
52
+ .then(() => onDone(false, name))
53
+ .catch(() => { });
54
+ }
55
+ }, [mode, editProject, onDone]);
56
+ const title = mode === "edit"
57
+ ? t("project.editTitle")
58
+ : t("project.createTitle");
59
+ 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) => {
60
+ colorFieldRef.current?.onChange(option.value);
61
+ colorFieldRef.current?.onAdvance();
62
+ setColorPickerOpen(false);
63
+ }, onClose: () => setColorPickerOpen(false) })) : null] }));
64
+ }
@@ -0,0 +1,40 @@
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 { darkTheme as theme } from "../theme.js";
5
+ import { t } from "../../i18n/index.js";
6
+ export function ProjectsModal(props) {
7
+ const { projects, loops, currentProjectId, onSelect, onClose } = props;
8
+ const [selectedIndex, setSelectedIndex] = React.useState(() => {
9
+ const allIdx = projects.findIndex((p) => p.id === "all");
10
+ if (allIdx >= 0)
11
+ return allIdx;
12
+ const currentIdx = projects.findIndex((p) => p.id === currentProjectId);
13
+ return currentIdx >= 0 ? currentIdx : 0;
14
+ });
15
+ useInput((input, key) => {
16
+ if (key.upArrow) {
17
+ setSelectedIndex((i) => i <= 0 ? projects.length - 1 : i - 1);
18
+ }
19
+ else if (key.downArrow) {
20
+ setSelectedIndex((i) => i >= projects.length - 1 ? 0 : i + 1);
21
+ }
22
+ else if (key.return) {
23
+ const project = projects[selectedIndex];
24
+ if (project)
25
+ onSelect(project.id);
26
+ }
27
+ else if (key.escape) {
28
+ onClose();
29
+ }
30
+ });
31
+ const loopCount = (pid) => loops.filter((l) => (l.projectId ?? "default") === pid).length;
32
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.elevated, flexDirection: "column", paddingX: 2, paddingY: 1, minWidth: 40, maxWidth: 60, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("project.filterTitle") }), projects.map((project, i) => {
33
+ const isSelected = i === selectedIndex;
34
+ const isActive = project.id === currentProjectId;
35
+ const prefix = isSelected ? "\u203a " : " ";
36
+ const bg = isSelected ? theme.bg.active : undefined;
37
+ const fg = isSelected ? theme.text.inverse : isActive ? theme.accent.brand : theme.text.primary;
38
+ return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, _jsx(Text, { color: project.color ?? theme.text.primary, children: "\u25cf " }), project.name, " ", _jsxs(Text, { color: theme.text.muted, children: ["(", loopCount(project.id), ")"] })] }) }, project.id));
39
+ }), _jsxs(Text, { color: theme.text.muted, children: ["\n", t("board.logModalEscClose")] })] }) }));
40
+ }
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState } from "react";
3
+ import { Box, Text, useInput, useFocus } from "ink";
4
+ import { darkTheme as theme } from "../theme.js";
5
+ import { FocusableList } from "./FocusableList.js";
6
+ import { FocusableButton } from "./FocusableButton.js";
7
+ import { Modal } from "./Modal.js";
8
+ import { t } from "../../i18n/index.js";
9
+ import { deleteProject } from "../daemon.js";
10
+ function loopCountFor(loops, projectId, defaultProjectId, knownProjectIds) {
11
+ const direct = loops.filter((l) => l.projectId === projectId).length;
12
+ if (projectId === defaultProjectId) {
13
+ const orphans = loops.filter((l) => !knownProjectIds.has(l.projectId)).length;
14
+ return direct + orphans;
15
+ }
16
+ return direct;
17
+ }
18
+ function DeleteConfirm(props) {
19
+ return (_jsxs(Modal, { title: t("project.deleteTitle"), onClose: props.onCancel, width: 50, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.text.primary, children: t("project.deleteConfirm", { name: props.project.name }) }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.deleteWarning", { count: props.loopCount }) }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(FocusableButton, { label: t("board.yes"), color: theme.semantic.danger, onPress: props.onConfirm, variant: "danger" }), _jsx(FocusableButton, { label: t("board.no"), color: theme.text.secondary, onPress: props.onCancel })] })] }));
20
+ }
21
+ function ListFocusWrapper(props) {
22
+ const { isFocused } = useFocus();
23
+ if (props.projects.length === 0) {
24
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) }));
25
+ }
26
+ const nameWidth = 20;
27
+ const loopsWidth = 12;
28
+ const headerSeparator = " \u00B7 ";
29
+ return (_jsxs(React.Fragment, { children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, children: [_jsx(Text, { color: theme.text.muted, children: " ".concat(t("project.headerName").padEnd(nameWidth)) }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, String(t("project.headerLoops")).padEnd(loopsWidth)] }), _jsxs(Text, { color: theme.text.muted, children: [headerSeparator, t("project.headerCreated")] })] }), _jsx(FocusableList, { items: props.projects, selectedIndex: props.selectedIndex, isFocused: isFocused, limit: 10, onSelect: props.onSelect, onActivate: props.onActivate, renderItem: (project, isSelected) => {
30
+ const count = loopCountFor(props.loops, project.id, props.defaultProjectId, props.knownProjectIds);
31
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
32
+ const countFg = isSelected ? theme.text.inverse : theme.text.muted;
33
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: project.color, children: "\u25CF" }), _jsxs(Text, { color: fg, children: [" ", project.name.padEnd(nameWidth - 1)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, String(count).padEnd(loopsWidth)] }), _jsxs(Text, { color: countFg, children: [headerSeparator, project.createdAt] })] }));
34
+ } }), props.children] }));
35
+ }
36
+ export function ProjectsPage(props) {
37
+ const [selectedIndex, setSelectedIndex] = useState(0);
38
+ const [subModal, setSubModal] = useState("none");
39
+ const defaultProject = props.projects.find((p) => p.isDefault) ?? null;
40
+ const defaultProjectId = defaultProject?.id ?? null;
41
+ const knownProjectIds = new Set(props.projects.map((p) => p.id));
42
+ const selected = props.projects[selectedIndex];
43
+ const loopCount = selected
44
+ ? loopCountFor(props.loops, selected.id, defaultProjectId, knownProjectIds)
45
+ : 0;
46
+ useInput((input, key) => {
47
+ if (subModal !== "none")
48
+ return;
49
+ if (key.escape) {
50
+ props.onClose();
51
+ return;
52
+ }
53
+ if (input === "n") {
54
+ if (props.onNavigateCreate) {
55
+ props.onNavigateCreate();
56
+ }
57
+ return;
58
+ }
59
+ if (input === "e" && selected) {
60
+ if (props.onNavigateEdit) {
61
+ props.onNavigateEdit(selected);
62
+ }
63
+ return;
64
+ }
65
+ if (input === "d" && selected) {
66
+ setSubModal("delete");
67
+ return;
68
+ }
69
+ });
70
+ async function handleDelete() {
71
+ if (!selected)
72
+ return;
73
+ try {
74
+ await deleteProject(selected.id);
75
+ props.onToast(t("project.toastDeleted", { name: selected.name }));
76
+ await props.onRefresh();
77
+ setSubModal("none");
78
+ setSelectedIndex(0);
79
+ }
80
+ catch (e) {
81
+ props.onToast(e.message);
82
+ }
83
+ }
84
+ if (subModal === "delete" && selected) {
85
+ return (_jsx(DeleteConfirm, { project: selected, loopCount: loopCount, onConfirm: handleDelete, onCancel: () => setSubModal("none") }));
86
+ }
87
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Box, { width: "40%", flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderColor: theme.accent.project, paddingX: 1, children: _jsx(Text, { color: theme.accent.project, bold: true, children: t("project.projectsTitle") }) }), props.projects.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) })) : (_jsx(ListFocusWrapper, { projects: props.projects, loops: props.loops, defaultProjectId: defaultProjectId, knownProjectIds: knownProjectIds, selectedIndex: selectedIndex, onSelect: setSelectedIndex, onActivate: (idx) => {
88
+ setSelectedIndex(idx);
89
+ if (props.onNavigateEdit) {
90
+ const p = props.projects[idx];
91
+ if (p)
92
+ props.onNavigateEdit(p);
93
+ }
94
+ }, children: null }))] }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginLeft: 1, children: selected ? (_jsxs(React.Fragment, { children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.brand, padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: selected.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + selected.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "ID: " }), _jsx(Text, { color: theme.text.secondary, children: selected.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Created: " }), _jsx(Text, { color: theme.text.secondary, children: selected.createdAt })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Loops: " }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: loopCount }) })] }), selected.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.brand, onPress: () => {
95
+ if (props.onNavigateEdit && selected) {
96
+ props.onNavigateEdit(selected);
97
+ }
98
+ } }), _jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: () => setSubModal("delete") })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text.muted, children: `${t("project.keyNewHint")} | ${t("project.keyEditHint")} | ${t("project.keyDeleteHint")}` }) })] })] })) : (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.noLoops") }) })) })] }) }));
99
+ }
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme, tabAccentColor } from "../theme.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { Inspector } from "./Inspector.js";
6
+ import { RunHistory } from "./RunHistory.js";
7
+ import { FocusableButton } from "./FocusableButton.js";
8
+ import { commandLine } from "../format.js";
9
+ const DIVIDER = "\u2500".repeat(40);
10
+ export function RightPanel(props) {
11
+ const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, } = props;
12
+ const borderColor = isFocused ? tabAccentColor(activeTab) : theme.border.default;
13
+ return (_jsx(Box, { flexDirection: "column", width: "40%", 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 ?? [] })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive })] })) }));
14
+ }
15
+ function TaskInspector(props) {
16
+ const { task, allTasks } = props;
17
+ if (!task) {
18
+ 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: DIVIDER }), _jsx(Text, { color: theme.text.muted, children: t("board.taskInspectorEmpty") })] }));
19
+ }
20
+ 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: DIVIDER }) }), _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 }) }), _jsx(Field, { label: t("board.taskFieldCommand"), children: _jsx(Text, { color: theme.text.primary, children: task.commandRaw
21
+ ? task.commandRaw.split("\n").filter(Boolean).join(" ")
22
+ : commandLine(task.command, task.commandArgs) }) }), _jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) }), _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(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
23
+ }
24
+ const LABEL_WIDTH = 9;
25
+ function Field(props) {
26
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), props.children] }));
27
+ }
28
+ function ProjectInspector(props) {
29
+ const { project, loopCount, onEdit, onDelete } = props;
30
+ if (!project) {
31
+ return (_jsx(Box, { padding: 1, flexGrow: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.inspectorEmpty") }) }));
32
+ }
33
+ 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.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 }))] }))] }));
34
+ }