tumwater 0.4.0 → 0.4.2
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/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -68,7 +68,20 @@ function markerHasLabel(line) {
|
|
|
68
68
|
* readCompleteLines uses. A line straddling a chunk boundary is held (tail + newline) until
|
|
69
69
|
* its head arrives from an older chunk, so no line is ever decoded torn. */
|
|
70
70
|
export function readTranscriptTail(file, limit, opts = {}) {
|
|
71
|
-
const
|
|
71
|
+
const opened = openTailFile(file);
|
|
72
|
+
if (opened === null)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const state = scanTailFromEof(opened.fd, opened.size, limit, opts.includePrompts === true);
|
|
76
|
+
return finalizeTailWindow(file, state, limit, opts);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
fs.closeSync(opened.fd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Opens the log and stat-guards it: returns the opened `{ fd, size }`, or null when the file
|
|
83
|
+
* is missing, empty, or rotated away between stat and open. Owns the open/stat guard. */
|
|
84
|
+
function openTailFile(file) {
|
|
72
85
|
const st = statOrNull(file);
|
|
73
86
|
if (!st || st.size === 0)
|
|
74
87
|
return null;
|
|
@@ -88,130 +101,160 @@ export function readTranscriptTail(file, limit, opts = {}) {
|
|
|
88
101
|
fs.closeSync(fd);
|
|
89
102
|
return null;
|
|
90
103
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
return { fd, size };
|
|
105
|
+
}
|
|
106
|
+
/** Finds the newest complete line's start in a chunk and sets up the straddling-line hold at
|
|
107
|
+
* the chunk's outer boundary; returns the walk's emitStart, or null when the whole chunk is one
|
|
108
|
+
* incomplete line. Owns the chunk-boundary arithmetic. */
|
|
109
|
+
function prepareTailChunk(fd, c, got, state) {
|
|
110
|
+
const lastNl = c.lastIndexOf(10);
|
|
111
|
+
if (lastNl < 0)
|
|
112
|
+
return null; // No complete line in this stretch yet.
|
|
113
|
+
if (state.end === 0)
|
|
114
|
+
state.end = state.pos - got + lastNl + 1; // The first complete line found is the newest one.
|
|
115
|
+
// The oldest line of c is incomplete when older bytes exist and the region does not
|
|
116
|
+
// start at a line boundary — hold it for the next chunk instead of decoding it torn.
|
|
117
|
+
let emitStart = 0;
|
|
118
|
+
if (state.pos - got > 0) {
|
|
119
|
+
const boundary = Buffer.alloc(1);
|
|
120
|
+
fs.readSync(fd, boundary, 0, 1, state.pos - got - 1);
|
|
121
|
+
if (boundary[0] !== 10) {
|
|
122
|
+
const firstNl = c.indexOf(10); // ≥ 0 — lastNl exists.
|
|
123
|
+
state.held = Buffer.from(c.subarray(0, firstNl + 1));
|
|
124
|
+
emitStart = firstNl + 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
state.held = EMPTY_BUFFER; // Reached file start: nothing older to hold for.
|
|
129
|
+
}
|
|
130
|
+
// A newline at c's start terminates a zero-length line (a blank line whose terminating
|
|
131
|
+
// \n is the region's first byte — e.g. the log starts with one). The walk below finds a
|
|
132
|
+
// line's start via lastIndexOf(10, lineEnd - 1), which for that line would search from
|
|
133
|
+
// index -1 and wrap to c's LAST newline — re-emitting every newer line until the
|
|
134
|
+
// candidate count reaches limit. Skip the zero-length line by starting the walk after it.
|
|
135
|
+
if (c[emitStart] === 10)
|
|
136
|
+
emitStart += 1;
|
|
137
|
+
return emitStart;
|
|
138
|
+
}
|
|
139
|
+
/** Advances the scan state across one pushed line: the arming walk while armed, else the
|
|
140
|
+
* candidate/agent_start transition; returns true when the walk must stop. Owns the arming
|
|
141
|
+
* state walk. */
|
|
142
|
+
function advanceTailScan(raw, state, includePrompts, limit) {
|
|
99
143
|
// Advances the arming state across one pushed line while `armed` — walking older lines for
|
|
100
144
|
// the label-bearing marker that precedes the arming agent_start. Every line pushed here
|
|
101
145
|
// sits inside [marker .. EOF] when a boundary lands, so the walk keeps it. Returns true when
|
|
102
146
|
// a boundary was found and the walk must stop; false to keep walking.
|
|
103
|
-
const walkArmed = (
|
|
147
|
+
const walkArmed = () => {
|
|
104
148
|
if (raw.includes('"tumwater_run"')) {
|
|
105
149
|
// A kind-only marker carries no label and is skipped — a full re-read lets it pass
|
|
106
150
|
// through too — so the scan keeps walking to the labeled marker that actually applies,
|
|
107
151
|
// or to the older agent_start when the run is unlabeled.
|
|
108
152
|
if (markerHasLabel(raw)) {
|
|
109
|
-
armed = false; // Boundary found — the post-loop EOF-while-armed fallback must not fire.
|
|
110
|
-
stoppedAtBoundary = true; // Window [this marker .. EOF] renders identically to a full re-read.
|
|
153
|
+
state.armed = false; // Boundary found — the post-loop EOF-while-armed fallback must not fire.
|
|
154
|
+
state.stoppedAtBoundary = true; // Window [this marker .. EOF] renders identically to a full re-read.
|
|
111
155
|
return true;
|
|
112
156
|
}
|
|
113
157
|
return false;
|
|
114
158
|
}
|
|
115
159
|
if (raw.includes('"agent_start"')) {
|
|
116
|
-
lines.splice(boundaryIndex + 1); // No label before the next older run: unlabeled — stop at A.
|
|
117
|
-
armed = false;
|
|
118
|
-
stoppedAtBoundary = true;
|
|
160
|
+
state.lines.splice(state.boundaryIndex + 1); // No label before the next older run: unlabeled — stop at A.
|
|
161
|
+
state.armed = false;
|
|
162
|
+
state.stoppedAtBoundary = true;
|
|
119
163
|
return true;
|
|
120
164
|
}
|
|
121
165
|
return false;
|
|
122
166
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const boundary = Buffer.alloc(1);
|
|
148
|
-
fs.readSync(fd, boundary, 0, 1, pos - got - 1);
|
|
149
|
-
if (boundary[0] !== 10) {
|
|
150
|
-
const firstNl = c.indexOf(10); // ≥ 0 — lastNl exists.
|
|
151
|
-
held = Buffer.from(c.subarray(0, firstNl + 1));
|
|
152
|
-
emitStart = firstNl + 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
held = EMPTY_BUFFER; // Reached file start: nothing older to hold for.
|
|
157
|
-
}
|
|
158
|
-
// A newline at c's start terminates a zero-length line (a blank line whose terminating
|
|
159
|
-
// \n is the region's first byte — e.g. the log starts with one). The walk below finds a
|
|
160
|
-
// line's start via lastIndexOf(10, lineEnd - 1), which for that line would search from
|
|
161
|
-
// index -1 and wrap to c's LAST newline — re-emitting every newer line until the
|
|
162
|
-
// candidate count reaches limit. Skip the zero-length line by starting the walk after it.
|
|
163
|
-
if (c[emitStart] === 10)
|
|
164
|
-
emitStart += 1;
|
|
165
|
-
let lineEnd = lastNl; // Byte index of the \n terminating the newest complete line in c.
|
|
166
|
-
while (lineEnd >= emitStart) {
|
|
167
|
-
const lineStart = c.lastIndexOf(10, lineEnd - 1) + 1;
|
|
168
|
-
if (lineStart < emitStart)
|
|
169
|
-
break; // Safety — cannot happen: emitStart follows a \n.
|
|
170
|
-
const raw = c.toString("utf8", lineStart, lineEnd);
|
|
171
|
-
if (!raw.trim()) {
|
|
172
|
-
lineEnd = lineStart - 1; // Blank lines render nothing.
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
lines.push(raw);
|
|
176
|
-
if (armed) {
|
|
177
|
-
// Walking for the label-bearing marker line preceding the arming agent_start.
|
|
178
|
-
if (walkArmed(raw))
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
else if (isEntryCandidate(raw, includePrompts)) {
|
|
182
|
-
candidates += 1;
|
|
183
|
-
}
|
|
184
|
-
else if (raw.includes('"agent_start"') && candidates >= limit) {
|
|
185
|
-
armed = true; // Keep walking for the marker line preceding this agent_start.
|
|
186
|
-
boundaryIndex = lines.length - 1;
|
|
187
|
-
}
|
|
188
|
-
lineEnd = lineStart - 1;
|
|
189
|
-
}
|
|
190
|
-
pos -= got;
|
|
191
|
-
if (stoppedAtBoundary || pos <= 0)
|
|
192
|
-
break;
|
|
167
|
+
if (state.armed)
|
|
168
|
+
return walkArmed();
|
|
169
|
+
if (isEntryCandidate(raw, includePrompts)) {
|
|
170
|
+
state.candidates += 1;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (raw.includes('"agent_start"') && state.candidates >= limit) {
|
|
174
|
+
state.armed = true; // Keep walking for the marker line preceding this agent_start.
|
|
175
|
+
state.boundaryIndex = state.lines.length - 1;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
/** Decodes and collects a chunk's complete lines newest-first, threading the arming state
|
|
180
|
+
* through advanceTailScan until the walk must stop. Owns the per-chunk line walk. */
|
|
181
|
+
function walkTailChunkLines(state, c, emitStart, includePrompts, limit) {
|
|
182
|
+
let lineEnd = c.lastIndexOf(10); // Byte index of the \n terminating the newest complete line in c.
|
|
183
|
+
while (lineEnd >= emitStart) {
|
|
184
|
+
const lineStart = c.lastIndexOf(10, lineEnd - 1) + 1;
|
|
185
|
+
if (lineStart < emitStart)
|
|
186
|
+
break; // Safety — cannot happen: emitStart follows a \n.
|
|
187
|
+
const raw = c.toString("utf8", lineStart, lineEnd);
|
|
188
|
+
if (!raw.trim()) {
|
|
189
|
+
lineEnd = lineStart - 1; // Blank lines render nothing.
|
|
190
|
+
continue;
|
|
193
191
|
}
|
|
192
|
+
state.lines.push(raw);
|
|
193
|
+
if (advanceTailScan(raw, state, includePrompts, limit))
|
|
194
|
+
break;
|
|
195
|
+
lineEnd = lineStart - 1;
|
|
194
196
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
}
|
|
198
|
+
/** The backward chunk-scan loop over the opened fd, from EOF to file start, returning the
|
|
199
|
+
* threaded TailScanState. Owns the chunk scan loop. */
|
|
200
|
+
function scanTailFromEof(fd, size, limit, includePrompts) {
|
|
201
|
+
const state = {
|
|
202
|
+
size,
|
|
203
|
+
pos: size, // Exclusive end of the not-yet-scanned region [pos - chunk, pos).
|
|
204
|
+
held: EMPTY_BUFFER, // Tail (with its newline) of a line that starts before the current chunk and ends inside it.
|
|
205
|
+
lines: [], // Complete lines, newest first.
|
|
206
|
+
candidates: 0, // Entry-candidate lines seen so far, counting from EOF.
|
|
207
|
+
end: 0, // Offset just past the last complete newline; 0 when the file has none.
|
|
208
|
+
stoppedAtBoundary: false,
|
|
209
|
+
armed: false, // A qualifying agent_start was recorded; walking older lines for its marker line.
|
|
210
|
+
boundaryIndex: -1, // Index in `lines` of the arming agent_start (valid while armed).
|
|
211
|
+
};
|
|
212
|
+
for (;;) {
|
|
213
|
+
const len = Math.min(TAIL_SCAN_CHUNK, state.pos);
|
|
214
|
+
if (len <= 0)
|
|
215
|
+
break;
|
|
216
|
+
const buf = Buffer.alloc(len);
|
|
217
|
+
const got = fs.readSync(fd, buf, 0, len, state.pos - len);
|
|
218
|
+
if (got <= 0)
|
|
219
|
+
break; // Shrank under us — use what we have.
|
|
220
|
+
// Covers the file region [pos - got, pos + held.length): this chunk plus the tail of
|
|
221
|
+
// the line that straddles its newer boundary (if any).
|
|
222
|
+
const c = state.held.length > 0
|
|
223
|
+
? Buffer.concat([buf.subarray(0, got), state.held])
|
|
224
|
+
: buf.subarray(0, got);
|
|
225
|
+
const emitStart = prepareTailChunk(fd, c, got, state);
|
|
226
|
+
if (emitStart === null) {
|
|
227
|
+
state.held = Buffer.from(c); // No complete line in this stretch yet.
|
|
228
|
+
state.pos -= got;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
walkTailChunkLines(state, c, emitStart, includePrompts, limit);
|
|
232
|
+
state.pos -= got;
|
|
233
|
+
if (state.stoppedAtBoundary || state.pos <= 0)
|
|
234
|
+
break;
|
|
197
235
|
}
|
|
198
|
-
|
|
236
|
+
return state;
|
|
237
|
+
}
|
|
238
|
+
/** Builds the rendered window from a finished (or EOF-armed) scan: the arming splice, the
|
|
239
|
+
* whole-file fallback and the slice(-limit) guard. Owns the final window. */
|
|
240
|
+
function finalizeTailWindow(file, state, limit, opts) {
|
|
241
|
+
if (state.armed) {
|
|
199
242
|
// Reached EOF while still arming: no marker precedes the run — unlabeled, stop at its
|
|
200
243
|
// agent_start.
|
|
201
|
-
lines.splice(boundaryIndex + 1);
|
|
202
|
-
stoppedAtBoundary = true;
|
|
244
|
+
state.lines.splice(state.boundaryIndex + 1);
|
|
245
|
+
state.stoppedAtBoundary = true;
|
|
203
246
|
}
|
|
204
|
-
let entries = formatTranscript(lines.reverse(), opts); // Oldest first.
|
|
205
|
-
if (entries.length < limit && stoppedAtBoundary) {
|
|
247
|
+
let entries = formatTranscript(state.lines.reverse(), opts); // Oldest first.
|
|
248
|
+
if (entries.length < limit && state.stoppedAtBoundary) {
|
|
206
249
|
// Contentless assistant turns made the rendered entries fewer than the candidate count
|
|
207
250
|
// promised — re-read the whole file (the pre-optimization behavior) so slice(-limit) is
|
|
208
251
|
// exact.
|
|
209
|
-
entries = formatTranscript(readCompleteLines(file, 0, size).lines, opts);
|
|
252
|
+
entries = formatTranscript(readCompleteLines(file, 0, state.size).lines, opts);
|
|
210
253
|
}
|
|
211
254
|
// limit can be 0 (or negative — nonsense a caller should never pass): entries.slice(-0) is
|
|
212
255
|
// entries.slice(0) — the whole window — because -0 === 0, so a bare slice(-limit) would
|
|
213
256
|
// return everything instead of nothing; guard it, the same contract readTranscript's take
|
|
214
257
|
// guard pins (the last `limit` entries of a zero-sized window is the empty list).
|
|
215
258
|
const kept = limit > 0 ? entries.slice(-limit) : [];
|
|
216
|
-
return { entries: kept, end };
|
|
259
|
+
return { entries: kept, end: state.end };
|
|
217
260
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answeredQuestionEntries, donePlanEntries, entryMatches, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries, } from "../../backlog/backlog.js";
|
|
2
2
|
import { backlogHolds, backlogInProgress, backlogMarks, } from "../../backlog/backlog-eligibility.js";
|
|
3
|
-
import { groupBlockedSeries, partsLabel } from "../../backlog/backlog-
|
|
3
|
+
import { groupBlockedSeries, partsLabel } from "../../backlog/backlog-index-data.js";
|
|
4
4
|
import { clipToWidth } from "../../text/text-width.js";
|
|
5
5
|
import { DASHBOARD_ANSWERED_QUESTIONS, DASHBOARD_RECENT_FINISHED } from "../status-payload.js";
|
|
6
6
|
/** Pure project-status pane logic for the TUI (src/ui/tui/tui.tsx): the three-section backlog body
|
|
@@ -16,14 +16,13 @@ export function createAnswerEditor(session) {
|
|
|
16
16
|
session.held.answer = null;
|
|
17
17
|
restoreDraft();
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
|
|
20
|
+
// Enter records the decision through tryAnswerQuestion, the core `tumwater questions
|
|
21
|
+
// answer` and the dashboard's form share. The position is the question's 1-based index
|
|
22
|
+
// among the open questions: the browsable list puts plans, then bugs, then questions first.
|
|
23
|
+
const answerOpenEdit = (ctx) => {
|
|
24
|
+
if (!session.anyEditorHeld() &&
|
|
22
25
|
ctx.view === ctx.roleIds.length + 1 && ctx.selectedEntry !== null) {
|
|
23
|
-
// Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
|
|
24
|
-
// Enter records the decision through tryAnswerQuestion, the core `tumwater questions
|
|
25
|
-
// answer` and the dashboard's form share. The position is the question's 1-based index
|
|
26
|
-
// among the open questions: the browsable list puts plans, then bugs, then questions first.
|
|
27
26
|
try {
|
|
28
27
|
const entries = session.flat();
|
|
29
28
|
const sel = Math.min(ctx.selectedEntry, entries.length - 1);
|
|
@@ -46,47 +45,58 @@ export function createAnswerEditor(session) {
|
|
|
46
45
|
}
|
|
47
46
|
return true;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
// Esc cancels the answer editor, restoring the previous prompt text; nothing is written.
|
|
51
|
+
const cancelAnswerEdit = () => {
|
|
52
|
+
exitAnswerMode();
|
|
53
|
+
session.flashMessage("answer cancelled");
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
// Enter records the answer through tryAnswerQuestion, titled so a question that moved
|
|
57
|
+
// since Ctrl+A is refused. A whitespace-only line, an unclosed fence, and a throw all
|
|
58
|
+
// flash and STAY in the editor so the text survives; answered and missing/moved both
|
|
59
|
+
// leave the editor and return the pane to list mode (the entry left the list).
|
|
60
|
+
const commitAnswerEdit = () => {
|
|
61
|
+
const target = answerFor;
|
|
62
|
+
if (target === null)
|
|
63
|
+
return false;
|
|
64
|
+
if (!session.input.trim()) {
|
|
65
|
+
session.flashMessage("questions answer needs a decision");
|
|
53
66
|
return true;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!session.input.trim()) {
|
|
62
|
-
session.flashMessage("questions answer needs a decision");
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
let outcome;
|
|
66
|
-
try {
|
|
67
|
-
outcome = tryAnswerQuestion(session.root, target.position, session.input, target.title);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
session.flashError(err);
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
if (outcome.status === "fenced") {
|
|
74
|
-
session.flashMessage(UNCLOSED_FENCE_MESSAGE);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
let woke = "";
|
|
78
|
-
if (outcome.status === "answered") {
|
|
79
|
-
session.commitBacklogEdit("QUESTIONS.md", `Answer question: ${outcome.title}`);
|
|
80
|
-
if (wakeQuestionAsker(session.root, outcome.title) !== null)
|
|
81
|
-
woke = ` · woke ${questionAsker(outcome.title)}`;
|
|
82
|
-
}
|
|
83
|
-
exitAnswerMode();
|
|
84
|
-
session.clearBacklogSelection();
|
|
85
|
-
session.flashMessage(outcome.status === "answered"
|
|
86
|
-
? `answered question #${target.position}: ${outcome.title}${woke}`
|
|
87
|
-
: "that question was already answered or moved — nothing recorded");
|
|
68
|
+
let outcome;
|
|
69
|
+
try {
|
|
70
|
+
outcome = tryAnswerQuestion(session.root, target.position, session.input, target.title);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
session.flashError(err);
|
|
88
74
|
return true;
|
|
89
75
|
}
|
|
76
|
+
if (outcome.status === "fenced") {
|
|
77
|
+
session.flashMessage(UNCLOSED_FENCE_MESSAGE);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
let woke = "";
|
|
81
|
+
if (outcome.status === "answered") {
|
|
82
|
+
session.commitBacklogEdit("QUESTIONS.md", `Answer question: ${outcome.title}`);
|
|
83
|
+
if (wakeQuestionAsker(session.root, outcome.title) !== null)
|
|
84
|
+
woke = ` · woke ${questionAsker(outcome.title)}`;
|
|
85
|
+
}
|
|
86
|
+
exitAnswerMode();
|
|
87
|
+
session.clearBacklogSelection();
|
|
88
|
+
session.flashMessage(outcome.status === "answered"
|
|
89
|
+
? `answered question #${target.position}: ${outcome.title}${woke}`
|
|
90
|
+
: "that question was already answered or moved — nothing recorded");
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
const handleKey = (_str, key, ctx) => {
|
|
94
|
+
if (key.ctrl && key.name === "a")
|
|
95
|
+
return answerOpenEdit(ctx);
|
|
96
|
+
if (key.name === "escape" && answerFor)
|
|
97
|
+
return cancelAnswerEdit();
|
|
98
|
+
if (key.name === "return" && answerFor)
|
|
99
|
+
return commitAnswerEdit();
|
|
90
100
|
return false;
|
|
91
101
|
};
|
|
92
102
|
return { handleKey, exit: exitAnswerMode };
|
|
@@ -23,76 +23,87 @@ export function createAttachEditor(session) {
|
|
|
23
23
|
session.held.attach = false;
|
|
24
24
|
restoreDraft();
|
|
25
25
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
// Mid-recall, hand the arrows' saved draft back before the attach editor takes the
|
|
41
|
-
// line — the same settle the role editors do — so leaving restores the
|
|
42
|
-
// draft, not a history entry, with no stale recall state riding along.
|
|
43
|
-
restoreDraft = session.takeDraft();
|
|
44
|
-
attachMode = true;
|
|
45
|
-
session.held.attach = true;
|
|
46
|
-
session.input = "";
|
|
47
|
-
session.cursor = 0;
|
|
48
|
-
session.flashMessage("attach an image path — Enter to attach, Esc to cancel");
|
|
49
|
-
}
|
|
26
|
+
// Ctrl+V toggles image-attach mode on the Activity view's prompt line (mnemonic: attach
|
|
27
|
+
// **v**ia the same key the reset-counters and move-to-top arms use on their own views):
|
|
28
|
+
// the line edits one image path that rides into the next director prompt as the same
|
|
29
|
+
// `[image attached: …]` `tumwater prompt --attach <path>` and the dashboard composer
|
|
30
|
+
// write. Entering saves the director draft and blanks the line; leaving restores it.
|
|
31
|
+
// Guarded to the Activity view (`ctx.view === 0`), so the Transcript reset arm, the
|
|
32
|
+
// Backlog move-to-top arm, and the Queued pane's move-to-top arm keep `Ctrl+V` on their
|
|
33
|
+
// views: a guard miss returns false and falls through the dispatch.
|
|
34
|
+
const toggleAttachEdit = (ctx) => {
|
|
35
|
+
if (ctx.view !== 0)
|
|
36
|
+
return false;
|
|
37
|
+
if (session.editorEntryDenied("attach"))
|
|
50
38
|
return true;
|
|
39
|
+
if (attachMode) {
|
|
40
|
+
exitAttachMode();
|
|
41
|
+
session.flashMessage("image attach cancelled");
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
43
|
+
else {
|
|
44
|
+
// Mid-recall, hand the arrows' saved draft back before the attach editor takes the
|
|
45
|
+
// line — the same settle the role editors do — so leaving restores the
|
|
46
|
+
// draft, not a history entry, with no stale recall state riding along.
|
|
47
|
+
restoreDraft = session.takeDraft();
|
|
48
|
+
attachMode = true;
|
|
49
|
+
session.held.attach = true;
|
|
50
|
+
session.input = "";
|
|
51
|
+
session.cursor = 0;
|
|
52
|
+
session.flashMessage("attach an image path — Enter to attach, Esc to cancel");
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
// Esc cancels the attach editor, restoring the previous prompt text and leaving any
|
|
57
|
+
// already-saved pending images untouched.
|
|
58
|
+
const cancelAttachEdit = () => {
|
|
59
|
+
exitAttachMode();
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
// Enter in attach mode reads the typed path as one image through the same rules
|
|
63
|
+
// `tumwater prompt --attach` uses (readPromptImageFiles), refuses a path no configured
|
|
64
|
+
// director seam can read up front, and stores it as a pending attachment on the session
|
|
65
|
+
// so the next director prompt carries it. Empty clears the pending images; a failed read
|
|
66
|
+
// or a refusal flashes and STAYS in the editor so the operator can fix the path. Either
|
|
67
|
+
// way the saved draft is restored.
|
|
68
|
+
const commitAttachEdit = () => {
|
|
69
|
+
const p = session.input.trim();
|
|
70
|
+
if (p === "") {
|
|
71
|
+
session.images = [];
|
|
55
72
|
exitAttachMode();
|
|
73
|
+
session.flashMessage("image attach cleared");
|
|
56
74
|
return true;
|
|
57
75
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// way the saved draft is restored.
|
|
65
|
-
const p = session.input.trim();
|
|
66
|
-
if (p === "") {
|
|
67
|
-
session.images = [];
|
|
68
|
-
exitAttachMode();
|
|
69
|
-
session.flashMessage("image attach cleared");
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
let readImages;
|
|
73
|
-
try {
|
|
74
|
-
readImages = readPromptImageFiles([p]);
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
session.flashError(err);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
const guidance = imagePromptGuidance(session.root, "director");
|
|
81
|
-
if (guidance.refusal) {
|
|
82
|
-
session.flashMessage(guidance.refusal);
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
const next = [...session.images, ...readImages];
|
|
86
|
-
const countProblem = promptImagesCountProblem(next.length);
|
|
87
|
-
if (countProblem !== null) {
|
|
88
|
-
session.flashMessage(countProblem);
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
session.images = next;
|
|
92
|
-
exitAttachMode();
|
|
93
|
-
session.flashMessage(guidance.notice ?? `attached ${path.basename(p)}`);
|
|
76
|
+
let readImages;
|
|
77
|
+
try {
|
|
78
|
+
readImages = readPromptImageFiles([p]);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
session.flashError(err);
|
|
94
82
|
return true;
|
|
95
83
|
}
|
|
84
|
+
const guidance = imagePromptGuidance(session.root, "director");
|
|
85
|
+
if (guidance.refusal) {
|
|
86
|
+
session.flashMessage(guidance.refusal);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
const next = [...session.images, ...readImages];
|
|
90
|
+
const countProblem = promptImagesCountProblem(next.length);
|
|
91
|
+
if (countProblem !== null) {
|
|
92
|
+
session.flashMessage(countProblem);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
session.images = next;
|
|
96
|
+
exitAttachMode();
|
|
97
|
+
session.flashMessage(guidance.notice ?? `attached ${path.basename(p)}`);
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
const handleKey = (_str, key, ctx) => {
|
|
101
|
+
if (key.ctrl && key.name === "v")
|
|
102
|
+
return toggleAttachEdit(ctx);
|
|
103
|
+
if (key.name === "escape" && attachMode)
|
|
104
|
+
return cancelAttachEdit();
|
|
105
|
+
if (key.name === "return" && attachMode)
|
|
106
|
+
return commitAttachEdit();
|
|
96
107
|
return false;
|
|
97
108
|
};
|
|
98
109
|
return { handleKey, exit: exitAttachMode };
|