@songsid/agend 2.2.0-beta.5 → 2.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +6 -2
- package/dist/agent-cli.js +9 -5
- package/dist/agent-cli.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +16 -0
- package/dist/backend/claude-code.js +92 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +7 -0
- package/dist/backend/credential-profile.js +7 -1
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/kiro.js +5 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +67 -0
- package/dist/backend/muse.js +370 -44
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/channel/adapters/discord.js +12 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +4 -5
- package/dist/channel/adapters/telegram.js +39 -17
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/types.d.ts +3 -0
- package/dist/cli.js +25 -10
- package/dist/cli.js.map +1 -1
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +165 -54
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +27 -0
- package/dist/delivery-outbox.js +175 -6
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +4 -1
- package/dist/fleet-manager.d.ts +110 -6
- package/dist/fleet-manager.js +558 -100
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +44 -9
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +14 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +22 -2
- package/dist/locale.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/outbound-handlers.d.ts +10 -0
- package/dist/outbound-handlers.js +39 -14
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +15 -13
- package/dist/outbound-schemas.js +5 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +2 -1
- package/dist/public-preview.js +8 -5
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-link.d.ts +4 -1
- package/dist/public-web-link.js +27 -10
- package/dist/public-web-link.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +3 -0
- package/dist/reply-buttons-controller.js +3 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/restart-progress.d.ts +4 -0
- package/dist/restart-progress.js +7 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/service-installer.d.ts +31 -0
- package/dist/service-installer.js +41 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -2
- package/dist/topic-commands.js +17 -5
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/ui/chat-strings.js +8 -2
- package/dist/ui/chat-thread.js +24 -4
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/preview-panel.js +7 -1
- package/dist/ui/preview.js +5 -1
- package/dist/ui/shared/app-stream.js +21 -4
- package/dist/ui/shared/app.css +9 -3
- package/dist/ui/shared/usage-dialog.js +10 -1
- package/dist/update-install.d.ts +6 -2
- package/dist/update-install.js +8 -2
- package/dist/update-install.js.map +1 -1
- package/dist/update-marker.d.ts +11 -0
- package/dist/update-marker.js +12 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/web-api.d.ts +5 -1
- package/dist/web-api.js +8 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +2 -2
- package/dist/web-commands.js +3 -2
- package/dist/web-commands.js.map +1 -1
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/package.json +1 -1
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/backend/muse.d.ts
CHANGED
|
@@ -15,6 +15,15 @@ export declare function museSessionCwd(head: string): string | null;
|
|
|
15
15
|
* readers — never on the fleet event loop.
|
|
16
16
|
*/
|
|
17
17
|
export declare function readFileHeadSync(filePath: string, maxChars: number): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* #1717: true when the pane shows muse's terminals panel with at least one
|
|
20
|
+
* running task — the "local work still active" state. Anchored to the last
|
|
21
|
+
* separator, where the live panel sits under the input box: a transcript
|
|
22
|
+
* quoting an old panel above a separator never counts. The idle proof checks
|
|
23
|
+
* the same shape positionally via museStatusFooter; this boolean feeds the
|
|
24
|
+
* pause gate below.
|
|
25
|
+
*/
|
|
26
|
+
export declare function museBackgroundWorkActive(pane: string): boolean;
|
|
18
27
|
/**
|
|
19
28
|
* The rows of muse's live input box, or null when the bottom of the pane is not
|
|
20
29
|
* muse's layout (#829). The box is the LAST thing above the status bar:
|
|
@@ -50,6 +59,55 @@ export declare function museDraftShows(rows: readonly string[], width: number, t
|
|
|
50
59
|
* glyph allowed in front of a choice.
|
|
51
60
|
*/
|
|
52
61
|
export declare function museLoginScreenActive(pane: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* #1716: `/quit` while a background task runs is answered with a confirmation
|
|
64
|
+
* instead of an exit (live 1.4.6,
|
|
65
|
+
* tests/fixtures/muse-1.4.6/prod-quit-with-local-work.pane.txt):
|
|
66
|
+
* ❯
|
|
67
|
+
* Local work is still active
|
|
68
|
+
* The following will stop when you exit:
|
|
69
|
+
* background task · tool.bash
|
|
70
|
+
* ❯ 1. Exit anyway
|
|
71
|
+
* 2. Stay
|
|
72
|
+
* Enter to confirm · Esc to cancel
|
|
73
|
+
* ──────────
|
|
74
|
+
* main · ↓ to select
|
|
75
|
+
* └ ◇ Wait in the background running 15s
|
|
76
|
+
* mock-model · high · ~/work · Launch overrides
|
|
77
|
+
* Unlike Claude's prompt this menu is NOT bottom-anchored — the terminals
|
|
78
|
+
* panel sits under it — so the match is anchored to the hint row instead:
|
|
79
|
+
* the header must be within 12 rows above it with both options in between.
|
|
80
|
+
* Enter would take the selected option ("Exit anyway"), so like #1217 this is
|
|
81
|
+
* recognised whole and never answered here: the stop flow cancels it.
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* #1715: with `skip_permissions: false` muse asks before running a command
|
|
85
|
+
* needing approval (live 1.4.6,
|
|
86
|
+
* tests/fixtures/muse-1.4.6/approval-escalated.pane.txt and
|
|
87
|
+
* approval-untrusted-staged.pane.txt):
|
|
88
|
+
* Would you like to run the following command?
|
|
89
|
+
* $ echo perm-check
|
|
90
|
+
* › 1. Yes, proceed (y)
|
|
91
|
+
* 2. No, and tell Muse Code what to do differently (esc)
|
|
92
|
+
* Change approval defaults in /permissions
|
|
93
|
+
* `--approval-mode untrusted` (new in 1.4.6) stages the command first
|
|
94
|
+
* (`Stage 1/1`, `Current argv: [...]`) and offers three options (allow once /
|
|
95
|
+
* always allow in this workspace / abort). 1.4.4 shows the same prompt with a
|
|
96
|
+
* `›` marker and no footer line. Enter would take the preselected allow
|
|
97
|
+
* option, so like #1716 this is recognised whole and never answered here.
|
|
98
|
+
*/
|
|
99
|
+
export declare const MUSE_APPROVAL_PROMPT: RegExp;
|
|
100
|
+
/**
|
|
101
|
+
* #1715: which request the approval prompt is showing — its `$ command` row.
|
|
102
|
+
* Stable across the `Calling tools (Ns)` timer redraws, so the episode can
|
|
103
|
+
* confirm; a whole-pane hash never would. Derived from the SAME selected
|
|
104
|
+
* block as the predicate (#1715 r2): a scrollback quote's command must never
|
|
105
|
+
* become the current request's identity.
|
|
106
|
+
*/
|
|
107
|
+
export declare function museApprovalRequestIdentity(pane: string): string | null;
|
|
108
|
+
export declare function museApprovalPromptActive(pane: string): boolean;
|
|
109
|
+
export declare const MUSE_LOCAL_WORK_QUIT: RegExp;
|
|
110
|
+
export declare function museLocalWorkQuitActive(pane: string): boolean;
|
|
53
111
|
/**
|
|
54
112
|
* Meta Muse Code — the `muse` CLI (v1.3.0 verified).
|
|
55
113
|
*
|
|
@@ -169,6 +227,15 @@ export declare class MuseBackend implements CliBackend {
|
|
|
169
227
|
getQuitCommand(): string | null;
|
|
170
228
|
getQuitKey(): string;
|
|
171
229
|
getQuitKeyPresses(): number;
|
|
230
|
+
/** #1716: `/quit` with a background task running stops at the "Local work is still active" menu. */
|
|
231
|
+
quitBlockedByDialog(pane: string): boolean;
|
|
232
|
+
/**
|
|
233
|
+
* #1717: the terminals panel with a `running` task — the pause flow defers
|
|
234
|
+
* an idle pause while this holds, so an AgEnD-initiated silent pause never
|
|
235
|
+
* kills the user's running background work. Explicit stop/restart still
|
|
236
|
+
* proceed via quitBlockedByDialog above.
|
|
237
|
+
*/
|
|
238
|
+
backgroundWorkRunning(pane: string): boolean;
|
|
172
239
|
getCompactCommand(): string;
|
|
173
240
|
getClearCommand(): string | null;
|
|
174
241
|
/**
|
package/dist/backend/muse.js
CHANGED
|
@@ -90,6 +90,27 @@ const MUSE_STATUS_BAR = /^\s{1,4}[^\s·]+(?:\s+·\s+[^·\s][^·]*)+$/;
|
|
|
90
90
|
const MUSE_DIALOG_CHROME = /Allow this (?:tool|command)|Approve this (?:tool|command)|^\s*[>❯]?\s*1\s+(?:Allow|Approve|Trust)\b|Use Up\/Down|Esc (?:quits|to cancel)|↓↑ to select|·\s*Enter to choose\b/i;
|
|
91
91
|
/** The status bar is one row; a long cwd in a narrow pane wraps it onto a few more. */
|
|
92
92
|
const MUSE_MAX_STATUS_ROWS = 4;
|
|
93
|
+
/**
|
|
94
|
+
* #1717: extended bottom-region depth, but ONLY for a terminals-panel-headed
|
|
95
|
+
* block (header + running tasks + bar). A long wrapped bar still fails closed
|
|
96
|
+
* past MUSE_MAX_STATUS_ROWS (#958): the extension below accepts a deeper
|
|
97
|
+
* separator only when the row right after it is the panel header.
|
|
98
|
+
*/
|
|
99
|
+
const MUSE_MAX_PANEL_DEPTH = 8;
|
|
100
|
+
/**
|
|
101
|
+
* #1717: while a background task runs, muse draws a terminals panel between
|
|
102
|
+
* the input box's bottom separator and the status bar (live 1.4.6,
|
|
103
|
+
* tests/fixtures/muse-1.4.6/prod-background-work.pane.txt):
|
|
104
|
+
* main · ↓ to select
|
|
105
|
+
* └ ◆ Wait in the background running 11s
|
|
106
|
+
* The header ends in `↓ to select` — note the single arrow, which is NOT the
|
|
107
|
+
* dialog chrome's `↓↑ to select`. The timer redraws every second, which is
|
|
108
|
+
* exactly the periodic-redraw case. The composer above stays free, so the
|
|
109
|
+
* panel must not fail the idle proof — but only this shape passes: anything
|
|
110
|
+
* else under the separator still fails closed.
|
|
111
|
+
*/
|
|
112
|
+
const MUSE_TERMINALS_HEADER = /^.+· ↓ to select\s*$/;
|
|
113
|
+
const MUSE_TERMINAL_TASK_ROW = /^[└├]\s+.*\brunning\b\s+(?:\d+(?:\.\d+)?[hms]\s*)+\s*$/;
|
|
93
114
|
/** The working line's timer: `◇ Thinking (2s · esc to interrupt)`. */
|
|
94
115
|
const MUSE_BUSY = /\(\s*(?:\d+(?:\.\d+)?[hms]\s*)+·\s*esc to interrupt\s*\)/;
|
|
95
116
|
const MUSE_MAX_INPUT_ROWS = 200;
|
|
@@ -98,10 +119,30 @@ const MUSE_MAX_INPUT_ROWS = 200;
|
|
|
98
119
|
* first row in the bar's grammar, the rest a wrapped continuation of it. A
|
|
99
120
|
* picker, a dialog, a busy line or a second separator there means the box above
|
|
100
121
|
* is not the live input (#829 review: a chooser under an old box).
|
|
122
|
+
*
|
|
123
|
+
* #1717: a terminals panel may sit between the separator and the bar (a
|
|
124
|
+
* `· ↓ to select` header plus `running` task rows). It is stripped first and
|
|
125
|
+
* each of its rows must match the panel grammar — a header with no task row,
|
|
126
|
+
* or any other shape, still fails closed.
|
|
101
127
|
*/
|
|
102
128
|
function museStatusFooter(rows, bottomSeparator, busy) {
|
|
103
|
-
|
|
104
|
-
if (status.length === 0
|
|
129
|
+
let status = rows.slice(bottomSeparator + 1);
|
|
130
|
+
if (status.length === 0)
|
|
131
|
+
return false;
|
|
132
|
+
if (MUSE_TERMINALS_HEADER.test(status[0])) {
|
|
133
|
+
let end = 1;
|
|
134
|
+
while (end < status.length && MUSE_TERMINAL_TASK_ROW.test(status[end]))
|
|
135
|
+
end++;
|
|
136
|
+
if (end === 1)
|
|
137
|
+
return false;
|
|
138
|
+
const panel = status.slice(0, end);
|
|
139
|
+
if (!panel.every(row => !MUSE_SEPARATOR.test(row.trim()) && !busy.test(row) && !MUSE_DIALOG_CHROME.test(row)))
|
|
140
|
+
return false;
|
|
141
|
+
status = status.slice(end);
|
|
142
|
+
if (status.length === 0)
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (!MUSE_STATUS_BAR.test(status[0]))
|
|
105
146
|
return false;
|
|
106
147
|
if (!status.every(row => /^\s+\S/.test(row) && !MUSE_SEPARATOR.test(row.trim()) && !busy.test(row) && !MUSE_DIALOG_CHROME.test(row)))
|
|
107
148
|
return false;
|
|
@@ -109,6 +150,85 @@ function museStatusFooter(rows, bottomSeparator, busy) {
|
|
|
109
150
|
// selection cursor under the bar is a picker, not the bar.
|
|
110
151
|
return !status.slice(1).some(row => /^\s*[>❯›]?\s*\d+[.)]?\s+\S/.test(row) || /^\s*[>›]/.test(row));
|
|
111
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* The structural core of the idle proof, shared with the quit-menu matcher
|
|
155
|
+
* (#1730 P2-1): true when the CURRENT screen — anchored to the bottom
|
|
156
|
+
* separator — shows the live, empty input box. A transcript quoting an older
|
|
157
|
+
* screen above never satisfies it.
|
|
158
|
+
*/
|
|
159
|
+
function museFreeComposerLayout(rows, busyPattern) {
|
|
160
|
+
// The approval prompt is auto-answered rather than input-blocking, so the
|
|
161
|
+
// idle proof itself must refuse a frame that shows dialog chrome.
|
|
162
|
+
const dialogChrome = MUSE_DIALOG_CHROME;
|
|
163
|
+
// Everything after the bottom separator must be the status bar (#1717:
|
|
164
|
+
// optionally a terminals panel first).
|
|
165
|
+
const bottomSeparator = museBottomSeparator(rows);
|
|
166
|
+
if (bottomSeparator < 2)
|
|
167
|
+
return false;
|
|
168
|
+
if (!museStatusFooter(rows, bottomSeparator, busyPattern))
|
|
169
|
+
return false;
|
|
170
|
+
// Directly above it, the EMPTY live prompt. A `❯` with text is a draft
|
|
171
|
+
// (or, higher up, a transcript echo) and never proves idle.
|
|
172
|
+
if (!/^❯\s*$/.test(rows[bottomSeparator - 1]))
|
|
173
|
+
return false;
|
|
174
|
+
const topSeparator = bottomSeparator - 2;
|
|
175
|
+
if (!MUSE_SEPARATOR.test(rows[topSeparator]))
|
|
176
|
+
return false;
|
|
177
|
+
// No busy indicator and no dialog just above the input box. The working
|
|
178
|
+
// line (`◇ Thinking (2s · esc to interrupt)`) sits immediately above the
|
|
179
|
+
// top separator; check 8 rows up to be safe.
|
|
180
|
+
const above = rows.slice(Math.max(0, topSeparator - 8), topSeparator);
|
|
181
|
+
if (above.some(r => busyPattern.test(r) || dialogChrome.test(r)))
|
|
182
|
+
return false;
|
|
183
|
+
// Arrow-key pickers (`/model`) are refused by their component chrome
|
|
184
|
+
// above, not by guessing from answer text that happens to list model ids.
|
|
185
|
+
// A `›` selection cursor is refused too; `❯` is not a cursor here — muse
|
|
186
|
+
// echoes submitted messages with it.
|
|
187
|
+
return !above.some(r => /^\s*›\s+\S/.test(r));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Nearest bottom separator, with the #1717 panel extension: the plain search
|
|
191
|
+
* stays capped at MUSE_MAX_STATUS_ROWS (a 5-row wrapped bar still fails
|
|
192
|
+
* closed, #958); a deeper separator is accepted only when the row right
|
|
193
|
+
* after it is the terminals-panel header. Nearest-first, so a stale
|
|
194
|
+
* separator in scrollback can never win over the live one.
|
|
195
|
+
*/
|
|
196
|
+
function museBottomSeparator(rows) {
|
|
197
|
+
for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
|
|
198
|
+
if (MUSE_SEPARATOR.test(rows[i]))
|
|
199
|
+
return i;
|
|
200
|
+
}
|
|
201
|
+
for (let i = Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS) - 1; i >= Math.max(0, rows.length - 1 - MUSE_MAX_PANEL_DEPTH); i--) {
|
|
202
|
+
if (MUSE_SEPARATOR.test(rows[i])) {
|
|
203
|
+
return MUSE_TERMINALS_HEADER.test(rows[i + 1] ?? "") ? i : -1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* #1717: true when the pane shows muse's terminals panel with at least one
|
|
210
|
+
* running task — the "local work still active" state. Anchored to the last
|
|
211
|
+
* separator, where the live panel sits under the input box: a transcript
|
|
212
|
+
* quoting an old panel above a separator never counts. The idle proof checks
|
|
213
|
+
* the same shape positionally via museStatusFooter; this boolean feeds the
|
|
214
|
+
* pause gate below.
|
|
215
|
+
*/
|
|
216
|
+
export function museBackgroundWorkActive(pane) {
|
|
217
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
218
|
+
while (rows.length && !rows[rows.length - 1].trim())
|
|
219
|
+
rows.pop();
|
|
220
|
+
let sep = -1;
|
|
221
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
222
|
+
if (MUSE_SEPARATOR.test(rows[i])) {
|
|
223
|
+
sep = i;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (sep < 0)
|
|
228
|
+
return false;
|
|
229
|
+
const after = rows.slice(sep + 1);
|
|
230
|
+
return after.length > 1 && MUSE_TERMINALS_HEADER.test(after[0]) && MUSE_TERMINAL_TASK_ROW.test(after[1]);
|
|
231
|
+
}
|
|
112
232
|
/**
|
|
113
233
|
* The rows of muse's live input box, or null when the bottom of the pane is not
|
|
114
234
|
* muse's layout (#829). The box is the LAST thing above the status bar:
|
|
@@ -127,13 +247,7 @@ export function museInputBox(pane, busy = MUSE_BUSY) {
|
|
|
127
247
|
const rows = pane.replace(/\r/g, "").split("\n");
|
|
128
248
|
while (rows.length && !rows[rows.length - 1].trim())
|
|
129
249
|
rows.pop();
|
|
130
|
-
|
|
131
|
-
for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
|
|
132
|
-
if (MUSE_SEPARATOR.test(rows[i])) {
|
|
133
|
-
bottom = i;
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
250
|
+
const bottom = museBottomSeparator(rows);
|
|
137
251
|
if (bottom < 2 || !museStatusFooter(rows, bottom, busy))
|
|
138
252
|
return null;
|
|
139
253
|
for (let top = bottom - 1, n = 0; top >= 1 && n < MUSE_MAX_INPUT_ROWS; top--, n++) {
|
|
@@ -234,6 +348,214 @@ const MUSE_LOGIN_MENU = {
|
|
|
234
348
|
inputBlocked: true,
|
|
235
349
|
description: "Muse login menu — waiting for a human to log in (never answered)",
|
|
236
350
|
};
|
|
351
|
+
/**
|
|
352
|
+
* #1716: `/quit` while a background task runs is answered with a confirmation
|
|
353
|
+
* instead of an exit (live 1.4.6,
|
|
354
|
+
* tests/fixtures/muse-1.4.6/prod-quit-with-local-work.pane.txt):
|
|
355
|
+
* ❯
|
|
356
|
+
* Local work is still active
|
|
357
|
+
* The following will stop when you exit:
|
|
358
|
+
* background task · tool.bash
|
|
359
|
+
* ❯ 1. Exit anyway
|
|
360
|
+
* 2. Stay
|
|
361
|
+
* Enter to confirm · Esc to cancel
|
|
362
|
+
* ──────────
|
|
363
|
+
* main · ↓ to select
|
|
364
|
+
* └ ◇ Wait in the background running 15s
|
|
365
|
+
* mock-model · high · ~/work · Launch overrides
|
|
366
|
+
* Unlike Claude's prompt this menu is NOT bottom-anchored — the terminals
|
|
367
|
+
* panel sits under it — so the match is anchored to the hint row instead:
|
|
368
|
+
* the header must be within 12 rows above it with both options in between.
|
|
369
|
+
* Enter would take the selected option ("Exit anyway"), so like #1217 this is
|
|
370
|
+
* recognised whole and never answered here: the stop flow cancels it.
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* #1715: with `skip_permissions: false` muse asks before running a command
|
|
374
|
+
* needing approval (live 1.4.6,
|
|
375
|
+
* tests/fixtures/muse-1.4.6/approval-escalated.pane.txt and
|
|
376
|
+
* approval-untrusted-staged.pane.txt):
|
|
377
|
+
* Would you like to run the following command?
|
|
378
|
+
* $ echo perm-check
|
|
379
|
+
* › 1. Yes, proceed (y)
|
|
380
|
+
* 2. No, and tell Muse Code what to do differently (esc)
|
|
381
|
+
* Change approval defaults in /permissions
|
|
382
|
+
* `--approval-mode untrusted` (new in 1.4.6) stages the command first
|
|
383
|
+
* (`Stage 1/1`, `Current argv: [...]`) and offers three options (allow once /
|
|
384
|
+
* always allow in this workspace / abort). 1.4.4 shows the same prompt with a
|
|
385
|
+
* `›` marker and no footer line. Enter would take the preselected allow
|
|
386
|
+
* option, so like #1716 this is recognised whole and never answered here.
|
|
387
|
+
*/
|
|
388
|
+
export const MUSE_APPROVAL_PROMPT = /^[ \t]*Would you like to run the following command\?[ \t]*$/m;
|
|
389
|
+
const MUSE_APPROVAL_STAGE = /^[ \t]*Stage \d+\/\d+[ \t]*$/;
|
|
390
|
+
const MUSE_APPROVAL_ARGV = /^[ \t]*Current argv: \[.*\][ \t]*$/;
|
|
391
|
+
const MUSE_APPROVAL_OPT = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]+/;
|
|
392
|
+
const MUSE_APPROVAL_FOOTER = /^[ \t]*Change approval defaults in \/permissions[ \t]*$/;
|
|
393
|
+
/**
|
|
394
|
+
* #1715: which request the approval prompt is showing — its `$ command` row.
|
|
395
|
+
* Stable across the `Calling tools (Ns)` timer redraws, so the episode can
|
|
396
|
+
* confirm; a whole-pane hash never would. Derived from the SAME selected
|
|
397
|
+
* block as the predicate (#1715 r2): a scrollback quote's command must never
|
|
398
|
+
* become the current request's identity.
|
|
399
|
+
*/
|
|
400
|
+
export function museApprovalRequestIdentity(pane) {
|
|
401
|
+
const s = museApprovalScreen(pane);
|
|
402
|
+
if (s === null)
|
|
403
|
+
return null;
|
|
404
|
+
return museApprovalBlock(s.screen, s.top, s.bottom)?.command ?? null;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* #1715 r2: the selected live approval block — its header row index and `$`
|
|
408
|
+
* command text. Headers are tried newest-first inside the live input block so
|
|
409
|
+
* a scrollback quote never shadows the current prompt, and the identity comes
|
|
410
|
+
* from the SAME block the predicate accepts (never a separate global search).
|
|
411
|
+
*/
|
|
412
|
+
function museApprovalBlock(screen, top, bottom) {
|
|
413
|
+
const window = [];
|
|
414
|
+
for (let i = bottom - 1; i > top; i--) {
|
|
415
|
+
if (screen[i].trim())
|
|
416
|
+
window.unshift({ row: screen[i], index: i });
|
|
417
|
+
}
|
|
418
|
+
for (let h = window.length - 1; h >= 0; h--) {
|
|
419
|
+
if (!MUSE_APPROVAL_PROMPT.test(window[h].row))
|
|
420
|
+
continue;
|
|
421
|
+
let k = h + 1;
|
|
422
|
+
const commandRow = k < window.length ? window[k].row : "";
|
|
423
|
+
const command = /^[ \t]*\$ (.+?)[ \t]*$/.exec(commandRow)?.[1]?.trim();
|
|
424
|
+
if (!command)
|
|
425
|
+
continue;
|
|
426
|
+
k++;
|
|
427
|
+
if (k < window.length && MUSE_APPROVAL_STAGE.test(window[k].row))
|
|
428
|
+
k++;
|
|
429
|
+
if (k < window.length && MUSE_APPROVAL_ARGV.test(window[k].row))
|
|
430
|
+
k++;
|
|
431
|
+
const last = window[window.length - 1]?.row;
|
|
432
|
+
const options = (last !== undefined && MUSE_APPROVAL_FOOTER.test(last) ? window.slice(k, -1) : window.slice(k))
|
|
433
|
+
.map(entry => entry.row);
|
|
434
|
+
if (options.length !== 2 && options.length !== 3)
|
|
435
|
+
continue;
|
|
436
|
+
if (!options.every(row => MUSE_APPROVAL_OPT.test(row)))
|
|
437
|
+
continue;
|
|
438
|
+
const first = (options[0] ?? "").replace(/^[ \t]*[❯›]?[ \t]*/, "");
|
|
439
|
+
const second = options[1] ?? "";
|
|
440
|
+
if (options.length === 2) {
|
|
441
|
+
if (!/^1\. Yes, proceed \(y\)[ \t]*$/.test(first))
|
|
442
|
+
continue;
|
|
443
|
+
if (!/^[ \t]*[❯›]?[ \t]*2\. No, and tell Muse Code what to do differently \(esc\)[ \t]*$/.test(second))
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
if (!/^1\. Allow this stage once \(y\)[ \t]*$/.test(first))
|
|
448
|
+
continue;
|
|
449
|
+
if (!/^[ \t]*[❯›]?[ \t]*2\. Always allow in this workspace: .+ \(p\)[ \t]*$/.test(second))
|
|
450
|
+
continue;
|
|
451
|
+
if (!/^[ \t]*[❯›]?[ \t]*3\. Abort the entire command \(esc\)[ \t]*$/.test(options[2] ?? ""))
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
return { head: window[h].index, command };
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function museApprovalScreen(pane) {
|
|
459
|
+
const screen = pane.replace(/\r/g, "").split("\n");
|
|
460
|
+
while (screen.length && !screen[screen.length - 1].trim())
|
|
461
|
+
screen.pop();
|
|
462
|
+
const bottom = museBottomSeparator(screen);
|
|
463
|
+
if (bottom < 0)
|
|
464
|
+
return null;
|
|
465
|
+
// Same ownership as the quit menu (#1730 P2-1): the prompt must live in the
|
|
466
|
+
// current input block, and a free composer means any prompt text is history.
|
|
467
|
+
let top = -1;
|
|
468
|
+
for (let i = bottom - 1; i >= 0; i--) {
|
|
469
|
+
if (MUSE_SEPARATOR.test(screen[i])) {
|
|
470
|
+
top = i;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (museFreeComposerLayout(screen, MUSE_BUSY))
|
|
475
|
+
return null;
|
|
476
|
+
// #1715 r3: any readable composer means prompt text is a quote, not the
|
|
477
|
+
// live block — the live approval panes carry no ❯ composer row at all
|
|
478
|
+
// (inputDraft null), while a quoting draft does, even one starting with a
|
|
479
|
+
// blank first row. A null draft (no readable box) carries nothing to judge,
|
|
480
|
+
// so it still proceeds.
|
|
481
|
+
if (museInputBox(pane, MUSE_BUSY) !== null)
|
|
482
|
+
return null;
|
|
483
|
+
return { screen, top, bottom };
|
|
484
|
+
}
|
|
485
|
+
export function museApprovalPromptActive(pane) {
|
|
486
|
+
const s = museApprovalScreen(pane);
|
|
487
|
+
return s !== null && museApprovalBlock(s.screen, s.top, s.bottom) !== null;
|
|
488
|
+
}
|
|
489
|
+
export const MUSE_LOCAL_WORK_QUIT = /^[ \t]*Local work is still active[ \t]*$/m;
|
|
490
|
+
const MUSE_QUIT_HINT = /^[ \t]*Enter to confirm · Esc to cancel[ \t]*$/;
|
|
491
|
+
const MUSE_QUIT_OPT_1 = /^[ \t]*[❯›]?[ \t]*1\.[ \t]+Exit anyway[ \t]*$/m;
|
|
492
|
+
const MUSE_QUIT_OPT_2 = /^[ \t]*[❯›]?[ \t]*2\.[ \t]+Stay[ \t]*$/m;
|
|
493
|
+
export function museLocalWorkQuitActive(pane) {
|
|
494
|
+
const screen = pane.replace(/\r/g, "").split("\n");
|
|
495
|
+
while (screen.length && !screen[screen.length - 1].trim())
|
|
496
|
+
screen.pop();
|
|
497
|
+
const bottom = museBottomSeparator(screen);
|
|
498
|
+
if (bottom < 0)
|
|
499
|
+
return false;
|
|
500
|
+
// #1730 P2-1 (refined): the menu must belong to the CURRENT input block —
|
|
501
|
+
// between the bottom separator and the nearest separator above it. Menu
|
|
502
|
+
// text quoted in an older screen is history even when the current composer
|
|
503
|
+
// is busy with a draft, not just when it is free.
|
|
504
|
+
let top = -1;
|
|
505
|
+
for (let i = bottom - 1; i >= 0; i--) {
|
|
506
|
+
if (MUSE_SEPARATOR.test(screen[i])) {
|
|
507
|
+
top = i;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// A free composer on the current screen means any menu text is history.
|
|
512
|
+
if (museFreeComposerLayout(screen, MUSE_BUSY))
|
|
513
|
+
return false;
|
|
514
|
+
// #1730 P2-1 r3: a populated ❯ line means any menu text is a quote, not the
|
|
515
|
+
// live chrome. The menu only ever answers a submitted /quit, which clears
|
|
516
|
+
// the box — the live capture's ❯ row is empty with the menu as chrome under
|
|
517
|
+
// it — while every quoting draft (r2's "cancel me", r3a's hint-carrying
|
|
518
|
+
// draft, r3b's menu-quoting draft) holds text on the ❯ line itself. A null
|
|
519
|
+
// draft (no readable box) carries nothing to judge, so it still proceeds.
|
|
520
|
+
// Residual: an empty ❯ followed by a verbatim menu paste is byte-identical
|
|
521
|
+
// to the live chrome and cannot be separated here.
|
|
522
|
+
const draft = museInputBox(pane, MUSE_BUSY);
|
|
523
|
+
if (draft !== null && draft.rows.length > 0 && draft.rows[0].trim() !== "")
|
|
524
|
+
return false;
|
|
525
|
+
// Candidates newest-first, so a stale quote above never shadows the live
|
|
526
|
+
// block below (or passes as live itself).
|
|
527
|
+
for (let h = screen.length - 1; h >= 0; h--) {
|
|
528
|
+
if (!MUSE_QUIT_HINT.test(screen[h]))
|
|
529
|
+
continue;
|
|
530
|
+
if (!(h > top && h < bottom))
|
|
531
|
+
continue;
|
|
532
|
+
// The block above, skipping padding blanks (the same 12-non-blank
|
|
533
|
+
// window as before — the live menu fits in 9), with screen indices so the
|
|
534
|
+
// WHOLE block is fenced to the live input box, not just the hint: the
|
|
535
|
+
// backward scan must not borrow the header or options from above top.
|
|
536
|
+
const window = [];
|
|
537
|
+
for (let i = h - 1; i >= 0 && window.length < 12; i--) {
|
|
538
|
+
if (screen[i].trim())
|
|
539
|
+
window.unshift({ row: screen[i], index: i });
|
|
540
|
+
}
|
|
541
|
+
if (window.length < 4)
|
|
542
|
+
continue;
|
|
543
|
+
// #1730 P2-1 r4: headers newest-first, eligible only — a scrollback header
|
|
544
|
+
// above top must not shadow the live header inside the box. The first
|
|
545
|
+
// (oldest) match can sit above top while the current menu is intact below.
|
|
546
|
+
for (let head = window.length - 1; head >= 0; head--) {
|
|
547
|
+
if (!MUSE_LOCAL_WORK_QUIT.test(window[head].row))
|
|
548
|
+
continue;
|
|
549
|
+
if (window[head].index <= top)
|
|
550
|
+
continue;
|
|
551
|
+
const block = window.slice(head).map(entry => entry.row).join("\n");
|
|
552
|
+
if (!MUSE_QUIT_OPT_1.test(block) || !MUSE_QUIT_OPT_2.test(block))
|
|
553
|
+
continue;
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
237
559
|
/**
|
|
238
560
|
* Meta Muse Code — the `muse` CLI (v1.3.0 verified).
|
|
239
561
|
*
|
|
@@ -559,41 +881,7 @@ export class MuseBackend {
|
|
|
559
881
|
const rows = pane.replace(/\r/g, "").split("\n");
|
|
560
882
|
while (rows.length && !rows[rows.length - 1].trim())
|
|
561
883
|
rows.pop();
|
|
562
|
-
|
|
563
|
-
const busyPattern = this.getBusyPattern();
|
|
564
|
-
// The approval prompt is auto-answered rather than input-blocking, so the
|
|
565
|
-
// idle proof itself must refuse a frame that shows dialog chrome.
|
|
566
|
-
const dialogChrome = MUSE_DIALOG_CHROME;
|
|
567
|
-
// Everything after the bottom separator must be the status bar.
|
|
568
|
-
let bottomSeparator = -1;
|
|
569
|
-
for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
|
|
570
|
-
if (separator.test(rows[i])) {
|
|
571
|
-
bottomSeparator = i;
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (bottomSeparator < 2)
|
|
576
|
-
return false;
|
|
577
|
-
if (!museStatusFooter(rows, bottomSeparator, busyPattern))
|
|
578
|
-
return false;
|
|
579
|
-
// Directly above it, the EMPTY live prompt. A `❯` with text is a draft
|
|
580
|
-
// (or, higher up, a transcript echo) and never proves idle.
|
|
581
|
-
if (!/^❯\s*$/.test(rows[bottomSeparator - 1]))
|
|
582
|
-
return false;
|
|
583
|
-
const topSeparator = bottomSeparator - 2;
|
|
584
|
-
if (!separator.test(rows[topSeparator]))
|
|
585
|
-
return false;
|
|
586
|
-
// No busy indicator and no dialog just above the input box. The working
|
|
587
|
-
// line (`◇ Thinking (2s · esc to interrupt)`) sits immediately above the
|
|
588
|
-
// top separator; check 8 rows up to be safe.
|
|
589
|
-
const above = rows.slice(Math.max(0, topSeparator - 8), topSeparator);
|
|
590
|
-
if (above.some(r => busyPattern.test(r) || dialogChrome.test(r)))
|
|
591
|
-
return false;
|
|
592
|
-
// Arrow-key pickers (`/model`) are refused by their component chrome
|
|
593
|
-
// above, not by guessing from answer text that happens to list model ids.
|
|
594
|
-
// A `›` selection cursor is refused too; `❯` is not a cursor here — muse
|
|
595
|
-
// echoes submitted messages with it.
|
|
596
|
-
return !above.some(r => /^\s*›\s+\S/.test(r));
|
|
884
|
+
return museFreeComposerLayout(rows, this.getBusyPattern());
|
|
597
885
|
}
|
|
598
886
|
getErrorPatterns() {
|
|
599
887
|
// NOTE: an Escape-interrupted run prints "interrupting run" in the status
|
|
@@ -659,6 +947,31 @@ export class MuseBackend {
|
|
|
659
947
|
keys: ["1", "Enter"],
|
|
660
948
|
description: "Muse tool approval — allow",
|
|
661
949
|
},
|
|
950
|
+
{
|
|
951
|
+
// #1715: never answered here. Enter would take the preselected allow
|
|
952
|
+
// option, so the human must choose. The prompt replaces the input
|
|
953
|
+
// box (no draft to clear, unlike #829); deliveries simply hold.
|
|
954
|
+
pattern: MUSE_APPROVAL_PROMPT,
|
|
955
|
+
isActive: museApprovalPromptActive,
|
|
956
|
+
requestIdentity: museApprovalRequestIdentity,
|
|
957
|
+
interactionKind: "permission",
|
|
958
|
+
keys: [],
|
|
959
|
+
holdOnly: true,
|
|
960
|
+
blocksDelivery: true,
|
|
961
|
+
inputBlocked: true,
|
|
962
|
+
description: "Muse approval prompt — waiting for a human choice, never auto-approving",
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
// #1716: never answered here. A delivery's Enter would pick
|
|
966
|
+
// "Exit anyway"; the stop flow cancels it (Escape, then SIGTERM).
|
|
967
|
+
pattern: MUSE_LOCAL_WORK_QUIT,
|
|
968
|
+
isActive: museLocalWorkQuitActive,
|
|
969
|
+
keys: [],
|
|
970
|
+
holdOnly: true,
|
|
971
|
+
blocksDelivery: true,
|
|
972
|
+
inputBlocked: true,
|
|
973
|
+
description: "Muse \"Local work is still active\" quit menu — holding, never answering with Enter",
|
|
974
|
+
},
|
|
662
975
|
];
|
|
663
976
|
}
|
|
664
977
|
getContextUsage() {
|
|
@@ -726,6 +1039,19 @@ export class MuseBackend {
|
|
|
726
1039
|
getQuitCommand() { return "/quit"; }
|
|
727
1040
|
getQuitKey() { return "C-c"; }
|
|
728
1041
|
getQuitKeyPresses() { return 2; }
|
|
1042
|
+
/** #1716: `/quit` with a background task running stops at the "Local work is still active" menu. */
|
|
1043
|
+
quitBlockedByDialog(pane) {
|
|
1044
|
+
return museLocalWorkQuitActive(pane);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* #1717: the terminals panel with a `running` task — the pause flow defers
|
|
1048
|
+
* an idle pause while this holds, so an AgEnD-initiated silent pause never
|
|
1049
|
+
* kills the user's running background work. Explicit stop/restart still
|
|
1050
|
+
* proceed via quitBlockedByDialog above.
|
|
1051
|
+
*/
|
|
1052
|
+
backgroundWorkRunning(pane) {
|
|
1053
|
+
return museBackgroundWorkActive(pane);
|
|
1054
|
+
}
|
|
729
1055
|
getCompactCommand() { return "/compact"; }
|
|
730
1056
|
getClearCommand() { return "/clear"; }
|
|
731
1057
|
/**
|