loop-task 2.1.12 → 2.1.13

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 (57) hide show
  1. package/dist/app/App.js +3 -1
  2. package/dist/cli.js +44 -2
  3. package/dist/client/commands.js +4 -1
  4. package/dist/client/project-commands.js +7 -2
  5. package/dist/core/context/template.js +1 -1
  6. package/dist/core/context/validate-context.js +2 -2
  7. package/dist/core/loop/chain-executor.js +11 -4
  8. package/dist/core/loop/loop-controller.js +9 -0
  9. package/dist/daemon/http/openapi.js +29 -2
  10. package/dist/daemon/http/route-loops.js +85 -0
  11. package/dist/daemon/http/route-misc.js +16 -0
  12. package/dist/daemon/http/route-projects.js +2 -2
  13. package/dist/daemon/http/route-tasks.js +58 -0
  14. package/dist/daemon/http/routes.js +3 -2
  15. package/dist/daemon/http/server.js +20 -2
  16. package/dist/daemon/index.js +48 -7
  17. package/dist/daemon/managers/loop-entry.js +2 -0
  18. package/dist/daemon/managers/loop-manager.js +4 -4
  19. package/dist/daemon/managers/project-manager.js +16 -2
  20. package/dist/daemon/mcp/index.js +2 -0
  21. package/dist/daemon/mcp/openapi-sync.js +50 -0
  22. package/dist/daemon/mcp/server.js +162 -0
  23. package/dist/daemon/mcp/tools.js +368 -0
  24. package/dist/daemon/server/handlers/index.js +8 -1
  25. package/dist/daemon/server/handlers/project-handlers.js +10 -5
  26. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  27. package/dist/daemon/server/index.js +3 -1
  28. package/dist/daemon/settings-manager.js +46 -0
  29. package/dist/entities/tasks/filters.js +1 -1
  30. package/dist/features/commands/commands.js +4 -1
  31. package/dist/features/commands/useCommandHandlers.js +32 -0
  32. package/dist/features/commands/useGlobalShortcuts.js +0 -1
  33. package/dist/features/overlays/ExportModal.js +1 -1
  34. package/dist/loop-config.js +1 -1
  35. package/dist/shared/clipboard.js +2 -2
  36. package/dist/shared/config/paths.js +3 -0
  37. package/dist/shared/container/index.js +2 -0
  38. package/dist/shared/hooks/useDaemonSettings.js +39 -0
  39. package/dist/shared/hooks/useUndoRedo.js +1 -1
  40. package/dist/shared/i18n/en.json +23 -0
  41. package/dist/shared/services/project-service.js +4 -4
  42. package/dist/shared/services/settings-service.js +49 -0
  43. package/dist/shared/services/types.js +1 -0
  44. package/dist/shared/ui/FocusableInput.js +1 -1
  45. package/dist/shared/ui/SelectModal.js +1 -1
  46. package/dist/shared/utils/syntax.js +3 -3
  47. package/dist/widgets/header/Header.js +15 -2
  48. package/dist/widgets/left-panel/Navigator.js +3 -2
  49. package/dist/widgets/left-panel/TaskBrowser.js +3 -2
  50. package/dist/widgets/log-modal/LogModal.js +1 -1
  51. package/dist/widgets/loop-form/WizardForm.js +2 -2
  52. package/dist/widgets/loop-form/useHandleComplete.js +4 -1
  53. package/dist/widgets/project-form/ProjectForm.js +11 -2
  54. package/dist/widgets/right-panel/Inspector.js +1 -1
  55. package/dist/widgets/right-panel/RightPanel.js +1 -1
  56. package/dist/widgets/task-form/TaskForm.js +24 -4
  57. package/package.json +4 -2
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useInject } from "./useInject.js";
3
+ import { TYPES } from "../services/types.js";
4
+ import { POLL_MS } from "../config/constants.js";
5
+ export function useDaemonSettings() {
6
+ const settingsService = useInject(TYPES.SettingsService);
7
+ const [state, setState] = useState({
8
+ httpApiEnabled: false,
9
+ mcpApiEnabled: false,
10
+ reachable: false,
11
+ });
12
+ useEffect(() => {
13
+ let cancelled = false;
14
+ const poll = async () => {
15
+ try {
16
+ const settings = await settingsService.getSettings();
17
+ if (!cancelled) {
18
+ setState({
19
+ httpApiEnabled: settings.httpApiEnabled,
20
+ mcpApiEnabled: settings.mcpApiEnabled,
21
+ reachable: true,
22
+ });
23
+ }
24
+ }
25
+ catch {
26
+ if (!cancelled) {
27
+ setState((prev) => ({ ...prev, reachable: false }));
28
+ }
29
+ }
30
+ };
31
+ void poll();
32
+ const timer = setInterval(() => void poll(), POLL_MS);
33
+ return () => {
34
+ cancelled = true;
35
+ clearInterval(timer);
36
+ };
37
+ }, [settingsService]);
38
+ return state;
39
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useRef, useCallback } from "react";
2
2
  /**
3
- * Pure undo/redo stack logicexported for direct unit testing
3
+ * Pure undo/redo stack logic exported for direct unit testing
4
4
  * without needing React DOM or renderHook.
5
5
  */
6
6
  export class UndoRedoStack {
@@ -29,6 +29,7 @@
29
29
  "cli.statusInterval": " Interval: {interval} ({duration})",
30
30
  "cli.statusStatus": " Status: {status}",
31
31
  "cli.statusRuns": " Runs: {runs} / {maxRuns}",
32
+ "cli.statusSilentChains": " Silent: {count} silent-chain runs",
32
33
  "cli.statusCreated": " Created: {created}",
33
34
  "cli.statusLastRun": " Last run: {lastRun} {exit} {duration}",
34
35
  "cli.statusNextRun": " Next run: {nextRun}",
@@ -47,6 +48,8 @@
47
48
  "cli.projectColorDescription": "Change a project's color",
48
49
  "cli.projectDeleteDescription": "Delete a project (loops move to Default)",
49
50
  "cli.optProjectColor": "Project color (name or #hex)",
51
+ "cli.optProjectDirectory": "Project working directory",
52
+ "cli.optProjectGithubSource": "GitHub repository in owner/repo format",
50
53
  "cli.projectArgName": "Project name",
51
54
  "cli.projectArgNewName": "New project name",
52
55
  "cli.projectArgIdOrName": "Project id or name",
@@ -144,6 +147,7 @@
144
147
  "board.headerSince": "SINCE",
145
148
  "board.headerRuns": "RUNS",
146
149
  "board.headerSkipped": "SKIP",
150
+ "board.headerSilent": "SILENT",
147
151
  "board.headerStatus": "STATUS",
148
152
  "board.headerDescription": "DESCRIPTION",
149
153
  "board.headerTiming": "TIMING",
@@ -174,6 +178,7 @@
174
178
  "board.fieldLastRun": "Last run:",
175
179
  "board.fieldLastExit": "Last exit:",
176
180
  "board.fieldNextRun": "Next run:",
181
+ "board.fieldSilentChains": "Silent:",
177
182
  "board.fieldPid": "PID:",
178
183
  "board.inherit": "(inherit)",
179
184
  "board.unlimited": "∞",
@@ -210,6 +215,7 @@
210
215
  "board.detailFieldLastExit": "Last exit: ",
211
216
  "board.detailFieldNextRun": "Next run: ",
212
217
  "board.detailFieldPid": "PID: ",
218
+ "board.detailFieldSilentChains": "Silent: ",
213
219
  "board.detailSummaryStatus": "status",
214
220
  "board.detailSummaryTiming": "next",
215
221
  "board.detailSummaryInterval": "every",
@@ -400,6 +406,7 @@
400
406
  "board.taskFieldId": "ID:",
401
407
  "board.taskFieldCommand": "Command:",
402
408
  "board.taskFieldChain": "Chain:",
409
+ "board.taskFieldSilent": "Silent:",
403
410
  "board.taskFieldCreated": "Created:",
404
411
  "board.taskHeaderName": "NAME",
405
412
  "board.taskHeaderCommand": "COMMAND",
@@ -415,6 +422,9 @@
415
422
  "board.selectedTask": "Task: {name}",
416
423
  "board.taskChainsNone": "-",
417
424
  "board.taskChainsFormat": "✓:{success} ✗:{failure}",
425
+ "board.silentChainCount": "{count} silent-chain runs",
426
+ "board.silentChainYes": "yes",
427
+ "board.silentChainNo": "no",
418
428
  "board.logModalCopied": "Log copied to clipboard",
419
429
  "project.selectProject": " Select Project ",
420
430
  "project.projectsLabel": "Projects",
@@ -451,6 +461,8 @@
451
461
  "project.wizard.colorHint": "Choose a color to identify this project",
452
462
  "project.wizard.directoryPrompt": "Working directory? (optional)",
453
463
  "project.wizard.directoryHint": "Default directory for loops in this project. Leave blank to inherit",
464
+ "project.wizard.githubSourcePrompt": "GitHub Source? (optional)",
465
+ "project.wizard.githubSourceHint": "Repository in owner/repo format, e.g. CKGrafico/loop-task",
454
466
  "project.error.updateFailed": "Failed to update project",
455
467
  "project.error.deleteFailed": "Failed to delete project",
456
468
  "project.toastCreated": "Project \"{name}\" created",
@@ -510,6 +522,15 @@
510
522
  "cmd.show": "Show a hidden panel",
511
523
  "cmd.hide": "Hide a panel",
512
524
  "cmd.api": "API endpoints info",
525
+ "cmd.toggleApi": "Toggle HTTP API on/off",
526
+ "cmd.mcp": "MCP server info",
527
+ "cmd.toggleMcp": "Toggle MCP server on/off",
528
+ "board.toastApiEnabled": "HTTP API enabled",
529
+ "board.toastApiDisabled": "HTTP API disabled",
530
+ "board.toastApiToggleError": "Failed to toggle API: {message}",
531
+ "board.toastMcpEnabled": "MCP server enabled",
532
+ "board.toastMcpDisabled": "MCP server disabled",
533
+ "board.toastMcpToggleError": "Failed to toggle MCP: {message}",
513
534
  "cmd.status": "Daemon status info (Soon...)",
514
535
  "cmd.filterStatus": "Filter by status",
515
536
  "cmd.sort": "Order sort by field",
@@ -597,6 +618,8 @@
597
618
  "wizard.taskCommandPrompt": "What command should this task run?",
598
619
  "wizard.onSuccessPrompt": "On success, chain to? (optional)",
599
620
  "wizard.onFailurePrompt": "On failure, chain to? (optional)",
621
+ "wizard.silentChainPrompt": "Silent chain? (suppress log output)",
622
+ "wizard.silentChainHint": "Skip logging for this task when chained",
600
623
  "wizard.contextPrompt": "Initial context? (optional)",
601
624
  "wizard.contextHint": "JSON object with string values, e.g. {\"env\":\"staging\"}",
602
625
  "wizard.contextInvalid": "Invalid context: must be a flat JSON object with non-nested, non-array values",
@@ -14,14 +14,14 @@ let IpcProjectService = class IpcProjectService {
14
14
  throw new Error(response.message);
15
15
  return response.data;
16
16
  }
17
- async create(name, color, directory) {
18
- const response = await sendRequest({ type: "project-create", payload: { name, color, directory } });
17
+ async create(name, color, directory, githubSource) {
18
+ const response = await sendRequest({ type: "project-create", payload: { name, color, directory, githubSource } });
19
19
  if (response.type !== "ok")
20
20
  throw new Error(response.message);
21
21
  return response.data;
22
22
  }
23
- async update(id, name, color, directory) {
24
- const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory } });
23
+ async update(id, name, color, directory, githubSource) {
24
+ const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory, githubSource } });
25
25
  if (response.type !== "ok") {
26
26
  throw new Error(response.message ?? t("project.error.updateFailed"));
27
27
  }
@@ -0,0 +1,49 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import { sendRequest } from "../../client/ipc.js";
9
+ let IpcSettingsService = class IpcSettingsService {
10
+ async getSettings() {
11
+ const response = await sendRequest({ type: "settings-get" });
12
+ if (response.type !== "ok") {
13
+ throw new Error(response.message ?? "Failed to get settings");
14
+ }
15
+ return response.data;
16
+ }
17
+ async getHttpApiEnabled() {
18
+ const response = await sendRequest({ type: "settings-get" });
19
+ if (response.type !== "ok") {
20
+ throw new Error(response.message ?? "Failed to get settings");
21
+ }
22
+ return response.data.httpApiEnabled;
23
+ }
24
+ async setHttpApiEnabled(enabled) {
25
+ const response = await sendRequest({ type: "settings-set", settings: { httpApiEnabled: enabled } });
26
+ if (response.type !== "ok") {
27
+ throw new Error(response.message ?? "Failed to set settings");
28
+ }
29
+ return response.data;
30
+ }
31
+ async getMcpApiEnabled() {
32
+ const response = await sendRequest({ type: "settings-get" });
33
+ if (response.type !== "ok") {
34
+ throw new Error(response.message ?? "Failed to get settings");
35
+ }
36
+ return response.data.mcpApiEnabled;
37
+ }
38
+ async setMcpApiEnabled(enabled) {
39
+ const response = await sendRequest({ type: "settings-set", settings: { mcpApiEnabled: enabled } });
40
+ if (response.type !== "ok") {
41
+ throw new Error(response.message ?? "Failed to set settings");
42
+ }
43
+ return response.data;
44
+ }
45
+ };
46
+ IpcSettingsService = __decorate([
47
+ injectable()
48
+ ], IpcSettingsService);
49
+ export { IpcSettingsService };
@@ -4,4 +4,5 @@ export const TYPES = {
4
4
  ProjectService: Symbol.for("ProjectService"),
5
5
  LogService: Symbol.for("LogService"),
6
6
  ExportService: Symbol.for("ExportService"),
7
+ SettingsService: Symbol.for("SettingsService"),
7
8
  };
@@ -23,7 +23,7 @@ export function FocusableInput(props) {
23
23
  }
24
24
  if (key.ctrl || key.escape)
25
25
  return;
26
- // Multi-char containing CR/LF with no bracketed markersignore
26
+ // Multi-char containing CR/LF with no bracketed markers ignore
27
27
  if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
28
28
  return;
29
29
  if (key.return) {
@@ -7,7 +7,7 @@ const MAX_VISIBLE = 10;
7
7
  /**
8
8
  * Filterable modal picker: type to narrow, up/down to navigate, enter to
9
9
  * select, esc to cancel. The single interaction pattern for every enumerated
10
- * field in the boarda fixed set of two options and a list of dozens both
10
+ * field in the board a fixed set of two options and a list of dozens both
11
11
  * use this, so users only ever learn one gesture for "change this value".
12
12
  */
13
13
  export function SelectModal(props) {
@@ -10,7 +10,7 @@ function scanTokens(line) {
10
10
  let i = 0;
11
11
  const len = line.length;
12
12
  while (i < len) {
13
- // Whitespace runpreserve it as a token
13
+ // Whitespace run preserve it as a token
14
14
  if (line[i] === " " || line[i] === "\t") {
15
15
  let j = i;
16
16
  while (j < len && (line[j] === " " || line[j] === "\t"))
@@ -58,7 +58,7 @@ function scanTokens(line) {
58
58
  i = j;
59
59
  continue;
60
60
  }
61
- // Unquoted wordconsume until whitespace or operator
61
+ // Unquoted word consume until whitespace or operator
62
62
  let j = i;
63
63
  while (j < len) {
64
64
  if (line[j] === " " || line[j] === "\t")
@@ -90,7 +90,7 @@ function scanTokens(line) {
90
90
  }
91
91
  /**
92
92
  * Tokenize a command line for syntax highlighting purposes.
93
- * This is a cosmetic tokenizerthe real parsing for execution
93
+ * This is a cosmetic tokenizer the real parsing for execution
94
94
  * lives in src/loop-config.ts parseCommandLine.
95
95
  *
96
96
  * Whitespace is not returned (use highlightSegments for rendering
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text, useStdout } from "ink";
4
4
  import { darkTheme as theme } from "../../shared/ui/theme.js";
@@ -27,15 +27,28 @@ function daemonText(status) {
27
27
  case "error": return "offline";
28
28
  }
29
29
  }
30
+ function ServiceStatus({ label, enabled, online }) {
31
+ // Unknown until the daemon is reachable and reports the setting.
32
+ const known = online && enabled !== undefined;
33
+ const on = known && enabled === true;
34
+ const color = !known
35
+ ? theme.text.muted
36
+ : on
37
+ ? theme.semantic.success
38
+ : theme.semantic.danger;
39
+ const symbol = !known ? "\u25CB" : on ? "\u25CF" : "\u25CB";
40
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: label }), _jsx(Text, { color: color, children: symbol })] }));
41
+ }
30
42
  export function Header(props) {
31
43
  const { stdout } = useStdout();
32
44
  const width = stdout?.columns ?? 80;
33
45
  const compact = width < HEADER_COMPACT_WIDTH;
46
+ const online = props.daemonStatus === "connected";
34
47
  const entries = [
35
48
  { label: t("board.runningLabel"), value: props.counts.running, color: theme.semantic.success },
36
49
  { label: t("board.waitingLabel"), value: props.counts.waiting, color: theme.accent.loop },
37
50
  { label: t("board.pausedLabel"), value: props.counts.paused, color: theme.semantic.warning },
38
51
  { label: t("board.idleLabel"), value: props.counts.idle, color: theme.semantic.idle },
39
52
  ];
40
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: theme.text.muted, children: VERSION }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
53
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: theme.text.muted, children: VERSION }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), _jsx(ServiceStatus, { label: "api", enabled: props.httpApiEnabled, online: online }), _jsx(ServiceStatus, { label: "mcp", enabled: props.mcpApiEnabled, online: online }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
41
54
  }
@@ -9,6 +9,7 @@ const DESC_WIDTH = 32;
9
9
  const SINCE_WIDTH = 13;
10
10
  const RUNS_WIDTH = 4;
11
11
  const SKIPPED_WIDTH = 4;
12
+ const SILENT_WIDTH = 4;
12
13
  const STATUS_WIDTH = 8;
13
14
  const COL_GAP = 1;
14
15
  const LIMIT = 15;
@@ -58,9 +59,9 @@ export function Navigator(props) {
58
59
  : isSelected
59
60
  ? theme.text.inverse
60
61
  : 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
+ 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: fg, children: String(loop.silentChainCount ?? 0).padStart(SILENT_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
  }
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
+ 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.headerSilent").padStart(SILENT_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
65
  const isSelected = i === selectedIndex;
65
66
  const indicator = isSelected ? "\u203a " : " ";
66
67
  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));
@@ -53,8 +53,9 @@ export function TaskNavigator(props) {
53
53
  ? cmd.slice(0, COMMAND_WIDTH - 3) + "..."
54
54
  : cmd.padEnd(COMMAND_WIDTH);
55
55
  const chains = chainsLabel(task);
56
+ const silent = task.silentChain ? " [s]" : "";
56
57
  const fg = isSelected ? theme.text.inverse : theme.text.primary;
57
- return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.activeTask : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: fg, children: name }), _jsx(Text, { color: fg, children: cmdDisplay }), _jsx(Text, { color: fg, children: chains })] }));
58
+ return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.activeTask : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: fg, children: name }), _jsx(Text, { color: fg, children: cmdDisplay }), _jsxs(Text, { color: fg, children: [chains, silent] })] }));
58
59
  }
59
60
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.taskBrowserEmpty") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderName").padEnd(NAME_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderCommand").padEnd(COMMAND_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderChains") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { height: LIMIT, selectedIndex: selectedIndex, scrollAlignment: "auto", children: visible.map((task, i) => (_jsx(React.Fragment, { children: renderTask(task, i) }, task.id))) }) })] }))] }));
60
61
  }
@@ -75,7 +76,7 @@ export function TaskInspector(props) {
75
76
  };
76
77
  const onSuccess = task.onSuccessTaskId ? resolveName(task.onSuccessTaskId) ?? task.onSuccessTaskId : t("board.taskNone");
77
78
  const onFailure = task.onFailureTaskId ? resolveName(task.onFailureTaskId) ?? task.onFailureTaskId : t("board.taskNone");
78
- return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(InspectorField, { label: t("board.fieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(InspectorField, { label: t("board.taskLabelName") + ": ", children: _jsx(Text, { color: theme.text.primary, children: task.name }) }), _jsx(InspectorField, { label: t("board.taskLabelCommand") + ": ", children: _jsx(Text, { color: theme.text.primary, children: cmd }) }), _jsx(InspectorField, { label: t("board.taskLabelOnSuccess") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onSuccess }) }), _jsx(InspectorField, { label: t("board.taskLabelOnFailure") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onFailure }) })] })] }));
79
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(InspectorField, { label: t("board.fieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(InspectorField, { label: t("board.taskLabelName") + ": ", children: _jsx(Text, { color: theme.text.primary, children: task.name }) }), _jsx(InspectorField, { label: t("board.taskLabelCommand") + ": ", children: _jsx(Text, { color: theme.text.primary, children: cmd }) }), _jsx(InspectorField, { label: t("board.taskLabelOnSuccess") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onSuccess }) }), _jsx(InspectorField, { label: t("board.taskLabelOnFailure") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onFailure }) }), _jsx(InspectorField, { label: t("board.taskFieldSilent") + ": ", children: _jsx(Text, { color: task.silentChain ? theme.semantic.warning : theme.text.muted, children: task.silentChain ? t("board.silentChainYes") : t("board.silentChainNo") }) })] })] }));
79
80
  }
80
81
  export function TaskActionButtons(props) {
81
82
  const { task, selectable, onAction } = props;
@@ -62,7 +62,7 @@ export function LogModal(props) {
62
62
  const visible = filtered.slice(startIdx, endIdx);
63
63
  useInput((input, key) => {
64
64
  // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
65
- // before the escape checkthe leading ESC trips key.escape and would
65
+ // before the escape check the leading ESC trips key.escape and would
66
66
  // close the modal before the paste is acknowledged. LogModal doesn't
67
67
  // insert pastes, so just swallow the sequence.
68
68
  if (input.includes("\x1b[200~")) {
@@ -23,7 +23,7 @@ export function WizardForm(props) {
23
23
  result[s.key] = values[s.key] ?? s.defaultValue ?? "";
24
24
  return result;
25
25
  }, [steps, values]);
26
- // Synchronous mirror of resolvedValuesupdated inside setValue so
26
+ // Synchronous mirror of resolvedValues updated inside setValue so
27
27
  // findNextField sees the new value immediately, before React re-renders.
28
28
  // Without this, onChange("Existing task") + onAdvance() run in the same
29
29
  // tick: setValues queues a state update, but findNextField reads stale
@@ -163,7 +163,7 @@ export function WizardForm(props) {
163
163
  setValue(step.key, valueFor(step) + sanitizePaste(input));
164
164
  return;
165
165
  }
166
- // Multi-char containing CR/LF with no bracketed markersignore
166
+ // Multi-char containing CR/LF with no bracketed markers ignore
167
167
  if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
168
168
  return;
169
169
  // Multi-char printable input = unbracketed single-line paste (e.g. right-click)
@@ -57,6 +57,7 @@ export function useHandleComplete(params) {
57
57
  // invalid context, skip
58
58
  }
59
59
  }
60
+ const cwdInput = (values.cwd ?? "").trim();
60
61
  const options = {
61
62
  interval,
62
63
  taskId: isExistingTask
@@ -65,7 +66,9 @@ export function useHandleComplete(params) {
65
66
  command: cmdOnly,
66
67
  commandArgs: args,
67
68
  commandRaw: isExistingTask ? undefined : cmdValue,
68
- cwd: (values.cwd ?? "").trim() || process.cwd(),
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()),
69
72
  immediate: isManual ? false : runNowValue,
70
73
  maxRuns: (values.maxRuns ?? "").trim()
71
74
  ? parseInt(values.maxRuns, 10)
@@ -45,6 +45,14 @@ export function ProjectFormView(props) {
45
45
  inputType: "text",
46
46
  defaultValue: mode === "create" ? process.cwd() : (editProject?.directory || undefined),
47
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
+ },
48
56
  ], [editProject, mode]);
49
57
  const handleComplete = useCallback((values) => {
50
58
  const name = (values.name ?? "").trim();
@@ -52,13 +60,14 @@ export function ProjectFormView(props) {
52
60
  return;
53
61
  const colorKey = values.color ?? "cyan";
54
62
  const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
63
+ const githubSource = values.githubSource?.trim() || undefined;
55
64
  if (mode === "edit" && editProject) {
56
- projectService.update(editProject.id, name, color, values.directory?.trim() || undefined)
65
+ projectService.update(editProject.id, name, color, values.directory?.trim() || undefined, githubSource)
57
66
  .then(() => onDone(true, name))
58
67
  .catch(() => { });
59
68
  }
60
69
  else {
61
- projectService.create(name, color, values.directory?.trim() || undefined)
70
+ projectService.create(name, color, values.directory?.trim() || undefined, githubSource)
62
71
  .then(() => onDone(false, name))
63
72
  .catch(() => { });
64
73
  }
@@ -23,5 +23,5 @@ export function Inspector(props) {
23
23
  const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
24
24
  const fullCmd = truncate(commandLine(loop.command, loop.commandArgs), 38);
25
25
  const desc = truncate(describeLoop(loop), 38);
26
- 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: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
26
+ 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: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), (loop.silentChainCount ?? 0) > 0 ? (_jsx(Field, { label: t("board.fieldSilentChains"), children: _jsx(Text, { color: theme.text.muted, children: t("board.silentChainCount", { count: (loop.silentChainCount ?? 0).toLocaleString() }) }) })) : null, _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
27
27
  }
@@ -21,7 +21,7 @@ function TaskInspector(props) {
21
21
  }
22
22
  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
23
23
  ? task.commandRaw.split("\n").filter(Boolean).join(" ")
24
- : 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 }) })] }));
24
+ : 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(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: DIVIDER }) })] }));
25
25
  }
26
26
  const LABEL_WIDTH = 9;
27
27
  function Field(props) {
@@ -39,7 +39,9 @@ export function TaskForm(props) {
39
39
  const [commandEditorOpen, setCommandEditorOpen] = useState(false);
40
40
  const [contextEditorOpen, setContextEditorOpen] = useState(false);
41
41
  const [contextValue, setContextValue] = useState(editTask?.context ? JSON.stringify(editTask.context) : "");
42
+ const [silentChainValue, setSilentChainValue] = useState(editTask?.silentChain ? t("board.silentChainYes") : "");
42
43
  const [openChainField, setOpenChainField] = useState(null);
44
+ const [openSilentChain, setOpenSilentChain] = useState(false);
43
45
  const chainFieldsRef = useRef({});
44
46
  const resolveChainId = useCallback((val) => {
45
47
  if (!val || val === t("wizard.chainNone"))
@@ -97,6 +99,15 @@ export function TaskForm(props) {
97
99
  return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.onFailurePrompt"), isActive: isActive }));
98
100
  },
99
101
  },
102
+ {
103
+ key: "silentChain",
104
+ prompt: t("wizard.silentChainPrompt"),
105
+ hint: t("wizard.silentChainHint"),
106
+ required: false,
107
+ defaultValue: silentChainValue || undefined,
108
+ onActivate: () => setOpenSilentChain(true),
109
+ renderCustom: ({ value, isActive }) => (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.silentChainPrompt"), isActive: isActive })),
110
+ },
100
111
  {
101
112
  key: "context",
102
113
  prompt: t("wizard.contextPrompt"),
@@ -122,7 +133,7 @@ export function TaskForm(props) {
122
133
  },
123
134
  ];
124
135
  return list;
125
- }, [commandValue, chainOptions, editTask, resolveChainName, contextValue]);
136
+ }, [commandValue, chainOptions, editTask, resolveChainName, contextValue, silentChainValue]);
126
137
  const handleComplete = useCallback((values) => {
127
138
  const name = values.name?.trim() ?? "";
128
139
  const rawCommand = joinCommandLines(commandValue);
@@ -130,6 +141,7 @@ export function TaskForm(props) {
130
141
  return;
131
142
  const onSuccessTaskId = resolveChainId(values.onSuccess ?? "");
132
143
  const onFailureTaskId = resolveChainId(values.onFailure ?? "");
144
+ const silentChain = values.silentChain === t("board.silentChainYes");
133
145
  let context;
134
146
  const contextStr = contextValue?.trim();
135
147
  if (contextStr) {
@@ -176,6 +188,7 @@ export function TaskForm(props) {
176
188
  steps,
177
189
  onSuccessTaskId,
178
190
  onFailureTaskId,
191
+ silentChain: silentChain || undefined,
179
192
  context,
180
193
  };
181
194
  }
@@ -187,6 +200,7 @@ export function TaskForm(props) {
187
200
  commandRaw: commandValue,
188
201
  onSuccessTaskId,
189
202
  onFailureTaskId,
203
+ silentChain: silentChain || undefined,
190
204
  context,
191
205
  };
192
206
  }
@@ -201,11 +215,11 @@ export function TaskForm(props) {
201
215
  .then((result) => onDone(false, result.id))
202
216
  .catch(() => { });
203
217
  }
204
- }, [commandValue, resolveChainId, mode, editTask, onDone, contextValue]);
218
+ }, [commandValue, resolveChainId, mode, editTask, onDone, contextValue, silentChainValue]);
205
219
  const title = mode === "edit"
206
220
  ? t("board.taskEditTitle")
207
221
  : t("board.taskCreateTitle");
208
- return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: openChainField !== null || commandEditorOpen || contextEditorOpen }), openChainField ? (_jsx(SelectModal, { title: openChainField === "onSuccess" ? t("wizard.onSuccessPrompt") : t("wizard.onFailurePrompt"), options: chainSelectOptions, initialValue: chainFieldsRef.current[openChainField]?.value, onSelect: (option) => {
222
+ return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: openChainField !== null || commandEditorOpen || contextEditorOpen || openSilentChain }), openChainField ? (_jsx(SelectModal, { title: openChainField === "onSuccess" ? t("wizard.onSuccessPrompt") : t("wizard.onFailurePrompt"), options: chainSelectOptions, initialValue: chainFieldsRef.current[openChainField]?.value, onSelect: (option) => {
209
223
  chainFieldsRef.current[openChainField]?.onChange(option.value);
210
224
  chainFieldsRef.current[openChainField]?.onAdvance();
211
225
  setOpenChainField(null);
@@ -215,5 +229,11 @@ export function TaskForm(props) {
215
229
  }, onCancel: () => setCommandEditorOpen(false) })) : null, contextEditorOpen ? (_jsx(CodeEditorModal, { initialValue: contextValue, onSave: (v) => {
216
230
  setContextValue(v);
217
231
  setContextEditorOpen(false);
218
- }, onCancel: () => setContextEditorOpen(false) })) : null] }));
232
+ }, onCancel: () => setContextEditorOpen(false) })) : null, openSilentChain ? (_jsx(SelectModal, { title: t("wizard.silentChainPrompt"), options: [
233
+ { value: t("board.silentChainNo"), label: t("board.silentChainNo") },
234
+ { value: t("board.silentChainYes"), label: t("board.silentChainYes") },
235
+ ], initialValue: silentChainValue || t("board.silentChainNo"), onSelect: (option) => {
236
+ setSilentChainValue(option.value === t("board.silentChainNo") ? "" : option.value);
237
+ setOpenSilentChain(false);
238
+ }, onClose: () => setOpenSilentChain(false) })) : null] }));
219
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "2.1.12",
3
+ "version": "2.1.13",
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": {
@@ -48,6 +48,7 @@
48
48
  "node": ">=20.0.0"
49
49
  },
50
50
  "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.29.0",
51
52
  "commander": "^13.1.0",
52
53
  "execa": "^9.6.0",
53
54
  "ink": "^7.1.0",
@@ -59,7 +60,8 @@
59
60
  "inversify": "^8.1.1",
60
61
  "inversify-hooks": "^4.0.0",
61
62
  "ms": "^2.1.3",
62
- "react": "^19.2.7"
63
+ "react": "^19.2.7",
64
+ "zod": "^4.4.3"
63
65
  },
64
66
  "devDependencies": {
65
67
  "@types/ms": "^2.1.0",