loop-task 2.1.4 → 2.1.5
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.
- package/dist/app/App.js +119 -0
- package/dist/{tui → app}/index.js +4 -5
- package/dist/app/providers/index.js +1 -0
- package/dist/cli/import-writer.js +1 -1
- package/dist/cli.js +9 -9
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +2 -164
- package/dist/client/ipc.js +3 -3
- package/dist/client/project-commands.js +159 -0
- package/dist/core/{command-runner.js → command/command-runner.js} +3 -3
- package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
- package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
- package/dist/core/loop/chain-executor.js +67 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +191 -0
- package/dist/core/loop/loop-runner.js +133 -0
- package/dist/core/loop/run-executor.js +90 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/daemon/daemon-log.js +1 -1
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +92 -0
- package/dist/daemon/http/route-loops.js +218 -0
- package/dist/daemon/http/route-misc.js +36 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +53 -0
- package/dist/daemon/http/routes.js +17 -0
- package/dist/daemon/http/server.js +64 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +7 -9
- package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
- package/dist/daemon/managers/loop-entry.js +24 -0
- package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
- package/dist/daemon/managers/loop-options.js +16 -0
- package/dist/daemon/managers/loop-serialization.js +43 -0
- package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
- package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
- package/dist/daemon/server/handlers/index.js +86 -0
- package/dist/daemon/server/handlers/log-handlers.js +130 -0
- package/dist/daemon/server/handlers/loop-handlers.js +54 -0
- package/dist/daemon/server/handlers/project-handlers.js +37 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/index.js +113 -0
- package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
- package/dist/daemon/{state.js → state/index.js} +4 -4
- package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
- package/dist/duration.js +1 -1
- package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
- package/dist/entities/loops/index.js +1 -0
- package/dist/entities/projects/filters.js +69 -0
- package/dist/entities/projects/index.js +1 -0
- package/dist/entities/tasks/filters.js +41 -0
- package/dist/entities/tasks/index.js +1 -0
- package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
- package/dist/features/code-editor/useEditorKeyboard.js +147 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/{tui → features/commands}/commands.js +2 -2
- package/dist/features/commands/useCommandHandlers.js +147 -0
- package/dist/features/commands/useContextualActions.js +75 -0
- package/dist/features/commands/useGlobalShortcuts.js +152 -0
- package/dist/features/forms/FormRouter.js +44 -0
- package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
- package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
- package/dist/features/overlays/OverlayStack.js +9 -0
- package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
- package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
- package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
- package/dist/features/overlays/useOverlayStack.js +51 -0
- package/dist/features/state/useAppState.js +150 -0
- package/dist/logger.js +0 -1
- package/dist/loop-config.js +9 -4
- package/dist/{config → shared/config}/constants.js +1 -1
- package/dist/shared/container/index.js +22 -0
- package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
- package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
- package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
- package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
- package/dist/shared/providers/InversifyProvider.js +6 -0
- package/dist/shared/services/export-service.js +43 -0
- package/dist/shared/services/log-service.js +28 -0
- package/dist/shared/services/loop-service.js +78 -0
- package/dist/shared/services/project-service.js +39 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +7 -0
- package/dist/{tui/components → shared/ui}/Button.js +1 -1
- package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
- package/dist/{tui/components → shared/ui}/Modal.js +1 -1
- package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
- package/dist/{tui/components → shared/ui}/Toast.js +1 -1
- package/dist/shared/ui/state.js +9 -0
- package/dist/{tui → shared}/utils/paste.js +2 -2
- package/dist/{tui → shared}/utils/validation.js +1 -1
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +129 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +68 -0
- package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
- package/dist/{tui/components → widgets/header}/Header.js +3 -3
- package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
- package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
- package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
- package/dist/widgets/loop-form/CreateForm.js +75 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
- package/dist/widgets/loop-form/useCreateSteps.js +107 -0
- package/dist/widgets/loop-form/useHandleComplete.js +74 -0
- package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
- package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
- package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
- package/package.json +2 -1
- package/dist/core/loop-controller.js +0 -511
- package/dist/daemon/http-server.js +0 -599
- package/dist/daemon/server.js +0 -374
- package/dist/tui/App.js +0 -680
- package/dist/tui/components/CodeEditorModal.js +0 -244
- package/dist/tui/components/CommandInput.js +0 -273
- package/dist/tui/components/CreateForm.js +0 -224
- package/dist/tui/daemon.js +0 -142
- package/dist/tui/format.js +0 -111
- /package/dist/{tui/router.js → app/router/index.js} +0 -0
- /package/dist/{tui → app}/types.js +0 -0
- /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
- /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
- /package/dist/core/{template.js → context/template.js} +0 -0
- /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
- /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
- /package/dist/{ipc → daemon/ipc}/send.js +0 -0
- /package/dist/{config → shared/config}/paths.js +0 -0
- /package/dist/{i18n → shared/i18n}/en.json +0 -0
- /package/dist/{i18n → shared/i18n}/index.js +0 -0
- /package/dist/{board → shared/ui}/format.js +0 -0
- /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
- /package/dist/{tui → shared/ui}/theme.js +0 -0
- /package/dist/{tui → shared}/utils/syntax.js +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { getSocketPath, removeSocketFile } from "../state/index.js";
|
|
3
|
+
import { t } from "../../shared/i18n/index.js";
|
|
4
|
+
import { send } from "../ipc/send.js";
|
|
5
|
+
import { dispatch } from "./handlers/index.js";
|
|
6
|
+
export class IpcServer {
|
|
7
|
+
constructor(manager, taskManager) {
|
|
8
|
+
this.clients = new Set();
|
|
9
|
+
this.subscribers = new Set();
|
|
10
|
+
this.manager = manager;
|
|
11
|
+
this.taskManager = taskManager;
|
|
12
|
+
this.socketPath = getSocketPath();
|
|
13
|
+
this.server = net.createServer((socket) => this.handleConnection(socket));
|
|
14
|
+
}
|
|
15
|
+
async listen() {
|
|
16
|
+
removeSocketFile();
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
this.server.on("error", reject);
|
|
19
|
+
this.server.listen(this.socketPath, () => {
|
|
20
|
+
this.server.removeListener("error", reject);
|
|
21
|
+
resolve();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async close() {
|
|
26
|
+
for (const client of this.clients) {
|
|
27
|
+
client.destroy();
|
|
28
|
+
}
|
|
29
|
+
this.clients.clear();
|
|
30
|
+
this.subscribers.clear();
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
this.server.close(() => {
|
|
33
|
+
removeSocketFile();
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
pushEvent(event, data) {
|
|
39
|
+
for (const socket of this.subscribers) {
|
|
40
|
+
try {
|
|
41
|
+
send(socket, { type: "event", event, data });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
this.subscribers.delete(socket);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
handleConnection(socket) {
|
|
49
|
+
this.clients.add(socket);
|
|
50
|
+
socket.on("close", () => {
|
|
51
|
+
this.clients.delete(socket);
|
|
52
|
+
this.subscribers.delete(socket);
|
|
53
|
+
});
|
|
54
|
+
let buffer = "";
|
|
55
|
+
socket.on("data", (chunk) => {
|
|
56
|
+
buffer += chunk.toString();
|
|
57
|
+
const lines = buffer.split("\n");
|
|
58
|
+
buffer = lines.pop() ?? "";
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (line.trim()) {
|
|
61
|
+
try {
|
|
62
|
+
const request = JSON.parse(line);
|
|
63
|
+
this.handleRequest(request, socket);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
send(socket, { type: "error", message: t("errors.invalidJson") });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
socket.on("close", () => {
|
|
72
|
+
this.clients.delete(socket);
|
|
73
|
+
});
|
|
74
|
+
socket.on("error", () => {
|
|
75
|
+
this.clients.delete(socket);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
respondOk(socket, ok, id, data) {
|
|
79
|
+
if (!ok) {
|
|
80
|
+
send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
|
|
81
|
+
}
|
|
82
|
+
else if (data !== undefined) {
|
|
83
|
+
send(socket, { type: "ok", data });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
send(socket, { type: "ok" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async handleRequest(request, socket) {
|
|
90
|
+
// Dispatch to extracted handlers
|
|
91
|
+
const handled = await dispatch(request, socket, {
|
|
92
|
+
manager: this.manager,
|
|
93
|
+
taskManager: this.taskManager,
|
|
94
|
+
respondOk: (s, ok, id, data) => this.respondOk(s, ok, id, data),
|
|
95
|
+
});
|
|
96
|
+
if (!handled) {
|
|
97
|
+
// Handle non-dispatchable cases inline
|
|
98
|
+
switch (request.type) {
|
|
99
|
+
case "subscribe": {
|
|
100
|
+
this.subscribers.add(socket);
|
|
101
|
+
send(socket, { type: "ok" });
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "shutdown": {
|
|
105
|
+
send(socket, { type: "ok" });
|
|
106
|
+
await this.manager.shutdown();
|
|
107
|
+
await this.close();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { readDaemonPid, isDaemonAlive, removeDaemonPid, removeDaemonSignature, readDaemonSignature, computeCodeSignature, getSocketPath, } from "
|
|
5
|
-
import { t } from "
|
|
6
|
-
import { DAEMON_POLL_MS, DAEMON_SPAWN_TIMEOUT_MS } from "
|
|
4
|
+
import { readDaemonPid, isDaemonAlive, removeDaemonPid, removeDaemonSignature, readDaemonSignature, computeCodeSignature, getSocketPath, } from "../state/index.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { DAEMON_POLL_MS, DAEMON_SPAWN_TIMEOUT_MS } from "../../shared/config/constants.js";
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
9
9
|
let cachedSignature = null;
|
|
@@ -54,7 +54,7 @@ export function ensureDaemon() {
|
|
|
54
54
|
removeDaemonPid();
|
|
55
55
|
removeDaemonSignature();
|
|
56
56
|
}
|
|
57
|
-
const daemonScript = path.resolve(__dirname, __filename.endsWith(".ts") ? "index.ts" : "index.js");
|
|
57
|
+
const daemonScript = path.resolve(__dirname, __filename.endsWith(".ts") ? "../index.ts" : "../index.js");
|
|
58
58
|
const args = __filename.endsWith(".ts")
|
|
59
59
|
? process.execPath.includes("bun")
|
|
60
60
|
? [daemonScript]
|
|
@@ -2,9 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { removeIfExists, writeFileAtomic } from "
|
|
6
|
-
import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "
|
|
7
|
-
export { getDataDir, getPidFile, getSocketPath } from "
|
|
5
|
+
import { removeIfExists, writeFileAtomic } from "../../shared/fs-utils.js";
|
|
6
|
+
import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "../../shared/config/paths.js";
|
|
7
|
+
export { getDataDir, getPidFile, getSocketPath } from "../../shared/config/paths.js";
|
|
8
8
|
function ensureDirs() {
|
|
9
9
|
fs.mkdirSync(getDataDir(), { recursive: true });
|
|
10
10
|
fs.mkdirSync(getLogsDir(), { recursive: true });
|
|
@@ -127,7 +127,7 @@ export function getLogPath(id) {
|
|
|
127
127
|
return logFile(id);
|
|
128
128
|
}
|
|
129
129
|
export function computeCodeSignature() {
|
|
130
|
-
const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "
|
|
130
|
+
const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
131
131
|
let maxMtimeMs = 0;
|
|
132
132
|
let totalSize = 0;
|
|
133
133
|
let count = 0;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { loopsJson, tasksJson, projectsJson } from "
|
|
4
|
+
import { loopsJson, tasksJson, projectsJson } from "../../shared/config/paths.js";
|
|
5
5
|
const DEBOUNCE_MS = 300;
|
|
6
6
|
const MTIME_POLL_MS = 2000;
|
|
7
7
|
export class FileWatcher {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
constructor() {
|
|
9
|
+
this.watchers = new Map();
|
|
10
|
+
this.mtimeTimers = new Map();
|
|
11
|
+
this.loopManager = null;
|
|
12
|
+
this.taskManager = null;
|
|
13
|
+
this.projectManager = null;
|
|
14
|
+
}
|
|
13
15
|
setManagers(loopManager, taskManager, projectManager) {
|
|
14
16
|
this.loopManager = loopManager;
|
|
15
17
|
this.taskManager = taskManager;
|
package/dist/duration.js
CHANGED
|
@@ -97,79 +97,3 @@ const STATUS_CYCLE = {
|
|
|
97
97
|
export function cycleStatusFilter(status) {
|
|
98
98
|
return STATUS_CYCLE[status];
|
|
99
99
|
}
|
|
100
|
-
export const defaultProjectFilters = {
|
|
101
|
-
query: "",
|
|
102
|
-
hasLoops: "all",
|
|
103
|
-
isSystem: "all",
|
|
104
|
-
sort: "name",
|
|
105
|
-
};
|
|
106
|
-
export function applyProjectFilters(projects, loops, filters) {
|
|
107
|
-
const filtered = projects.filter((project) => {
|
|
108
|
-
if (filters.hasLoops === "with-loops") {
|
|
109
|
-
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
110
|
-
if (count === 0)
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
if (filters.hasLoops === "empty") {
|
|
114
|
-
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
115
|
-
if (count > 0)
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
if (filters.isSystem === "system" && !project.isSystem)
|
|
119
|
-
return false;
|
|
120
|
-
if (filters.isSystem === "user" && project.isSystem)
|
|
121
|
-
return false;
|
|
122
|
-
const query = filters.query.trim().toLowerCase();
|
|
123
|
-
if (query && !project.name.toLowerCase().includes(query))
|
|
124
|
-
return false;
|
|
125
|
-
return true;
|
|
126
|
-
});
|
|
127
|
-
return filtered.sort((a, b) => {
|
|
128
|
-
if (filters.sort === "name") {
|
|
129
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
130
|
-
}
|
|
131
|
-
if (filters.sort === "loop-count") {
|
|
132
|
-
const ca = loops.filter((l) => (l.projectId ?? "default") === a.id).length;
|
|
133
|
-
const cb = loops.filter((l) => (l.projectId ?? "default") === b.id).length;
|
|
134
|
-
if (cb !== ca)
|
|
135
|
-
return cb - ca;
|
|
136
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
137
|
-
}
|
|
138
|
-
const da = a.createdAt;
|
|
139
|
-
const db = b.createdAt;
|
|
140
|
-
if (da !== db)
|
|
141
|
-
return db.localeCompare(da);
|
|
142
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
const PROJECT_SORT_CYCLE = {
|
|
146
|
-
name: "loop-count",
|
|
147
|
-
"loop-count": "created-date",
|
|
148
|
-
"created-date": "name",
|
|
149
|
-
};
|
|
150
|
-
export function cycleProjectSortMode(mode) {
|
|
151
|
-
return PROJECT_SORT_CYCLE[mode];
|
|
152
|
-
}
|
|
153
|
-
const PROJECT_HAS_LOOPS_CYCLE = {
|
|
154
|
-
all: "with-loops",
|
|
155
|
-
"with-loops": "empty",
|
|
156
|
-
empty: "all",
|
|
157
|
-
};
|
|
158
|
-
export function cycleProjectHasLoopsFilter(filter) {
|
|
159
|
-
return PROJECT_HAS_LOOPS_CYCLE[filter];
|
|
160
|
-
}
|
|
161
|
-
const PROJECT_IS_SYSTEM_CYCLE = {
|
|
162
|
-
all: "system",
|
|
163
|
-
system: "user",
|
|
164
|
-
user: "all",
|
|
165
|
-
};
|
|
166
|
-
export function cycleProjectIsSystemFilter(filter) {
|
|
167
|
-
return PROJECT_IS_SYSTEM_CYCLE[filter];
|
|
168
|
-
}
|
|
169
|
-
export function resolveInputOwner(state) {
|
|
170
|
-
if (state.modalOpen)
|
|
171
|
-
return "modal";
|
|
172
|
-
if (state.commandBarHasText || state.commandBarDropdownOpen)
|
|
173
|
-
return "commandBar";
|
|
174
|
-
return "panel";
|
|
175
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultFilters, applyLoopFilters, cycleSortMode, cycleStatusFilter, } from "./filters.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const defaultProjectFilters = {
|
|
2
|
+
query: "",
|
|
3
|
+
hasLoops: "all",
|
|
4
|
+
isSystem: "all",
|
|
5
|
+
sort: "name",
|
|
6
|
+
};
|
|
7
|
+
export function applyProjectFilters(projects, loops, filters) {
|
|
8
|
+
const filtered = projects.filter((project) => {
|
|
9
|
+
if (filters.hasLoops === "with-loops") {
|
|
10
|
+
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
11
|
+
if (count === 0)
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (filters.hasLoops === "empty") {
|
|
15
|
+
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
16
|
+
if (count > 0)
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (filters.isSystem === "system" && !project.isSystem)
|
|
20
|
+
return false;
|
|
21
|
+
if (filters.isSystem === "user" && project.isSystem)
|
|
22
|
+
return false;
|
|
23
|
+
const query = filters.query.trim().toLowerCase();
|
|
24
|
+
if (query && !project.name.toLowerCase().includes(query))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
return filtered.sort((a, b) => {
|
|
29
|
+
if (filters.sort === "name") {
|
|
30
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
31
|
+
}
|
|
32
|
+
if (filters.sort === "loop-count") {
|
|
33
|
+
const ca = loops.filter((l) => (l.projectId ?? "default") === a.id).length;
|
|
34
|
+
const cb = loops.filter((l) => (l.projectId ?? "default") === b.id).length;
|
|
35
|
+
if (cb !== ca)
|
|
36
|
+
return cb - ca;
|
|
37
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
38
|
+
}
|
|
39
|
+
const da = a.createdAt;
|
|
40
|
+
const db = b.createdAt;
|
|
41
|
+
if (da !== db)
|
|
42
|
+
return db.localeCompare(da);
|
|
43
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const PROJECT_SORT_CYCLE = {
|
|
47
|
+
name: "loop-count",
|
|
48
|
+
"loop-count": "created-date",
|
|
49
|
+
"created-date": "name",
|
|
50
|
+
};
|
|
51
|
+
export function cycleProjectSortMode(mode) {
|
|
52
|
+
return PROJECT_SORT_CYCLE[mode];
|
|
53
|
+
}
|
|
54
|
+
const PROJECT_HAS_LOOPS_CYCLE = {
|
|
55
|
+
all: "with-loops",
|
|
56
|
+
"with-loops": "empty",
|
|
57
|
+
empty: "all",
|
|
58
|
+
};
|
|
59
|
+
export function cycleProjectHasLoopsFilter(filter) {
|
|
60
|
+
return PROJECT_HAS_LOOPS_CYCLE[filter];
|
|
61
|
+
}
|
|
62
|
+
const PROJECT_IS_SYSTEM_CYCLE = {
|
|
63
|
+
all: "system",
|
|
64
|
+
system: "user",
|
|
65
|
+
user: "all",
|
|
66
|
+
};
|
|
67
|
+
export function cycleProjectIsSystemFilter(filter) {
|
|
68
|
+
return PROJECT_IS_SYSTEM_CYCLE[filter];
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultProjectFilters, applyProjectFilters, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter, } from "./filters.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const defaultTaskFilters = {
|
|
2
|
+
query: "",
|
|
3
|
+
sort: "name",
|
|
4
|
+
};
|
|
5
|
+
function taskSearchableText(task) {
|
|
6
|
+
return `${task.id} ${task.name} ${task.command} ${task.commandArgs.join(" ")}`.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
function matches(task, filters) {
|
|
9
|
+
const query = filters.query.trim().toLowerCase();
|
|
10
|
+
if (!query)
|
|
11
|
+
return true;
|
|
12
|
+
return taskSearchableText(task).includes(query);
|
|
13
|
+
}
|
|
14
|
+
function compare(left, right, sort) {
|
|
15
|
+
if (sort === "name") {
|
|
16
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
17
|
+
}
|
|
18
|
+
if (sort === "command") {
|
|
19
|
+
const leftCmd = left.commandRaw ?? [left.command, ...left.commandArgs].join(" ");
|
|
20
|
+
const rightCmd = right.commandRaw ?? [right.command, ...right.commandArgs].join(" ");
|
|
21
|
+
const byCmd = leftCmd.localeCompare(rightCmd, undefined, { sensitivity: "base" });
|
|
22
|
+
if (byCmd !== 0)
|
|
23
|
+
return byCmd;
|
|
24
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
25
|
+
}
|
|
26
|
+
// "created" — newest first
|
|
27
|
+
return right.createdAt.localeCompare(left.createdAt);
|
|
28
|
+
}
|
|
29
|
+
export function applyTaskFilters(tasks, filters) {
|
|
30
|
+
return tasks
|
|
31
|
+
.filter((task) => matches(task, filters))
|
|
32
|
+
.sort((left, right) => compare(left, right, filters.sort));
|
|
33
|
+
}
|
|
34
|
+
const TASK_SORT_CYCLE = {
|
|
35
|
+
name: "command",
|
|
36
|
+
command: "created",
|
|
37
|
+
created: "name",
|
|
38
|
+
};
|
|
39
|
+
export function cycleTaskSortMode(mode) {
|
|
40
|
+
return TASK_SORT_CYCLE[mode];
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultTaskFilters, applyTaskFilters, cycleTaskSortMode, } from "./filters.js";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useMemo, useEffect } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
6
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
7
|
+
import { commandLine } from "../../shared/ui/format.js";
|
|
7
8
|
function buildChains(tasks) {
|
|
8
9
|
const taskMap = new Map();
|
|
9
10
|
for (const task of tasks) {
|
|
@@ -58,9 +59,10 @@ function flattenTree(node, result = [], prefix = "") {
|
|
|
58
59
|
export function ChainEditor(props) {
|
|
59
60
|
const [tasks, setTasks] = useState([]);
|
|
60
61
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
62
|
+
const taskService = useInject(TYPES.TaskService);
|
|
61
63
|
useEffect(() => {
|
|
62
|
-
|
|
63
|
-
}, []);
|
|
64
|
+
taskService.list().then(setTasks).catch(() => setTasks([]));
|
|
65
|
+
}, [taskService]);
|
|
64
66
|
const trees = useMemo(() => buildChains(tasks), [tasks]);
|
|
65
67
|
const flatNodes = useMemo(() => {
|
|
66
68
|
const all = [];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { useUndoRedo } from "../../shared/hooks/useUndoRedo.js";
|
|
7
|
+
import { highlightSegments } from "../../shared/utils/syntax.js";
|
|
8
|
+
import { joinCommandLines } from "../../loop-config.js";
|
|
9
|
+
import { CODE_EDITOR_UNDO_LIMIT, CODE_EDITOR_SYNTAX_COLORS } from "../../shared/config/constants.js";
|
|
10
|
+
import { useModalDimensions } from "./useModalDimensions.js";
|
|
11
|
+
import { useEditorKeyboard } from "./useEditorKeyboard.js";
|
|
12
|
+
export function CodeEditorModal(props) {
|
|
13
|
+
const { initialValue, onSave, onCancel } = props;
|
|
14
|
+
const { modalWidth, modalHeight, editorVisibleLines: EDITOR_VISIBLE_LINES } = useModalDimensions();
|
|
15
|
+
const { value, setValue, undo, redo } = useUndoRedo(initialValue, CODE_EDITOR_UNDO_LIMIT);
|
|
16
|
+
const [cursorRow, setCursorRow] = useState(() => {
|
|
17
|
+
const lines = initialValue.split("\n");
|
|
18
|
+
return Math.max(0, lines.length - 1);
|
|
19
|
+
});
|
|
20
|
+
const [cursorCol, setCursorCol] = useState(() => {
|
|
21
|
+
const lines = initialValue.split("\n");
|
|
22
|
+
return (lines[lines.length - 1] ?? "").length;
|
|
23
|
+
});
|
|
24
|
+
const [flashMsg, setFlashMsg] = useState(null);
|
|
25
|
+
const lines = useMemo(() => (value ? value.split("\n") : [""]), [value]);
|
|
26
|
+
const lineCount = lines.length;
|
|
27
|
+
const lineNumWidth = String(lineCount).length;
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (flashMsg) {
|
|
30
|
+
const timer = setTimeout(() => setFlashMsg(null), 1500);
|
|
31
|
+
return () => clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}, [flashMsg]);
|
|
34
|
+
const scrollStart = useMemo(() => {
|
|
35
|
+
if (cursorRow < EDITOR_VISIBLE_LINES)
|
|
36
|
+
return 0;
|
|
37
|
+
return cursorRow - EDITOR_VISIBLE_LINES + 1;
|
|
38
|
+
}, [cursorRow]);
|
|
39
|
+
const visibleLines = useMemo(() => lines.slice(scrollStart, scrollStart + EDITOR_VISIBLE_LINES), [lines, scrollStart]);
|
|
40
|
+
const applyMutation = useCallback((nextLines, newCursorRow, newCursorCol) => {
|
|
41
|
+
setValue(nextLines.join("\n"));
|
|
42
|
+
setCursorRow(newCursorRow);
|
|
43
|
+
setCursorCol(newCursorCol);
|
|
44
|
+
}, [setValue]);
|
|
45
|
+
useEditorKeyboard({
|
|
46
|
+
lines,
|
|
47
|
+
cursorRow,
|
|
48
|
+
cursorCol,
|
|
49
|
+
value,
|
|
50
|
+
setValue,
|
|
51
|
+
setCursorRow,
|
|
52
|
+
setCursorCol,
|
|
53
|
+
undo,
|
|
54
|
+
redo,
|
|
55
|
+
onSave,
|
|
56
|
+
onCancel,
|
|
57
|
+
setFlashMsg,
|
|
58
|
+
applyMutation,
|
|
59
|
+
});
|
|
60
|
+
const joined = joinCommandLines(value);
|
|
61
|
+
const innerWidth = modalWidth - 2;
|
|
62
|
+
const truncatedPreview = joined.length > innerWidth
|
|
63
|
+
? joined.slice(0, innerWidth - 1) + t("codeEditor.previewTruncated")
|
|
64
|
+
: joined;
|
|
65
|
+
const accent = theme.accent.brand;
|
|
66
|
+
return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: modalWidth, height: modalHeight, flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: accent, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: accent, bold: true, children: t("codeEditor.title") }), _jsxs(Text, { color: theme.text.muted, children: [lineCount, " ", t("cmdEditor.lines")] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleLines.map((line, visIdx) => {
|
|
67
|
+
const rowIdx = scrollStart + visIdx;
|
|
68
|
+
const isCursor = rowIdx === cursorRow;
|
|
69
|
+
const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
|
|
70
|
+
if (isCursor) {
|
|
71
|
+
const before = line.slice(0, cursorCol);
|
|
72
|
+
const cur = cursorCol < line.length ? line[cursorCol] : " ";
|
|
73
|
+
const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
|
|
74
|
+
// Cursor line: render with cursor (no syntax highlight on cursor line)
|
|
75
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.primary, children: before }), _jsx(Text, { inverse: true, children: cur }), _jsx(Text, { color: theme.text.primary, children: after })] }, rowIdx));
|
|
76
|
+
}
|
|
77
|
+
// Non-cursor line: syntax highlighted (whitespace preserved)
|
|
78
|
+
const segments = highlightSegments(line, CODE_EDITOR_SYNTAX_COLORS, theme.text.muted);
|
|
79
|
+
if (segments.length === 0) {
|
|
80
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { children: " " })] }, rowIdx));
|
|
81
|
+
}
|
|
82
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), segments.map((seg, ti) => (_jsx(Text, { color: seg.color, children: seg.value }, ti)))] }, rowIdx));
|
|
83
|
+
}) }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [t("codeEditor.previewLabel"), " "] }), _jsx(Text, { color: theme.text.secondary, children: truncatedPreview || t("codeEditor.emptyPlaceholder") })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonCopy") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+x" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonPaste") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+v" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonClear") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+l" })] }), flashMsg ? (_jsx(Text, { color: theme.semantic.success, children: flashMsg })) : null] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("codeEditor.hint") }) })] }) }));
|
|
84
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
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 } from "
|
|
4
|
-
import { t } from "../../i18n/index.js";
|
|
5
|
-
import { CODE_EDITOR_MAX_VISIBLE } from "../../config/constants.js";
|
|
3
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
4
|
+
import { t } from "../../shared/i18n/index.js";
|
|
5
|
+
import { CODE_EDITOR_MAX_VISIBLE } from "../../shared/config/constants.js";
|
|
6
6
|
export function CodeEditorPreview(props) {
|
|
7
7
|
const { value, hint, isActive, onActivate } = props;
|
|
8
8
|
const lines = value ? value.split("\n") : [];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useInput } from "ink";
|
|
2
|
+
import { t } from "../../shared/i18n/index.js";
|
|
3
|
+
import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
|
|
4
|
+
import { sanitizeMultilinePaste, wrapCommand } from "../../shared/utils/paste.js";
|
|
5
|
+
export function useEditorKeyboard(params) {
|
|
6
|
+
const { lines, cursorRow, cursorCol, value, setValue, setCursorRow, setCursorCol, undo, redo, onSave, onCancel, setFlashMsg, applyMutation, } = params;
|
|
7
|
+
useInput((input, key) => {
|
|
8
|
+
if (input.includes("\x1b[200~")) {
|
|
9
|
+
const pasted = wrapCommand(sanitizeMultilinePaste(input));
|
|
10
|
+
if (pasted) {
|
|
11
|
+
const next = [...lines];
|
|
12
|
+
const line = next[cursorRow] ?? "";
|
|
13
|
+
next[cursorRow] =
|
|
14
|
+
line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
15
|
+
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (key.escape) {
|
|
20
|
+
onCancel();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if ((key.ctrl && input === "s") || input === "\x13") {
|
|
24
|
+
onSave(value);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if ((key.ctrl && !key.shift && input === "z") || input === "\x1a") {
|
|
28
|
+
undo();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (key.ctrl && key.shift && input === "z") {
|
|
32
|
+
redo();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if ((key.ctrl && input === "x") || input === "\x18") {
|
|
36
|
+
copyToClipboard(value);
|
|
37
|
+
setValue("");
|
|
38
|
+
setCursorRow(0);
|
|
39
|
+
setCursorCol(0);
|
|
40
|
+
setFlashMsg(t("codeEditor.copied"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if ((key.ctrl && input === "v") || input === "\x16") {
|
|
44
|
+
const clip = readFromClipboard();
|
|
45
|
+
if (clip) {
|
|
46
|
+
const pasted = sanitizeMultilinePaste(clip);
|
|
47
|
+
if (pasted) {
|
|
48
|
+
const next = [...lines];
|
|
49
|
+
const line = next[cursorRow] ?? "";
|
|
50
|
+
next[cursorRow] =
|
|
51
|
+
line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
52
|
+
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setFlashMsg(t("codeEditor.clipboardEmpty"));
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if ((key.ctrl && input === "l") || input === "\x0c") {
|
|
61
|
+
setValue("");
|
|
62
|
+
setCursorRow(0);
|
|
63
|
+
setCursorCol(0);
|
|
64
|
+
setFlashMsg(t("codeEditor.cleared"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.return) {
|
|
68
|
+
const next = [...lines];
|
|
69
|
+
const line = next[cursorRow] ?? "";
|
|
70
|
+
next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
|
|
71
|
+
applyMutation(next, cursorRow + 1, 0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.upArrow) {
|
|
75
|
+
if (cursorRow > 0) {
|
|
76
|
+
const newRow = cursorRow - 1;
|
|
77
|
+
setCursorRow(newRow);
|
|
78
|
+
setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.downArrow) {
|
|
83
|
+
if (cursorRow < lines.length - 1) {
|
|
84
|
+
const newRow = cursorRow + 1;
|
|
85
|
+
setCursorRow(newRow);
|
|
86
|
+
setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (key.leftArrow) {
|
|
91
|
+
if (cursorCol > 0) {
|
|
92
|
+
setCursorCol((c) => c - 1);
|
|
93
|
+
}
|
|
94
|
+
else if (cursorRow > 0) {
|
|
95
|
+
setCursorRow((r) => r - 1);
|
|
96
|
+
setCursorCol((lines[cursorRow - 1] ?? "").length);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (key.rightArrow) {
|
|
101
|
+
const curLen = (lines[cursorRow] ?? "").length;
|
|
102
|
+
if (cursorCol < curLen) {
|
|
103
|
+
setCursorCol((c) => c + 1);
|
|
104
|
+
}
|
|
105
|
+
else if (cursorRow < lines.length - 1) {
|
|
106
|
+
setCursorRow((r) => r + 1);
|
|
107
|
+
setCursorCol(0);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (key.backspace || key.delete) {
|
|
112
|
+
if (cursorCol > 0) {
|
|
113
|
+
const next = [...lines];
|
|
114
|
+
const line = next[cursorRow] ?? "";
|
|
115
|
+
next[cursorRow] =
|
|
116
|
+
line.slice(0, cursorCol - 1) + line.slice(cursorCol);
|
|
117
|
+
applyMutation(next, cursorRow, cursorCol - 1);
|
|
118
|
+
}
|
|
119
|
+
else if (cursorRow > 0) {
|
|
120
|
+
const next = [...lines];
|
|
121
|
+
const prev = next[cursorRow - 1] ?? "";
|
|
122
|
+
const cur = next[cursorRow] ?? "";
|
|
123
|
+
next.splice(cursorRow - 1, 2, prev + cur);
|
|
124
|
+
applyMutation(next, cursorRow - 1, prev.length);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (input.length > 1 && !key.meta) {
|
|
129
|
+
const pasted = wrapCommand(sanitizeMultilinePaste(input));
|
|
130
|
+
if (pasted) {
|
|
131
|
+
const next = [...lines];
|
|
132
|
+
const line = next[cursorRow] ?? "";
|
|
133
|
+
next[cursorRow] =
|
|
134
|
+
line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
135
|
+
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
140
|
+
const next = [...lines];
|
|
141
|
+
const line = next[cursorRow] ?? "";
|
|
142
|
+
next[cursorRow] =
|
|
143
|
+
line.slice(0, cursorCol) + input + line.slice(cursorCol);
|
|
144
|
+
applyMutation(next, cursorRow, cursorCol + 1);
|
|
145
|
+
}
|
|
146
|
+
}, { isActive: true });
|
|
147
|
+
}
|