@rse/ase 1.0.6 → 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.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import { measureElement } from "ink";
|
|
7
|
+
import { reachableStates, rowLanes } from "./ase-task-board-core.js";
|
|
8
|
+
import { nearestPlace } from "./ase-task-board-graph.js";
|
|
9
|
+
import { groupItems, relocate, requestChoices, assignChoices } from "./ase-task-board-tui-model.js";
|
|
10
|
+
import { gridColumns } from "./ase-task-board-tui-view.js";
|
|
11
|
+
import { refSegs, tabFirst, tabLayout, confirmBox, transferBox, assignBox, promptBox, promptIgnored, promptNext, dropFirst, CONFIRM_BUTTON, DIALOG_CHROME, DIALOG_CLOSE, DROP_ROWS } from "./ase-task-board-tui-popup.js";
|
|
12
|
+
/* handle a mouse press onto the deletion confirmation, the transfer popup, the assign
|
|
13
|
+
popup, the prompt popup, or the read dialog, returning whether one of them was open */
|
|
14
|
+
const handlePopupMouse = (ctx, btn, mx, my) => {
|
|
15
|
+
const { columns, rows, all, dialog, setDialog, setNotice, confirm, setConfirm, transfer, setTransfer, assign, setAssign, assignTo, prompt, setPrompt, cancelPrompt, submitPrompt, dialogW, tabLabels, dialogSel, dialogScroll, transferList, remove, transferTo } = ctx;
|
|
16
|
+
if (prompt !== null) {
|
|
17
|
+
/* a click onto a suggested request name of the drop-down picks it (and switches to the
|
|
18
|
+
prompt, resp. to the request button if the prompt is ignored), a click onto the request
|
|
19
|
+
or cancel button presses it, and a click onto the input box of the request name or the
|
|
20
|
+
prompt (unless ignored) focuses it */
|
|
21
|
+
const box = promptBox(columns, rows, prompt.request !== null);
|
|
22
|
+
const choices = prompt.request !== null && prompt.focus === "name" ? requestChoices(prompt.request) : [];
|
|
23
|
+
const k = dropFirst(choices, prompt.request?.name ?? "") + my - box.dropY - 1;
|
|
24
|
+
const inBox = mx >= box.boxX && mx < box.boxX + box.boxW;
|
|
25
|
+
const inName = mx >= box.boxX && mx < box.boxX + box.nameW;
|
|
26
|
+
if (btn !== 0)
|
|
27
|
+
return true;
|
|
28
|
+
if (prompt.request !== null && inName && my > box.dropY && my <= box.dropY + Math.min(DROP_ROWS, choices.length)) {
|
|
29
|
+
const picked = { ...prompt, request: { ...prompt.request, name: choices[k] } };
|
|
30
|
+
setPrompt({ ...picked, focus: promptNext(picked) });
|
|
31
|
+
}
|
|
32
|
+
else if (my === box.buttonY && mx >= box.okX && mx < box.okX + box.okText.length)
|
|
33
|
+
submitPrompt();
|
|
34
|
+
else if (my === box.buttonY && mx >= box.noX && mx < box.noX + box.noText.length)
|
|
35
|
+
cancelPrompt();
|
|
36
|
+
else if (prompt.request !== null && inName && my >= box.nameY - 1 && my <= box.nameY + 1)
|
|
37
|
+
setPrompt({ ...prompt, focus: "name" });
|
|
38
|
+
else if (inBox && my >= box.promptY && my <= box.promptY + box.viewH + 1 && !promptIgnored(prompt))
|
|
39
|
+
setPrompt({ ...prompt, focus: "prompt" });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (confirm !== null) {
|
|
43
|
+
/* a click onto the " delete " or " cancel " button of the deletion confirmation */
|
|
44
|
+
const box = confirmBox(columns, rows);
|
|
45
|
+
if (btn === 0 && my === box.row && mx >= box.deleteX && mx < box.deleteX + CONFIRM_BUTTON) {
|
|
46
|
+
remove(confirm.id);
|
|
47
|
+
setConfirm(null);
|
|
48
|
+
}
|
|
49
|
+
else if (btn === 0 && my === box.row && mx >= box.cancelX && mx < box.cancelX + CONFIRM_BUTTON) {
|
|
50
|
+
setNotice(`deleting task "${confirm.id}" cancelled`);
|
|
51
|
+
setConfirm(null);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (assign !== null) {
|
|
56
|
+
/* a click onto a suggested assignee of the drop-down assigns the task to it,
|
|
57
|
+
while a click outside the popup cancels */
|
|
58
|
+
const box = assignBox(columns, rows);
|
|
59
|
+
const choices = assignChoices(assign);
|
|
60
|
+
const k = dropFirst(choices.map((choice) => choice.name), assign.name) + my - box.dropY - 1;
|
|
61
|
+
if (btn !== 0)
|
|
62
|
+
return true;
|
|
63
|
+
if (mx < box.left || mx >= box.left + box.width || my < box.top || my >= box.top + box.height) {
|
|
64
|
+
setNotice(`assigning task "${assign.id}" cancelled`);
|
|
65
|
+
setAssign(null);
|
|
66
|
+
}
|
|
67
|
+
else if (mx >= box.nameX && mx < box.nameX + box.nameW
|
|
68
|
+
&& my > box.dropY && my <= box.dropY + Math.min(DROP_ROWS, choices.length))
|
|
69
|
+
assignTo(assign.id, choices[k].name);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (transfer !== null) {
|
|
73
|
+
/* a click onto a selectable entry of the transfer popup transfers
|
|
74
|
+
the task to its state, while a click outside the popup cancels */
|
|
75
|
+
const box = transferBox(transferList.length, transferList.findIndex((e) => e.status === transfer.at), columns, rows);
|
|
76
|
+
const k = box.first + my - box.row;
|
|
77
|
+
if (btn !== 0)
|
|
78
|
+
return true;
|
|
79
|
+
if (mx < box.left || mx >= box.left + box.width || my < box.top || my >= box.top + box.height) {
|
|
80
|
+
setNotice(`transferring task "${transfer.id}" cancelled`);
|
|
81
|
+
setTransfer(null);
|
|
82
|
+
}
|
|
83
|
+
else if (k >= box.first && k < box.first + box.viewH && transferList[k]?.ok)
|
|
84
|
+
transferTo(transfer.id, transferList[k].status);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (dialog !== null) {
|
|
88
|
+
/* a click onto the " X " of the header (the second dialog row) closes */
|
|
89
|
+
const barX = Math.floor((columns - dialogW) / 2) + 1;
|
|
90
|
+
const closeX = barX - 1 + dialogW - DIALOG_CLOSE;
|
|
91
|
+
if (btn === 0 && my === 1 && mx >= closeX && mx < closeX + 3)
|
|
92
|
+
setDialog(null);
|
|
93
|
+
else if (btn === 0 && my === 3 && mx >= barX && mx < barX + dialogW - 2) {
|
|
94
|
+
/* a click onto a predecessor or successor id (in the fourth dialog row) jumps to its task view */
|
|
95
|
+
let x = barX;
|
|
96
|
+
for (const seg of refSegs(all.pred.get(dialog.id) ?? [], all.succ.get(dialog.id) ?? [], () => undefined, dialogW - 2)) {
|
|
97
|
+
if (mx >= x && mx < x + seg.text.length) {
|
|
98
|
+
if (seg.ref !== undefined && all.cards.has(seg.ref))
|
|
99
|
+
setDialog({ id: seg.ref, tab: 0, first: 0, scrolls: {} });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
x += seg.text.length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (btn === 0 && my === 5 && mx >= barX && mx < barX + dialogW - 2) {
|
|
106
|
+
/* a click onto the tab bar (the sixth dialog row) selects the clicked
|
|
107
|
+
tab, or the previous/next tab on a scroll arrow, but never closes */
|
|
108
|
+
const lay = tabLayout(tabLabels, dialog.first, dialogSel, dialogW - 2);
|
|
109
|
+
const x = mx - barX;
|
|
110
|
+
const item = lay.items.find((it) => x >= it.x && x < it.x + it.text.length);
|
|
111
|
+
let tab = dialogSel;
|
|
112
|
+
if (item !== undefined)
|
|
113
|
+
tab = item.index;
|
|
114
|
+
else if (x === 0 && lay.less)
|
|
115
|
+
tab = dialogSel - 1;
|
|
116
|
+
else if (x === dialogW - 3 && lay.more)
|
|
117
|
+
tab = dialogSel + 1;
|
|
118
|
+
if (tab !== dialogSel)
|
|
119
|
+
setDialog({ ...dialog, tab, first: tabFirst(tabLabels, dialog.first, tab, dialogW - 4) });
|
|
120
|
+
}
|
|
121
|
+
else if (btn === 64 || btn === 65)
|
|
122
|
+
dialogScroll(btn === 64 ? -3 : 3);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
/* show the agents view with the (first) agent of an agent card of the agent graph selected */
|
|
128
|
+
const showAgent = (ctx, id) => {
|
|
129
|
+
const card = ctx.flow.cards.get(id);
|
|
130
|
+
if (card === undefined)
|
|
131
|
+
return;
|
|
132
|
+
ctx.focusAgent(card.agents[0].id);
|
|
133
|
+
ctx.setView("agents");
|
|
134
|
+
};
|
|
135
|
+
/* handle a mouse press (0-based column/row, button 0: left press,
|
|
136
|
+
64/65: wheel up/down): a click selects the clicked task and a double click
|
|
137
|
+
opens it, while a click onto its " X " closes an open task view, a click onto a
|
|
138
|
+
predecessor/successor id jumps to its task view, and the wheel scrolls it */
|
|
139
|
+
export const handleMouse = (ctx, btn, mx, my) => {
|
|
140
|
+
const { board, surface, view, setFilter, typing, setTyping, sel, setSel, setDialog, scroll, layout, carry, setCarry, lastClick, cardBoxes, laneBoxes, groupBoxes, headBoxes, agentBoxes, boxAt, graphView, toggle, grown, setGrow, cycleView, agentList, agentIdx, selectAgent, scrollLog, rangeBoxes, setRange, flowLayout, setFlowSel, scrollFlow } = ctx;
|
|
141
|
+
if (handlePopupMouse(ctx, btn, mx, my))
|
|
142
|
+
return;
|
|
143
|
+
/* a click onto the view value of the header switches the view, a click
|
|
144
|
+
onto the filter field starts typing into it (unless a task is moved),
|
|
145
|
+
a click onto its clear button clears it, and any other click ends
|
|
146
|
+
typing (keeping the filter query) */
|
|
147
|
+
const head = btn === 0 ? boxAt(headBoxes.current, mx, my) : undefined;
|
|
148
|
+
if (typing && head !== "filter")
|
|
149
|
+
setTyping(false);
|
|
150
|
+
if (head === "clear") {
|
|
151
|
+
if (view !== "agents" && view !== "flow")
|
|
152
|
+
setFilter("");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (head === "view") {
|
|
156
|
+
setCarry(null);
|
|
157
|
+
cycleView();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
/* in the agents view, the wheel scrolls the log of the detail view
|
|
161
|
+
and a click onto the card of an agent selects the agent */
|
|
162
|
+
if (view === "agents") {
|
|
163
|
+
if (btn === 64 || btn === 65)
|
|
164
|
+
scrollLog(btn === 64 ? -3 : 3);
|
|
165
|
+
else if (btn === 0) {
|
|
166
|
+
const id = boxAt(agentBoxes.current, mx, my);
|
|
167
|
+
const k = agentList.findIndex((agent) => agent.id === id);
|
|
168
|
+
if (k >= 0 && k !== agentIdx)
|
|
169
|
+
selectAgent(k - agentIdx);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (head === "filter") {
|
|
174
|
+
if (carry === null && view !== "flow")
|
|
175
|
+
setTyping(true);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (view === "stats") {
|
|
179
|
+
/* a click onto a time range button of the statistics view selects the time range */
|
|
180
|
+
const range = btn === 0 ? boxAt(rangeBoxes.current, mx, my) : undefined;
|
|
181
|
+
if (range !== undefined)
|
|
182
|
+
setRange(range);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if ((btn === 64 || btn === 65) && view === "flow") {
|
|
186
|
+
/* the wheel scrolls the viewport of the agent graph */
|
|
187
|
+
scrollFlow(0, btn === 64 ? -3 : 3);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if ((btn === 64 || btn === 65) && view === "lanes") {
|
|
191
|
+
/* the wheel over a lane scrolls it, by stepping the selection
|
|
192
|
+
through its cards (entering the lane at its first card) */
|
|
193
|
+
const at = boxAt(laneBoxes.current, mx, my);
|
|
194
|
+
if (at === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const [g, l] = at.split(":").map(Number);
|
|
197
|
+
const lane = board.groups[g].lanes[l];
|
|
198
|
+
const list = board.lanes.get(lane.status) ?? [];
|
|
199
|
+
if (list.length === 0 || surface.minimized.includes(lane.status) || surface.collapsed.includes(board.groups[g].title))
|
|
200
|
+
return;
|
|
201
|
+
const idx = sel.g === g && sel.l === l ? list.findIndex((c) => c.id === sel.id) : -1;
|
|
202
|
+
const next = idx < 0 ? 0 : Math.max(0, Math.min(list.length - 1, idx + (btn === 64 ? -1 : 1)));
|
|
203
|
+
setSel({ g, l, id: list[next].id });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (btn !== 0)
|
|
207
|
+
return;
|
|
208
|
+
/* detect a double click: a second left click onto the same cell within 400ms */
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const last = lastClick.current;
|
|
211
|
+
const double = last !== null && last.x === mx && last.y === my && now - last.at <= 400;
|
|
212
|
+
lastClick.current = double ? null : { x: mx, y: my, at: now };
|
|
213
|
+
/* in the agent graph, a click selects the agent card or state under it (mapped into
|
|
214
|
+
the graph grid via the measured viewport origin), and a double click onto an agent
|
|
215
|
+
card additionally shows the agents view with its (first) agent selected */
|
|
216
|
+
if (view === "flow") {
|
|
217
|
+
if (flowLayout === null || graphView.current === null)
|
|
218
|
+
return;
|
|
219
|
+
const m = measureElement(graphView.current);
|
|
220
|
+
const gx = mx - m.x + scroll.x;
|
|
221
|
+
const gy = my - m.y + scroll.y;
|
|
222
|
+
for (const n of flowLayout.graph.nodes.values())
|
|
223
|
+
if (gx >= n.x && gx < n.x + n.w && gy >= n.y && gy < n.y + n.h) {
|
|
224
|
+
setFlowSel(n.id);
|
|
225
|
+
if (double)
|
|
226
|
+
showAgent(ctx, n.id);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
/* a click onto a lane outside its cards (and while moving a task, anywhere
|
|
231
|
+
onto a lane) selects the lane, like PgUp/PgDn, where a double click onto the
|
|
232
|
+
lane minimizes/maximizes it (resp. shrinks a grown lane, resp. expands its
|
|
233
|
+
collapsed phase), while a click onto the title of a phase selects it, like ←/→,
|
|
234
|
+
and a double click onto it collapses/expands it */
|
|
235
|
+
const card = view === "lanes" && carry === null ? boxAt(cardBoxes.current, mx, my) : undefined;
|
|
236
|
+
if (view === "lanes" && card === undefined) {
|
|
237
|
+
const grp = boxAt(groupBoxes.current, mx, my);
|
|
238
|
+
if (grp !== undefined && my === measureElement(groupBoxes.current.get(grp)).y) {
|
|
239
|
+
const g = Number(grp);
|
|
240
|
+
if (double)
|
|
241
|
+
toggle("collapsed", board.groups[g].title);
|
|
242
|
+
else if (g !== sel.g) {
|
|
243
|
+
const items = groupItems(board, g, surface);
|
|
244
|
+
setSel(items.find((it) => it.l === sel.l) ?? items[0]);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const at = boxAt(laneBoxes.current, mx, my);
|
|
249
|
+
if (at !== undefined) {
|
|
250
|
+
const [g, l] = at.split(":").map(Number);
|
|
251
|
+
if (double && surface.collapsed.includes(board.groups[g].title))
|
|
252
|
+
toggle("collapsed", board.groups[g].title);
|
|
253
|
+
else if (double && grown)
|
|
254
|
+
setGrow(false);
|
|
255
|
+
else if (double)
|
|
256
|
+
toggle("minimized", board.groups[g].lanes[l].status);
|
|
257
|
+
const next = groupItems(board, g, surface).find((it) => it.l === l);
|
|
258
|
+
if (next !== undefined && !(sel.g === g && sel.l === l))
|
|
259
|
+
setSel(next);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (carry !== null)
|
|
264
|
+
return;
|
|
265
|
+
let hit;
|
|
266
|
+
if (view === "lanes")
|
|
267
|
+
hit = card;
|
|
268
|
+
else if (layout !== null && graphView.current !== null) {
|
|
269
|
+
/* map the click into the graph grid via the measured viewport origin */
|
|
270
|
+
const m = measureElement(graphView.current);
|
|
271
|
+
const gx = mx - m.x + scroll.x;
|
|
272
|
+
const gy = my - m.y + scroll.y;
|
|
273
|
+
for (const n of layout.graph.nodes.values())
|
|
274
|
+
if (gx >= n.x && gx < n.x + n.w && gy >= n.y && gy < n.y + n.h)
|
|
275
|
+
hit = n.id;
|
|
276
|
+
}
|
|
277
|
+
if (hit !== undefined && board.cards.has(hit)) {
|
|
278
|
+
/* a click selects the clicked task, a double click additionally opens its view */
|
|
279
|
+
setSel(relocate(board, { g: sel.g, l: sel.l, id: hit }, view === "graph" ? { ...surface, minimized: [], collapsed: [] } : surface));
|
|
280
|
+
if (double)
|
|
281
|
+
setDialog({ id: hit, tab: 0, first: 0, scrolls: {} });
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/* handle a key press onto the deletion confirmation, the transfer popup, the assign
|
|
285
|
+
popup, the prompt popup, or the filter field (outside of the read dialog) */
|
|
286
|
+
const handlePopupKey = (ctx, input, key) => {
|
|
287
|
+
const { dialog, setFilter, typing, setTyping, setNotice, confirm, setConfirm, transfer, setTransfer, transferList, remove, transferTo, assign, setAssign, assignTo, prompt, setPrompt, cancelPrompt, submitPrompt } = ctx;
|
|
288
|
+
/* operate the prompt popup: TAB resp. Shift+TAB switch the focus to the next resp.
|
|
289
|
+
previous field (the request name of a request by prompt, the prompt unless ignored, and
|
|
290
|
+
the request and cancel buttons), and ESC cancels in every field */
|
|
291
|
+
if (prompt !== null && (key.tab || key.escape)) {
|
|
292
|
+
if (key.escape)
|
|
293
|
+
cancelPrompt();
|
|
294
|
+
else {
|
|
295
|
+
const fields = ["name", "prompt", "request", "cancel"]
|
|
296
|
+
.filter((field) => (field !== "name" || prompt.request !== null) && (field !== "prompt" || !promptIgnored(prompt)));
|
|
297
|
+
const i = Math.max(0, fields.indexOf(prompt.focus));
|
|
298
|
+
setPrompt({ ...prompt, focus: fields[(i + (key.shift ? fields.length - 1 : 1)) % fields.length] });
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
/* press the focused button of the prompt popup: RETURN (or SPACE) presses it,
|
|
303
|
+
and the left/right arrows switch between the request and cancel buttons */
|
|
304
|
+
if (prompt !== null && (prompt.focus === "request" || prompt.focus === "cancel")) {
|
|
305
|
+
if ((key.return || input === " ") && prompt.focus === "request")
|
|
306
|
+
submitPrompt();
|
|
307
|
+
else if (key.return || input === " ")
|
|
308
|
+
cancelPrompt();
|
|
309
|
+
else if (key.leftArrow || key.rightArrow)
|
|
310
|
+
setPrompt({ ...prompt, focus: prompt.focus === "request" ? "cancel" : "request" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
/* type the prompt of an edit request: RETURN requests the edit, Shift+RETURN
|
|
314
|
+
(under the kitty keyboard protocol only), Alt+RETURN (under the kitty keyboard
|
|
315
|
+
protocol, or where the terminal sends it as ESC+RETURN, e.g. with "Option as Meta"
|
|
316
|
+
on macOS), and Ctrl+J (in every terminal, as a plain linefeed without the kitty
|
|
317
|
+
keyboard protocol) insert a newline,
|
|
318
|
+
the left/right arrows move the cursor by one character, HOME/END move it
|
|
319
|
+
to the start/end, BACKSPACE removes the character before the cursor, DELETE
|
|
320
|
+
removes the character at the cursor, and all other input (also pasted text,
|
|
321
|
+
with its newlines kept and its other control characters turned into spaces)
|
|
322
|
+
is inserted at the cursor */
|
|
323
|
+
if (prompt !== null && prompt.request !== null && prompt.focus === "name") {
|
|
324
|
+
/* type the request name of a request by prompt: RETURN switches to the prompt (resp. to
|
|
325
|
+
the request button if the prompt is ignored), the
|
|
326
|
+
up/down arrows cycle through the suggested request names of the drop-down (matching
|
|
327
|
+
the request name as last typed), BACKSPACE removes the last character, and all
|
|
328
|
+
other input of letters, digits, and dashes is appended (lower-cased) */
|
|
329
|
+
const request = prompt.request;
|
|
330
|
+
const choices = requestChoices(request);
|
|
331
|
+
const name = (value, typed = value) => setPrompt({ ...prompt, request: { ...request, name: value, typed } });
|
|
332
|
+
if (key.return)
|
|
333
|
+
setPrompt({ ...prompt, focus: promptNext(prompt) });
|
|
334
|
+
else if ((key.upArrow || key.downArrow) && choices.length > 0) {
|
|
335
|
+
const i = choices.indexOf(request.name);
|
|
336
|
+
const n = choices.length;
|
|
337
|
+
name(choices[i < 0 ? (key.upArrow ? n - 1 : 0) : (i + (key.upArrow ? n - 1 : 1)) % n], request.typed);
|
|
338
|
+
}
|
|
339
|
+
else if (key.backspace || key.delete)
|
|
340
|
+
name([...request.name].slice(0, -1).join(""));
|
|
341
|
+
else if (!key.ctrl && !key.meta && /^[A-Za-z0-9-]+$/.test(input))
|
|
342
|
+
name(request.name + input.toLowerCase());
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (prompt !== null) {
|
|
346
|
+
const chars = [...prompt.text];
|
|
347
|
+
const pos = Math.max(0, Math.min(prompt.pos, chars.length));
|
|
348
|
+
const insert = (text) => {
|
|
349
|
+
const ins = [...text];
|
|
350
|
+
chars.splice(pos, 0, ...ins);
|
|
351
|
+
setPrompt({ ...prompt, text: chars.join(""), pos: pos + ins.length });
|
|
352
|
+
};
|
|
353
|
+
if ((key.return && (key.shift || key.meta)) || (key.ctrl && input === "j"))
|
|
354
|
+
insert("\n");
|
|
355
|
+
else if (key.return)
|
|
356
|
+
submitPrompt();
|
|
357
|
+
else if (key.leftArrow || key.rightArrow)
|
|
358
|
+
setPrompt({ ...prompt, pos: Math.max(0, Math.min(chars.length, pos + (key.leftArrow ? -1 : 1))) });
|
|
359
|
+
else if (key.home || key.end)
|
|
360
|
+
setPrompt({ ...prompt, pos: key.home ? 0 : chars.length });
|
|
361
|
+
else if (key.backspace) {
|
|
362
|
+
if (pos > 0) {
|
|
363
|
+
chars.splice(pos - 1, 1);
|
|
364
|
+
setPrompt({ ...prompt, text: chars.join(""), pos: pos - 1 });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (key.delete) {
|
|
368
|
+
if (pos < chars.length) {
|
|
369
|
+
chars.splice(pos, 1);
|
|
370
|
+
setPrompt({ ...prompt, text: chars.join(""), pos });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (!key.ctrl && !key.meta && input !== "")
|
|
374
|
+
insert(input.replace(/\r\n?/g, "\n").replace(/[^\P{Cc}\n]/gu, " "));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
/* answer the confirmation of a task deletion: "y" deletes, ESC cancels, the
|
|
378
|
+
left/right arrows and TAB/Shift+TAB switch the selected button, RETURN
|
|
379
|
+
presses it, and all other keys are ignored */
|
|
380
|
+
if (confirm !== null) {
|
|
381
|
+
if (input === "y" || (key.return && confirm.yes)) {
|
|
382
|
+
remove(confirm.id);
|
|
383
|
+
setConfirm(null);
|
|
384
|
+
}
|
|
385
|
+
else if (key.escape || key.return) {
|
|
386
|
+
setNotice(`deleting task "${confirm.id}" cancelled`);
|
|
387
|
+
setConfirm(null);
|
|
388
|
+
}
|
|
389
|
+
else if (key.leftArrow || key.rightArrow || key.tab)
|
|
390
|
+
setConfirm({ ...confirm, yes: !confirm.yes });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
/* answer the transfer popup: the up/down arrows select the previous/next
|
|
394
|
+
selectable state, RETURN transfers the task to it, ESC cancels, and all
|
|
395
|
+
other keys are ignored */
|
|
396
|
+
if (transfer !== null) {
|
|
397
|
+
if (key.upArrow || key.downArrow) {
|
|
398
|
+
const step = key.upArrow ? -1 : 1;
|
|
399
|
+
for (let k = transferList.findIndex((e) => e.status === transfer.at) + step; k >= 0 && k < transferList.length; k += step)
|
|
400
|
+
if (transferList[k].ok) {
|
|
401
|
+
setTransfer({ ...transfer, at: transferList[k].status });
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (key.return)
|
|
406
|
+
transferTo(transfer.id, transfer.at);
|
|
407
|
+
else if (key.escape) {
|
|
408
|
+
setNotice(`transferring task "${transfer.id}" cancelled`);
|
|
409
|
+
setTransfer(null);
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
/* answer the assign popup: RETURN assigns the task to the assignee, ESC cancels, the
|
|
414
|
+
up/down arrows cycle through the suggested assignees of the drop-down (matching the
|
|
415
|
+
assignee as last typed), BACKSPACE removes the last character, and all other input
|
|
416
|
+
(without control characters and commas) is appended */
|
|
417
|
+
if (assign !== null) {
|
|
418
|
+
const choices = assignChoices(assign).map((choice) => choice.name);
|
|
419
|
+
const name = (value, typed = value) => setAssign({ ...assign, name: value, typed });
|
|
420
|
+
if (key.return)
|
|
421
|
+
assignTo(assign.id, assign.name);
|
|
422
|
+
else if (key.escape) {
|
|
423
|
+
setNotice(`assigning task "${assign.id}" cancelled`);
|
|
424
|
+
setAssign(null);
|
|
425
|
+
}
|
|
426
|
+
else if ((key.upArrow || key.downArrow) && choices.length > 0) {
|
|
427
|
+
const i = choices.indexOf(assign.name);
|
|
428
|
+
const n = choices.length;
|
|
429
|
+
name(choices[i < 0 ? (key.upArrow ? n - 1 : 0) : (i + (key.upArrow ? n - 1 : 1)) % n], assign.typed);
|
|
430
|
+
}
|
|
431
|
+
else if (key.backspace || key.delete)
|
|
432
|
+
name([...assign.name].slice(0, -1).join(""));
|
|
433
|
+
else if (!key.ctrl && !key.meta && /^[^\p{Cc},]+$/u.test(input))
|
|
434
|
+
name(assign.name + input);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
/* while typing into the filter field, all keys edit the filter query
|
|
438
|
+
(applied live), until ENTER keeps it or ESC clears it */
|
|
439
|
+
if (typing && dialog === null) {
|
|
440
|
+
if (key.return)
|
|
441
|
+
setTyping(false);
|
|
442
|
+
else if (key.escape) {
|
|
443
|
+
setFilter("");
|
|
444
|
+
setTyping(false);
|
|
445
|
+
}
|
|
446
|
+
else if (key.backspace || key.delete)
|
|
447
|
+
setFilter((f) => f.slice(0, -1));
|
|
448
|
+
else if (!key.ctrl && !key.meta && !/\p{Cc}/u.test(input))
|
|
449
|
+
setFilter((f) => f + input);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
/* handle a key press within the read dialog */
|
|
453
|
+
const handleDialogKey = (ctx, dialog, input, key) => {
|
|
454
|
+
const { rows, setDialog, dialogW, tabLabels, dialogSel, dialogScroll, startEdit } = ctx;
|
|
455
|
+
const page = Math.max(1, rows - DIALOG_CHROME - 2);
|
|
456
|
+
if (key.escape || key.return)
|
|
457
|
+
setDialog(null);
|
|
458
|
+
else if (input === "e")
|
|
459
|
+
startEdit(dialog.id);
|
|
460
|
+
else if (key.leftArrow || key.rightArrow || key.tab) {
|
|
461
|
+
const tab = Math.max(0, Math.min(tabLabels.length - 1, dialogSel + (key.leftArrow || (key.tab && key.shift) ? -1 : 1)));
|
|
462
|
+
setDialog({ ...dialog, tab, first: tabFirst(tabLabels, dialog.first, tab, dialogW - 4) });
|
|
463
|
+
}
|
|
464
|
+
else if (key.upArrow)
|
|
465
|
+
dialogScroll(-1);
|
|
466
|
+
else if (key.downArrow)
|
|
467
|
+
dialogScroll(1);
|
|
468
|
+
else if (key.pageUp)
|
|
469
|
+
dialogScroll(-page);
|
|
470
|
+
else if (key.pageDown)
|
|
471
|
+
dialogScroll(page);
|
|
472
|
+
};
|
|
473
|
+
/* handle a key press within the lane view: moving and growing, else navigating */
|
|
474
|
+
const handleLaneKey = (ctx, input, key) => {
|
|
475
|
+
const { columns, board, surface, sel, setSel, setNotice, carry, setCarry, cycle, drop, grown, setGrow, boardH } = ctx;
|
|
476
|
+
if (key.escape && carry !== null) {
|
|
477
|
+
/* cancel the move and return the selection to the task at its original position */
|
|
478
|
+
setCarry(null);
|
|
479
|
+
setSel(relocate(board, { g: sel.g, l: sel.l, id: carry.id }, surface));
|
|
480
|
+
setNotice(`moving task "${carry.id}" cancelled`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (input === " ") {
|
|
484
|
+
/* pick up the selected task, determining all lanes whose state
|
|
485
|
+
is directly or indirectly reachable from its current state */
|
|
486
|
+
if (carry === null) {
|
|
487
|
+
const card = board.cards.get(sel.id);
|
|
488
|
+
if (card === undefined)
|
|
489
|
+
return;
|
|
490
|
+
if (cycle === null) {
|
|
491
|
+
setNotice("task lifecycle model not yet loaded");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const targets = new Set(reachableStates(board, cycle, card));
|
|
495
|
+
setCarry({ id: card.id, from: card.status, targets });
|
|
496
|
+
setGrow(false);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
/* drop the carried task onto the selected lane, if reachable */
|
|
500
|
+
if (surface.collapsed.includes(board.groups[sel.g].title))
|
|
501
|
+
return;
|
|
502
|
+
const to = board.groups[sel.g].lanes[sel.l].status;
|
|
503
|
+
if (to === carry.from) {
|
|
504
|
+
setCarry(null);
|
|
505
|
+
setNotice(`moving task "${carry.id}" cancelled`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (!carry.targets.has(to)) {
|
|
509
|
+
setNotice(`task "${carry.id}" cannot move from ${carry.from} to ${to}: not reachable in the lifecycle model`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
drop(carry.id, to);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (input === "g") {
|
|
516
|
+
/* grow the selected lane to the full board, or shrink it back again */
|
|
517
|
+
if (carry !== null)
|
|
518
|
+
return;
|
|
519
|
+
if (!grown && (surface.collapsed.includes(board.groups[sel.g].title)
|
|
520
|
+
|| surface.minimized.includes(board.groups[sel.g].lanes[sel.l].status)))
|
|
521
|
+
setNotice("only a maximized lane of an expanded phase can grow");
|
|
522
|
+
else
|
|
523
|
+
setGrow(!grown);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (grown) {
|
|
527
|
+
/* within the grown lane, the arrows step through the grid of its cards,
|
|
528
|
+
PgUp/PgDn step by (roughly) the visible rows, Home/End jump to the
|
|
529
|
+
first/last card, ESC shrinks the lane, and the other lane keys are ignored */
|
|
530
|
+
if (key.escape) {
|
|
531
|
+
setGrow(false);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const list = board.lanes.get(board.groups[sel.g].lanes[sel.l].status) ?? [];
|
|
535
|
+
const cols = gridColumns(columns - 2);
|
|
536
|
+
const page = cols * Math.max(1, Math.floor((boardH - 4) / (surface.titles ? 5 : 3)));
|
|
537
|
+
const step = key.leftArrow ? -1 : key.rightArrow ? 1 : key.upArrow ? -cols : key.downArrow ? cols :
|
|
538
|
+
key.pageUp ? -page : key.pageDown ? page : key.home ? -list.length : key.end ? list.length : 0;
|
|
539
|
+
const idx = list.findIndex((c) => c.id === sel.id);
|
|
540
|
+
if (step === 0 || list.length === 0 || (key.upArrow && idx >= 0 && idx < cols))
|
|
541
|
+
return;
|
|
542
|
+
const next = idx < 0 ? 0 : Math.max(0, Math.min(list.length - 1, idx + step));
|
|
543
|
+
setSel({ g: sel.g, l: sel.l, id: list[next].id });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
handleNavKey(ctx, input, key);
|
|
547
|
+
};
|
|
548
|
+
/* handle a key press navigating the lanes and groups of the lane view */
|
|
549
|
+
const handleNavKey = (ctx, input, key) => {
|
|
550
|
+
const { board, surface, sel, setSel, carry, toggle } = ctx;
|
|
551
|
+
if (input === "m" && !key.shift) {
|
|
552
|
+
if (surface.collapsed.includes(board.groups[sel.g].title))
|
|
553
|
+
return;
|
|
554
|
+
const lane = board.groups[sel.g].lanes[sel.l];
|
|
555
|
+
toggle("minimized", lane.status);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (input === "c" && !key.shift) {
|
|
559
|
+
toggle("collapsed", board.groups[sel.g].title);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
/* collapse or expand the lane row of the selected lane in all expanded phases at once
|
|
563
|
+
(under the kitty keyboard protocol, Shift+c arrives as "c" with the shift modifier) */
|
|
564
|
+
if (input === "C" || (input === "c" && key.shift)) {
|
|
565
|
+
const row = rowLanes(board.groups, surface.collapsed, sel.l);
|
|
566
|
+
if (row.length > 0)
|
|
567
|
+
toggle("minimized", row);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (key.leftArrow || key.rightArrow) {
|
|
571
|
+
const g = Math.max(0, Math.min(board.groups.length - 1, sel.g + (key.leftArrow ? -1 : 1)));
|
|
572
|
+
if (g !== sel.g) {
|
|
573
|
+
const items = groupItems(board, g, surface);
|
|
574
|
+
setSel(items.find((it) => it.l === sel.l) ?? items[0]);
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if ((key.home || key.end) && carry === null) {
|
|
579
|
+
/* jump to the first or last item of the selected lane, scrolling it to its top or bottom */
|
|
580
|
+
const items = groupItems(board, sel.g, surface).filter((it) => it.l === sel.l);
|
|
581
|
+
const next = key.home ? items[0] : items[items.length - 1];
|
|
582
|
+
if (next !== undefined)
|
|
583
|
+
setSel(next);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if ((key.upArrow || key.downArrow) && carry === null) {
|
|
587
|
+
/* within a collapsed phase, the items are the lanes themselves */
|
|
588
|
+
const items = groupItems(board, sel.g, surface);
|
|
589
|
+
const idx = items.findIndex((it) => it.l === sel.l && it.id === sel.id);
|
|
590
|
+
const next = items[Math.max(0, Math.min(items.length - 1, idx + (key.upArrow ? -1 : 1)))];
|
|
591
|
+
if (next !== undefined)
|
|
592
|
+
setSel(next);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
596
|
+
/* jump directly to the first item of the upper or lower lane of the group
|
|
597
|
+
(while moving a task, also on up/down, as only the target lane matters) */
|
|
598
|
+
const up = key.pageUp || key.upArrow;
|
|
599
|
+
const l = Math.max(0, Math.min(board.groups[sel.g].lanes.length - 1, sel.l + (up ? -1 : 1)));
|
|
600
|
+
const next = groupItems(board, sel.g, surface).find((it) => it.l === l);
|
|
601
|
+
if (next !== undefined && l !== sel.l)
|
|
602
|
+
setSel(next);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (key.tab) {
|
|
606
|
+
/* step through the lanes of the expanded phases lane row by lane row: first
|
|
607
|
+
left/right through the phases, then wrap into the next/previous
|
|
608
|
+
lane row (and at the very end/start around the board) */
|
|
609
|
+
const rows = Math.max(...board.groups.map((g) => g.lanes.length));
|
|
610
|
+
const lanes = [];
|
|
611
|
+
for (let l = 0; l < rows; l++)
|
|
612
|
+
board.groups.forEach((group, g) => {
|
|
613
|
+
if (l < group.lanes.length && !surface.collapsed.includes(group.title))
|
|
614
|
+
lanes.push({ g, l });
|
|
615
|
+
});
|
|
616
|
+
if (lanes.length === 0)
|
|
617
|
+
return;
|
|
618
|
+
const idx = lanes.findIndex((p) => p.g === sel.g && p.l === sel.l);
|
|
619
|
+
const target = idx < 0 ? lanes[key.shift ? lanes.length - 1 : 0] :
|
|
620
|
+
lanes[(idx + (key.shift ? lanes.length - 1 : 1)) % lanes.length];
|
|
621
|
+
const next = groupItems(board, target.g, surface).find((it) => it.l === target.l);
|
|
622
|
+
if (next !== undefined)
|
|
623
|
+
setSel(next);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
/* handle a key press */
|
|
627
|
+
export const handleKey = (ctx, input, key) => {
|
|
628
|
+
const { exit, all, board, surface, view, setView, typing, setTyping, sel, setSel, dialog, setDialog, notice, setNotice, carry, setCarry, cycle, confirm, setConfirm, transfer, setTransfer, assign, startAssign, prompt, mouse, setMouse, places, nodes, toggleFlag, startEdit, startPrompt, startRequest, cycleView, selectAgent, scrollLog, logH, stepRange, flowSel, setFlowSel, flowPlaces, scrollFlow, viewH } = ctx;
|
|
629
|
+
if (notice !== null)
|
|
630
|
+
setNotice(null);
|
|
631
|
+
/* answer the deletion confirmation, the transfer popup, the assign popup, the prompt popup, or the filter field */
|
|
632
|
+
if (confirm !== null || transfer !== null || assign !== null || prompt !== null || (typing && dialog === null)) {
|
|
633
|
+
handlePopupKey(ctx, input, key);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
/* toggle the mouse support in every view (under the kitty keyboard
|
|
637
|
+
protocol, Shift+m arrives as "m" with the shift modifier) */
|
|
638
|
+
if (input === "M" || (input === "m" && key.shift)) {
|
|
639
|
+
setMouse(!mouse);
|
|
640
|
+
setNotice(mouse ? "mouse support disabled: regular text selection available" : "mouse support enabled");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
/* request the deletion of a task in every view (under the kitty keyboard
|
|
644
|
+
protocol, Shift+d arrives as "d" with the shift modifier) */
|
|
645
|
+
const target = dialog !== null ? dialog.id : view === "agents" || view === "stats" || view === "flow" ? "" : sel.id;
|
|
646
|
+
if ((input === "D" || (input === "d" && key.shift)) && target !== "" && carry === null) {
|
|
647
|
+
setConfirm({ id: target, yes: false });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
/* request the transfer of a task to another state in every view, starting
|
|
651
|
+
at its current state (under the kitty keyboard protocol, Shift+t
|
|
652
|
+
arrives as "t" with the shift modifier) */
|
|
653
|
+
if ((input === "T" || (input === "t" && key.shift)) && target !== "" && carry === null) {
|
|
654
|
+
const card = all.cards.get(target);
|
|
655
|
+
if (cycle === null)
|
|
656
|
+
setNotice("task lifecycle model not yet loaded");
|
|
657
|
+
else if (card !== undefined)
|
|
658
|
+
setTransfer({ id: card.id, at: card.status });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
/* request the assignment of a task in every view (under the kitty keyboard
|
|
662
|
+
protocol, Shift+a arrives as "a" with the shift modifier) */
|
|
663
|
+
if ((input === "A" || (input === "a" && key.shift)) && target !== "" && carry === null) {
|
|
664
|
+
startAssign(target);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
/* request the edit of a task by a prompt in every view (under the kitty
|
|
668
|
+
keyboard protocol, Shift+e arrives as "e" with the shift modifier) */
|
|
669
|
+
if ((input === "E" || (input === "e" && key.shift)) && target !== "" && carry === null) {
|
|
670
|
+
startPrompt(target);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
/* request a task by a prompt of any request name in every view (under the kitty
|
|
674
|
+
keyboard protocol, Shift+r arrives as "r" with the shift modifier) */
|
|
675
|
+
if ((input === "R" || (input === "r" && key.shift)) && target !== "" && carry === null) {
|
|
676
|
+
startRequest(target);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (dialog !== null) {
|
|
680
|
+
handleDialogKey(ctx, dialog, input, key);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (input === "q") {
|
|
684
|
+
exit();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (input === "/") {
|
|
688
|
+
if (carry === null && view !== "agents" && view !== "flow")
|
|
689
|
+
setTyping(true);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
/* cycle the views forward (resp. backward with "V", which arrives as "v"
|
|
693
|
+
with the shift modifier under the kitty keyboard protocol) */
|
|
694
|
+
if (input === "v" || input === "V") {
|
|
695
|
+
setCarry(null);
|
|
696
|
+
cycleView(input === "V" || key.shift);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (view === "agents") {
|
|
700
|
+
/* the arrows select an agent of the master list, while Shift with the arrows,
|
|
701
|
+
PgUp/PgDn, and Home/End scroll the log of the detail view */
|
|
702
|
+
if ((key.upArrow || key.downArrow) && key.shift)
|
|
703
|
+
scrollLog(key.upArrow ? -1 : 1);
|
|
704
|
+
else if (key.upArrow || key.downArrow)
|
|
705
|
+
selectAgent(key.upArrow ? -1 : 1);
|
|
706
|
+
else if (key.pageUp || key.pageDown)
|
|
707
|
+
scrollLog((key.pageUp ? -1 : 1) * Math.max(1, logH - 1));
|
|
708
|
+
else if (key.home || key.end)
|
|
709
|
+
scrollLog(key.home ? "top" : "bottom");
|
|
710
|
+
else if (input === "?")
|
|
711
|
+
toggleFlag("keys");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (view === "stats") {
|
|
715
|
+
/* the left/right arrows step through the time ranges of the statistics */
|
|
716
|
+
if (key.leftArrow || key.rightArrow)
|
|
717
|
+
stepRange(key.leftArrow ? -1 : 1);
|
|
718
|
+
else if (input === "?")
|
|
719
|
+
toggleFlag("keys");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (view === "flow") {
|
|
723
|
+
/* the arrows move spatially to the nearest agent card or state in their direction,
|
|
724
|
+
and ENTER on an agent card shows the agents view with its (first) agent selected, while
|
|
725
|
+
PgUp/PgDn and Home/End scroll the viewport */
|
|
726
|
+
const dir = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : null;
|
|
727
|
+
if (key.return)
|
|
728
|
+
showAgent(ctx, flowSel);
|
|
729
|
+
else if (key.pageUp || key.pageDown)
|
|
730
|
+
scrollFlow(0, (key.pageUp ? -1 : 1) * Math.max(1, viewH - 1));
|
|
731
|
+
else if (key.home || key.end)
|
|
732
|
+
scrollFlow(0, key.home ? -Infinity : Infinity);
|
|
733
|
+
else if (dir !== null) {
|
|
734
|
+
const next = nearestPlace(flowPlaces, flowSel, dir);
|
|
735
|
+
if (next !== undefined)
|
|
736
|
+
setFlowSel(next);
|
|
737
|
+
}
|
|
738
|
+
else if (input === "?")
|
|
739
|
+
toggleFlag("keys");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (key.return && sel.id !== "") {
|
|
743
|
+
setDialog({ id: sel.id, tab: 0, first: 0, scrolls: {} });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (input === "e") {
|
|
747
|
+
if (sel.id !== "")
|
|
748
|
+
startEdit(sel.id);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (input === "N" || (input === "n" && key.shift)) {
|
|
752
|
+
if (carry === null)
|
|
753
|
+
startEdit(null);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if ((input === "t" && !key.shift) || input === "?") {
|
|
757
|
+
toggleFlag(input === "t" ? "titles" : "keys");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (input === "s" && view === "graph") {
|
|
761
|
+
toggleFlag("standalone");
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (view === "graph") {
|
|
765
|
+
/* move spatially to the nearest node in the direction of the arrow */
|
|
766
|
+
const dir = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : null;
|
|
767
|
+
if (dir === null)
|
|
768
|
+
return;
|
|
769
|
+
const next = places.has(sel.id) ? nearestPlace(places, sel.id, dir) : nodes[0]?.id;
|
|
770
|
+
if (next !== undefined && board.cards.has(next))
|
|
771
|
+
setSel(relocate(board, { g: sel.g, l: sel.l, id: next }, { ...surface, minimized: [], collapsed: [] }));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
handleLaneKey(ctx, input, key);
|
|
775
|
+
};
|