mixdog 1.3.2 → 1.3.3
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/package.json +7 -7
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +21 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +92 -40
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/session-runtime/room-member-gate.mjs +0 -61
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { basename, dirname, extname, join } from 'node:path';
|
|
2
2
|
import { homedir, tmpdir } from 'node:os';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { redactDirectories } from '../../shared/redact-directories.mjs';
|
|
5
|
+
import { removeDirectoryWithRetry } from '../../shared/remove-directory.mjs';
|
|
4
6
|
import { constants as fsConstants, rmSync } from 'node:fs';
|
|
5
7
|
import { pathToFileURL } from 'node:url';
|
|
6
8
|
import JSZip from 'jszip';
|
|
@@ -44,6 +46,74 @@ function killQuietly(child) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// How long a killed conversion is waited for before it is reported anyway.
|
|
50
|
+
const SOFFICE_KILL_GRACE_MS = 10_000;
|
|
51
|
+
|
|
52
|
+
// soffice.com hands the work to a soffice.bin child, which a plain kill of the
|
|
53
|
+
// launcher leaves running (and holding the files it was given); Windows needs
|
|
54
|
+
// the whole tree stopped.
|
|
55
|
+
// The tree is stopped asynchronously and the helper itself is bounded, so a
|
|
56
|
+
// cancellation never blocks the event loop or waits on a stuck taskkill.
|
|
57
|
+
const SOFFICE_TASKKILL_TIMEOUT_MS = 5_000;
|
|
58
|
+
|
|
59
|
+
// The launcher is killed only once taskkill is done (or out of time): a launcher
|
|
60
|
+
// that exits first leaves taskkill with no descendants to find.
|
|
61
|
+
function killProcessTree(child) {
|
|
62
|
+
if (process.platform !== 'win32' || !child.pid) {
|
|
63
|
+
killQuietly(child);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let launcherKilled = false;
|
|
67
|
+
const killLauncher = () => {
|
|
68
|
+
if (launcherKilled) return;
|
|
69
|
+
launcherKilled = true;
|
|
70
|
+
killQuietly(child);
|
|
71
|
+
};
|
|
72
|
+
let killer = null;
|
|
73
|
+
try {
|
|
74
|
+
killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
75
|
+
} catch {
|
|
76
|
+
killLauncher();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const bound = setTimeout(() => {
|
|
80
|
+
killQuietly(killer);
|
|
81
|
+
killLauncher();
|
|
82
|
+
}, SOFFICE_TASKKILL_TIMEOUT_MS);
|
|
83
|
+
bound.unref?.();
|
|
84
|
+
const settle = () => {
|
|
85
|
+
clearTimeout(bound);
|
|
86
|
+
killLauncher();
|
|
87
|
+
};
|
|
88
|
+
killer.once('error', settle);
|
|
89
|
+
killer.once('close', settle);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Documents reach this LibreOffice from untrusted sources (chat attachments),
|
|
93
|
+
// so the private profile it runs with turns every macro off: the strictest
|
|
94
|
+
// security level, and macro execution disabled outright.
|
|
95
|
+
export const MACRO_LOCKDOWN_REGISTRY = [
|
|
96
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
97
|
+
'<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
|
|
98
|
+
'<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="MacroSecurityLevel" oor:op="fuse"><value>3</value></prop></item>',
|
|
99
|
+
'<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="DisableMacrosExecution" oor:op="fuse"><value>true</value></prop></item>',
|
|
100
|
+
'</oor:items>',
|
|
101
|
+
'',
|
|
102
|
+
].join('\n');
|
|
103
|
+
|
|
104
|
+
/** The command line of one headless conversion, in a private profile. */
|
|
105
|
+
export function sofficeConvertArguments(profile, { to, outDir, input }) {
|
|
106
|
+
return [
|
|
107
|
+
`-env:UserInstallation=${pathToFileURL(profile).href}`,
|
|
108
|
+
...SOFFICE_QUIET_ARGS,
|
|
109
|
+
'--convert-to',
|
|
110
|
+
to,
|
|
111
|
+
'--outdir',
|
|
112
|
+
outDir,
|
|
113
|
+
input,
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
47
117
|
// A detection probe must always answer. soffice.exe is a GUI launcher that can
|
|
48
118
|
// sit forever without exiting, which deadlocked detection before the rendering
|
|
49
119
|
// call was ever reached, so the probe is bounded and the child killed on expiry.
|
|
@@ -132,9 +202,20 @@ let exitHandlerRegistered = false;
|
|
|
132
202
|
// window closes. It must not build a throwaway profile per call either —
|
|
133
203
|
// creating the profile costs far more than the conversion it serves. One
|
|
134
204
|
// private profile is built on first use and reused for the life of the process.
|
|
135
|
-
function sharedProfileDir() {
|
|
136
|
-
|
|
205
|
+
export function sharedProfileDir() {
|
|
206
|
+
if (pendingProfile) return pendingProfile;
|
|
207
|
+
const attempt = (async () => {
|
|
137
208
|
const created = await mkdtemp(join(tmpdir(), 'mixdog-office-profile-'));
|
|
209
|
+
try {
|
|
210
|
+
await mkdir(join(created, 'user'), { recursive: true });
|
|
211
|
+
await writeFile(join(created, 'user', 'registrymodifications.xcu'), MACRO_LOCKDOWN_REGISTRY);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// A half-built profile is never registered, so nothing else would remove it.
|
|
214
|
+
await rm(created, { recursive: true, force: true }).catch(() => {});
|
|
215
|
+
const failure = new Error(redactDirectories(error?.message || String(error), [created]));
|
|
216
|
+
if (error?.code) failure.code = error.code;
|
|
217
|
+
throw failure;
|
|
218
|
+
}
|
|
138
219
|
// An exit handler cannot await, and a profile left behind accumulates one
|
|
139
220
|
// directory per run of the app.
|
|
140
221
|
createdProfiles.add(created);
|
|
@@ -152,7 +233,12 @@ function sharedProfileDir() {
|
|
|
152
233
|
}
|
|
153
234
|
return created;
|
|
154
235
|
})();
|
|
155
|
-
|
|
236
|
+
pendingProfile = attempt;
|
|
237
|
+
// A failed build must not stay cached: the next call starts over.
|
|
238
|
+
attempt.catch(() => {
|
|
239
|
+
if (pendingProfile === attempt) pendingProfile = null;
|
|
240
|
+
});
|
|
241
|
+
return attempt;
|
|
156
242
|
}
|
|
157
243
|
|
|
158
244
|
// One profile means one LibreOffice at a time: a second process sharing it
|
|
@@ -177,6 +263,8 @@ export function runSoffice(program, args, { signal, timeoutMs, timeoutMessage, c
|
|
|
177
263
|
return new Promise((resolve) => {
|
|
178
264
|
let settled = false;
|
|
179
265
|
let timer = null;
|
|
266
|
+
let killTimer = null;
|
|
267
|
+
let stopped = null;
|
|
180
268
|
let stderr = '';
|
|
181
269
|
const child = spawn(program, args, {
|
|
182
270
|
windowsHide: true,
|
|
@@ -187,25 +275,33 @@ export function runSoffice(program, args, { signal, timeoutMs, timeoutMessage, c
|
|
|
187
275
|
if (settled) return;
|
|
188
276
|
settled = true;
|
|
189
277
|
clearTimeout(timer);
|
|
278
|
+
clearTimeout(killTimer);
|
|
190
279
|
signal?.removeEventListener?.('abort', onAbort);
|
|
191
280
|
resolve(value);
|
|
192
281
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
282
|
+
// A conversion that is cancelled or out of time is reported once the
|
|
283
|
+
// process has closed, so the caller never removes files it still holds.
|
|
284
|
+
const stop = (value) => {
|
|
285
|
+
if (settled || stopped) return;
|
|
286
|
+
stopped = value;
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
289
|
+
killTimer = setTimeout(() => finish(value), SOFFICE_KILL_GRACE_MS);
|
|
290
|
+
killProcessTree(child);
|
|
196
291
|
};
|
|
292
|
+
const onAbort = () => stop({ ok: false, error: cancelMessage });
|
|
197
293
|
child.stderr.setEncoding('utf8');
|
|
198
294
|
child.stderr.on('data', (chunk) => {
|
|
199
295
|
if (stderr.length < MAX_SOFFICE_STDERR_CHARS) stderr = `${stderr}${chunk}`.slice(0, MAX_SOFFICE_STDERR_CHARS);
|
|
200
296
|
});
|
|
201
297
|
child.once('error', (error) => finish({ ok: false, error: error?.message || String(error) }));
|
|
202
298
|
child.once('close', (code) =>
|
|
203
|
-
finish(
|
|
299
|
+
finish(
|
|
300
|
+
stopped ||
|
|
301
|
+
(code === 0 ? { ok: true } : { ok: false, error: stderr.trim() || `LibreOffice exited with code ${code}` })
|
|
302
|
+
)
|
|
204
303
|
);
|
|
205
|
-
timer = setTimeout(() => {
|
|
206
|
-
killQuietly(child);
|
|
207
|
-
finish({ ok: false, error: timeoutMessage });
|
|
208
|
-
}, timeoutMs);
|
|
304
|
+
timer = setTimeout(() => stop({ ok: false, error: timeoutMessage }), timeoutMs);
|
|
209
305
|
if (signal?.aborted) onAbort();
|
|
210
306
|
else signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
211
307
|
});
|
|
@@ -228,21 +324,17 @@ function convertWithLibreOffice(program, input, { to, outDir, signal = null, tim
|
|
|
228
324
|
return queueConversion(async () => {
|
|
229
325
|
const profile = await sharedProfileDir();
|
|
230
326
|
const env = await headlessFontEnvironment(profile);
|
|
231
|
-
const result = await runSoffice(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{ signal, timeoutMs, timeoutMessage: messages.timeout, cancelMessage: messages.cancelled, env }
|
|
243
|
-
);
|
|
244
|
-
if (!result.ok) await discardProfileDir();
|
|
245
|
-
return result;
|
|
327
|
+
const result = await runSoffice(program, sofficeConvertArguments(profile, { to, outDir, input }), {
|
|
328
|
+
signal,
|
|
329
|
+
timeoutMs,
|
|
330
|
+
timeoutMessage: messages.timeout,
|
|
331
|
+
cancelMessage: messages.cancelled,
|
|
332
|
+
env,
|
|
333
|
+
});
|
|
334
|
+
if (result.ok) return result;
|
|
335
|
+
await discardProfileDir();
|
|
336
|
+
// The profile is the converter's private folder; what it printed about it stays out of the error.
|
|
337
|
+
return { ...result, error: redactDirectories(result.error, [profile]) };
|
|
246
338
|
});
|
|
247
339
|
}
|
|
248
340
|
|
|
@@ -525,11 +617,11 @@ export async function validateLibreOfficeReopen(path, { signal = null } = {}) {
|
|
|
525
617
|
* folder and lands with an exclusive copy, so a file already at `output` is
|
|
526
618
|
* never replaced.
|
|
527
619
|
*/
|
|
528
|
-
export async function convertLegacyOffice(path, output, { signal = null } = {}) {
|
|
620
|
+
export async function convertLegacyOffice(path, output, { signal = null, remove = undefined, log = undefined } = {}) {
|
|
529
621
|
const program = await libreOfficeProgram();
|
|
530
622
|
if (!program) {
|
|
531
623
|
throw new Error(
|
|
532
|
-
`${path} is a legacy binary file, not an Office package, and converting it needs LibreOffice. Install LibreOffice, or open it in Microsoft Office and save it as ${output}.`
|
|
624
|
+
`${path} is a legacy binary file, not an Office package, and converting it needs LibreOffice. Install the Office feature in Mixdog settings (it installs LibreOffice), or open it in Microsoft Office and save it as ${output}.`
|
|
533
625
|
);
|
|
534
626
|
}
|
|
535
627
|
const extension = extname(output).slice(1).toLowerCase();
|
|
@@ -545,6 +637,8 @@ export async function convertLegacyOffice(path, output, { signal = null } = {})
|
|
|
545
637
|
cancelled: 'LibreOffice conversion was cancelled',
|
|
546
638
|
},
|
|
547
639
|
});
|
|
640
|
+
// A cancellation is the caller's abort, not a conversion failure.
|
|
641
|
+
if (signal?.aborted) throw signal.reason ?? Object.assign(new Error(result.error), { name: 'AbortError' });
|
|
548
642
|
if (!result.ok) throw new Error(`LibreOffice could not convert ${path} to .${extension}: ${result.error}`);
|
|
549
643
|
const generated = convertedOutputPath(outputDir, path, extension);
|
|
550
644
|
if (!(await producedFileSize(generated))) {
|
|
@@ -554,8 +648,14 @@ export async function convertLegacyOffice(path, output, { signal = null } = {})
|
|
|
554
648
|
}
|
|
555
649
|
await copyFile(generated, output, fsConstants.COPYFILE_EXCL);
|
|
556
650
|
return output;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
// The scratch folder is this function's own: no error leaves naming it
|
|
653
|
+
// (a failed copy reports both of its paths). A cancellation stays an abort.
|
|
654
|
+
if (error?.name === 'AbortError') throw error;
|
|
655
|
+
if (signal?.aborted) throw signal.reason;
|
|
656
|
+
throw new Error(redactDirectories(error instanceof Error ? error.message : String(error), [outputDir]));
|
|
557
657
|
} finally {
|
|
558
|
-
await
|
|
658
|
+
await removeDirectoryWithRetry(outputDir, { remove, log });
|
|
559
659
|
}
|
|
560
660
|
}
|
|
561
661
|
|
|
@@ -581,7 +681,9 @@ export async function renderPortableOoxml(path, output, { signal = null } = {})
|
|
|
581
681
|
const generated = convertedOutputPath(outputDir, path, 'pdf');
|
|
582
682
|
// LibreOffice can exit 0 without writing anything (a missing or unreadable source).
|
|
583
683
|
if (!(await producedFileSize(generated))) {
|
|
584
|
-
throw new Error(
|
|
684
|
+
throw new Error(
|
|
685
|
+
`LibreOffice produced no PDF from ${path}; the file may be missing, damaged or password-protected.`
|
|
686
|
+
);
|
|
585
687
|
}
|
|
586
688
|
if (generated !== output) await rename(generated, output);
|
|
587
689
|
return output;
|
|
@@ -48,6 +48,21 @@ export const MASCOT_SPRITES = Object.freeze({
|
|
|
48
48
|
'................',
|
|
49
49
|
'................',
|
|
50
50
|
],
|
|
51
|
+
// Mixy's: a low 2x2 block with a lighter tip, clear of her ears.
|
|
52
|
+
thick: [
|
|
53
|
+
'................',
|
|
54
|
+
'................',
|
|
55
|
+
'................',
|
|
56
|
+
'................',
|
|
57
|
+
'................',
|
|
58
|
+
'................',
|
|
59
|
+
'................',
|
|
60
|
+
'................',
|
|
61
|
+
'.............BL.',
|
|
62
|
+
'.............BL.',
|
|
63
|
+
'................',
|
|
64
|
+
'................',
|
|
65
|
+
],
|
|
51
66
|
},
|
|
52
67
|
legs: {
|
|
53
68
|
dog: [
|
|
@@ -279,17 +294,17 @@ export const MASCOT_SPRITES = Object.freeze({
|
|
|
279
294
|
],
|
|
280
295
|
},
|
|
281
296
|
mixy: {
|
|
282
|
-
tail: '
|
|
297
|
+
tail: 'thick',
|
|
283
298
|
legs: 'dog',
|
|
284
299
|
eye: 'K',
|
|
285
300
|
ears: [
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'
|
|
289
|
-
'.DD..........
|
|
290
|
-
'.DD
|
|
291
|
-
'.DD
|
|
292
|
-
'.DD
|
|
301
|
+
'................',
|
|
302
|
+
'................',
|
|
303
|
+
'................',
|
|
304
|
+
'.DD..........DD.',
|
|
305
|
+
'.DD..........DD.',
|
|
306
|
+
'.DD..........DD.',
|
|
307
|
+
'.DD..........DD.',
|
|
293
308
|
'................',
|
|
294
309
|
'................',
|
|
295
310
|
'................',
|
|
@@ -31,8 +31,9 @@ const RETIRED_SPECIES = { dalmatian: 'labrador', cat: 'labrador', bird: 'labrado
|
|
|
31
31
|
const RETIRED_ACCESSORIES = { headset: 'glasses' };
|
|
32
32
|
export const MASCOT_EXPRESSIONS = Object.freeze(['calm', 'happy', 'wink', 'sleepy']);
|
|
33
33
|
|
|
34
|
-
/** Mixy, the primary assistant: its own silver pup with
|
|
35
|
-
* slate collar with the gold star medal.
|
|
34
|
+
/** Mixy, the primary assistant: its own silver pup with both ears down, a low
|
|
35
|
+
* thick tail with a lighter tip and a slate collar with the gold star medal.
|
|
36
|
+
* Never offered in the picker. */
|
|
36
37
|
export const MIXY_AVATAR = Object.freeze({
|
|
37
38
|
kind: 'mascot',
|
|
38
39
|
species: 'mixy',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface AgentRoomTurnDetail {
|
|
2
|
+
status: string;
|
|
3
|
+
result?: { content: string };
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AgentRoomHost {
|
|
7
|
+
member(
|
|
8
|
+
roomSessionId: string,
|
|
9
|
+
request: { to?: unknown; prompt?: unknown }
|
|
10
|
+
): Promise<{ agentId: string; name: string; sessionId: string; text: string }>;
|
|
11
|
+
ask(
|
|
12
|
+
roomSessionId: string,
|
|
13
|
+
request: {
|
|
14
|
+
agentId: string;
|
|
15
|
+
prompt: unknown;
|
|
16
|
+
from: { sessionId: string; agentId?: string };
|
|
17
|
+
id?: string;
|
|
18
|
+
hop?: number;
|
|
19
|
+
onSettled?: (detail: AgentRoomTurnDetail) => void;
|
|
20
|
+
follow?: (submissionId: string) => unknown;
|
|
21
|
+
}
|
|
22
|
+
): Promise<boolean>;
|
|
23
|
+
relay(
|
|
24
|
+
memberSessionId: string,
|
|
25
|
+
prompt: string,
|
|
26
|
+
options?: { id?: string; hop?: number; follow?: (submissionId: string) => unknown }
|
|
27
|
+
): Promise<{ settled: Promise<string> }>;
|
|
28
|
+
stop(roomSessionId: string): boolean;
|
|
29
|
+
}
|
|
30
|
+
export function registerAgentRoomHost(host: AgentRoomHost): () => void;
|
|
31
|
+
export function agentRoomHost(): AgentRoomHost;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Runtime → desktop request channel for group rooms. Rooms run only in the
|
|
2
|
+
// desktop service hosted in this daemon (apps/desktop agent-rooms.ts): it
|
|
3
|
+
// registers a host here, and the `session` tool's sends to a room, and the
|
|
4
|
+
// relay turns of a member's own reported work, go through it.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately dependency-free, like session-sidebar-host.mjs: the desktop
|
|
7
|
+
// service bundle imports this file too, and both copies share one registry
|
|
8
|
+
// through a global symbol.
|
|
9
|
+
|
|
10
|
+
const HOSTS_KEY = Symbol.for('mixdog.agentRoomHosts.v1');
|
|
11
|
+
|
|
12
|
+
function hosts() {
|
|
13
|
+
globalThis[HOSTS_KEY] ??= [];
|
|
14
|
+
return globalThis[HOSTS_KEY];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register a desktop room host:
|
|
19
|
+
* - `member(roomSessionId, { to, prompt })` → `{ agentId, name, sessionId, text }`:
|
|
20
|
+
* the addressed member (by `to`, an agent id or display name, else a leading
|
|
21
|
+
* `@name` in `prompt`), its member session, and the message without that
|
|
22
|
+
* mention; throws when none resolves.
|
|
23
|
+
* - `ask(roomSessionId, { agentId, prompt, from, id?, hop?, onSettled? })` →
|
|
24
|
+
* true: a message from another session (`from: { sessionId, agentId? }`)
|
|
25
|
+
* posted as its row and answered by that member alone; `onSettled(detail)`
|
|
26
|
+
* gets the member's turn end as a session turn's (`{ status, result?,
|
|
27
|
+
* error? }`).
|
|
28
|
+
* - `relay(memberSessionId, prompt, { id?, hop? })` → `{ settled }`: the
|
|
29
|
+
* member's turn in its room relaying `prompt` (its reported work); `settled`
|
|
30
|
+
* resolves the turn's end status ('done' when it finished).
|
|
31
|
+
* - `stop(roomSessionId)` → boolean: the room's stop button: the speaking
|
|
32
|
+
* member's turn is cancelled, queued calls settle cancelled, and no one else
|
|
33
|
+
* gets a turn; true when an exchange was running.
|
|
34
|
+
* `hop`: the send's or report's place in a bot-to-bot chain (bot-hops.mjs);
|
|
35
|
+
* the member's turn carries it, so the sends it makes count on from there.
|
|
36
|
+
* `follow(submissionId)` (both): the turn that answers is submitted under
|
|
37
|
+
* another id than `id` (one turn for several calls, or a fresh turn for one
|
|
38
|
+
* cut short); the host awaits it before submitting, so the caller's durable
|
|
39
|
+
* record follows that turn across a restart.
|
|
40
|
+
* @returns {() => void} Unregister.
|
|
41
|
+
*/
|
|
42
|
+
export function registerAgentRoomHost(host) {
|
|
43
|
+
const entry = { host };
|
|
44
|
+
hosts().push(entry);
|
|
45
|
+
return () => {
|
|
46
|
+
const list = hosts();
|
|
47
|
+
const index = list.indexOf(entry);
|
|
48
|
+
if (index >= 0) list.splice(index, 1);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The active (most recently registered) desktop room host; throws when no
|
|
53
|
+
* desktop app is attached, since nothing else runs a room. */
|
|
54
|
+
export function agentRoomHost() {
|
|
55
|
+
const active = hosts().at(-1);
|
|
56
|
+
if (!active) throw new Error('group rooms run in the desktop app, and no desktop app is attached to this runtime');
|
|
57
|
+
return active.host;
|
|
58
|
+
}
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
// transcript meta of the message it delivers, and in the report that answers
|
|
3
3
|
// it). A turn knows the hop of the submission that started it (`turnHop`,
|
|
4
4
|
// stamped on the session when the turn is prepared); a send made in it goes
|
|
5
|
-
// out at turnHop + 1.
|
|
6
|
-
// a
|
|
5
|
+
// out at turnHop + 1. Only a message another bot sent counts: a turn started by
|
|
6
|
+
// the user, a routine or a runtime notification (a task report, a wake) starts
|
|
7
|
+
// at 0, so a bot's reports of its own sends never use up its budget. Background
|
|
8
|
+
// work (a command, an agent task) launched in a bot-descended turn carries that
|
|
9
|
+
// turn's hop on its completion notification (`hop` in the completion meta and
|
|
10
|
+
// entry), and the turn the notification starts or joins resumes at it.
|
|
7
11
|
export const MAX_BOT_HOPS = 3;
|
|
8
12
|
export const BOT_HOP_LIMIT_ERROR = 'session: Bot-to-bot chain limit reached; ask the user before continuing.';
|
|
9
13
|
|
|
10
|
-
const hopOf = (value) => (Number.isInteger(value) && value > 0 ? value : 0);
|
|
14
|
+
export const hopOf = (value) => (Number.isInteger(value) && value > 0 ? value : 0);
|
|
11
15
|
|
|
12
|
-
/** The hop
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/** The hop of the turn that made a send, from the send's own place in the chain
|
|
17
|
+
* (`hop`, as a task report or watch carries it, with its `target`): a send to
|
|
18
|
+
* another session is one hop past its turn, one to a task session the bot
|
|
19
|
+
* created only continues it. A report wakes the sender's turn back at this
|
|
20
|
+
* depth: bot-descended work resumes where it was, independent work at 0. */
|
|
21
|
+
export const sendDepth = ({ hop, target } = {}) => Math.max(0, hopOf(hop) - (target === 'session' ? 1 : 0));
|
|
17
22
|
|
|
18
|
-
/** Stamp the
|
|
19
|
-
export function stampTurnHop(session, transcriptMeta
|
|
20
|
-
session.turnHop =
|
|
23
|
+
/** Stamp the hop of the submission that starts the turn (else 0) on `session`. */
|
|
24
|
+
export function stampTurnHop(session, transcriptMeta) {
|
|
25
|
+
session.turnHop = hopOf(transcriptMeta?.hop);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/** A bot's message steered into the running turn raises the turn's hop. */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `text` with every mention of the given directories taken out (and the
|
|
5
|
+
* separator after it), whether written as a native path, with forward slashes
|
|
6
|
+
* or as a file URL, so an error never discloses a private scratch location.
|
|
7
|
+
*/
|
|
8
|
+
export function redactDirectories(text, directories) {
|
|
9
|
+
let out = String(text ?? '');
|
|
10
|
+
for (const directory of directories) {
|
|
11
|
+
if (!directory) continue;
|
|
12
|
+
const forms = new Set([directory, directory.replaceAll('\\', '/'), pathToFileURL(directory).href]);
|
|
13
|
+
for (const form of [...forms].sort((a, b) => b.length - a.length)) {
|
|
14
|
+
out = out.split(`${form}\\`).join('').split(`${form}/`).join('').split(form).join('');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export const REMOVE_RETRY_MS = 100;
|
|
4
|
+
const REMOVE_ATTEMPTS = 6;
|
|
5
|
+
const RETRYABLE_REMOVE_CODES = new Set(['EBUSY', 'EPERM', 'ENOTEMPTY', 'EACCES']);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Removes a private scratch folder. A process that just exited can still hold
|
|
9
|
+
* its files for a moment on Windows, so a busy folder is retried briefly; a
|
|
10
|
+
* failure that persists is logged (never swallowed) and does not throw.
|
|
11
|
+
*/
|
|
12
|
+
export async function removeDirectoryWithRetry(
|
|
13
|
+
dir,
|
|
14
|
+
{ remove = rm, log = (line) => process.stderr.write(line), retryMs = REMOVE_RETRY_MS } = {}
|
|
15
|
+
) {
|
|
16
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
17
|
+
try {
|
|
18
|
+
await remove(dir, { recursive: true, force: true });
|
|
19
|
+
return;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (attempt >= REMOVE_ATTEMPTS || !RETRYABLE_REMOVE_CODES.has(error?.code)) {
|
|
22
|
+
log(`scratch folder ${dir} could not be removed: ${error?.message || error}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs * attempt));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export type SessionSidebarChange =
|
|
1
|
+
export type SessionSidebarChange =
|
|
2
|
+
| { favorite: boolean }
|
|
3
|
+
| { archived: boolean }
|
|
4
|
+
| { archived: true; cascade: false }
|
|
5
|
+
| { forgotten: true };
|
|
2
6
|
export type SessionSidebarHost = (sessionId: unknown, change: unknown) => unknown;
|
|
3
|
-
export
|
|
7
|
+
export type SessionSidebarState = { archived: Record<string, number> };
|
|
8
|
+
export function registerSessionSidebarHost(
|
|
9
|
+
host: SessionSidebarHost,
|
|
10
|
+
options?: { state?: (() => SessionSidebarState | Promise<SessionSidebarState>) | null }
|
|
11
|
+
): () => void;
|
|
4
12
|
export function applySessionSidebarChange(sessionId: string, change: SessionSidebarChange): Promise<void>;
|
|
13
|
+
export function readSessionSidebarState(): Promise<SessionSidebarState | null>;
|
|
@@ -15,11 +15,15 @@ function hosts() {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Register a desktop host. `host(sessionId, change)` applies one change:
|
|
18
|
-
* `{ favorite: boolean }
|
|
18
|
+
* `{ favorite: boolean }`, `{ archived: boolean }` (an archive takes the
|
|
19
|
+
* session's sub-agent sessions with it, a restore brings back the ones it took),
|
|
20
|
+
* `{ archived: true, cascade: false }` (that session alone)
|
|
21
|
+
* or `{ forgotten: true }` (a session the store deleted: its records go).
|
|
22
|
+
* `state()` answers the archive flags: `{ archived }`, session id → stamp.
|
|
19
23
|
* @returns {() => void} Unregister.
|
|
20
24
|
*/
|
|
21
|
-
export function registerSessionSidebarHost(host) {
|
|
22
|
-
const entry = { host };
|
|
25
|
+
export function registerSessionSidebarHost(host, { state = null } = {}) {
|
|
26
|
+
const entry = { host, state: typeof state === 'function' ? state : null };
|
|
23
27
|
hosts().push(entry);
|
|
24
28
|
return () => {
|
|
25
29
|
const list = hosts();
|
|
@@ -36,3 +40,12 @@ export async function applySessionSidebarChange(sessionId, change) {
|
|
|
36
40
|
if (!active) throw new Error('no desktop app is attached to this runtime');
|
|
37
41
|
await active.host(sessionId, change);
|
|
38
42
|
}
|
|
43
|
+
|
|
44
|
+
/** The active desktop's flags, `{ archived }` (session id → epoch-ms stamp);
|
|
45
|
+
* null when no desktop (or one without a state reader) is attached. */
|
|
46
|
+
export async function readSessionSidebarState() {
|
|
47
|
+
const active = hosts().at(-1);
|
|
48
|
+
if (!active?.state) return null;
|
|
49
|
+
const state = await active.state();
|
|
50
|
+
return { archived: { ...state?.archived } };
|
|
51
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clean } from './clean.mjs';
|
|
2
|
+
import { hopOf } from './bot-hops.mjs';
|
|
2
3
|
import {
|
|
3
4
|
parseTaskNotification,
|
|
4
5
|
renderTaskCompletionEnvelope,
|
|
@@ -113,6 +114,8 @@ export function normalizeToolNotifyContext(context = {}) {
|
|
|
113
114
|
callerSessionId: callerSessionId || null,
|
|
114
115
|
routingSessionId: routingSessionId || null,
|
|
115
116
|
clientHostPid,
|
|
117
|
+
// The bot-chain hop of the turn that launched the work (bot-hops.mjs).
|
|
118
|
+
...(hopOf(context.hop) ? { hop: context.hop } : {}),
|
|
116
119
|
};
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -133,6 +136,7 @@ function toolCompletionMeta({ surface = 'tool', id, status, resultType, instruct
|
|
|
133
136
|
? { routing_session_id: ctx.routingSessionId }
|
|
134
137
|
: {}),
|
|
135
138
|
...(ctx.clientHostPid ? { client_host_pid: String(ctx.clientHostPid) } : {}),
|
|
139
|
+
...(ctx.hop ? { hop: ctx.hop } : {}),
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -573,6 +573,15 @@ function gitStageUnit(a) {
|
|
|
573
573
|
});
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
function gitWorktreeUnit(a) {
|
|
577
|
+
return unitDescriptor('Git', {
|
|
578
|
+
active: 'Managing',
|
|
579
|
+
done: 'Managed',
|
|
580
|
+
noun: 'worktree',
|
|
581
|
+
effect: String(a.worktree_action || '').toLowerCase() !== 'list',
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
576
585
|
function cwdUnit(a) {
|
|
577
586
|
const action = String(a.action || a.type || '').toLowerCase();
|
|
578
587
|
// A bare `path` selects that Project even when action is omitted.
|
|
@@ -604,14 +613,6 @@ function cwdUnit(a) {
|
|
|
604
613
|
const [active, done] = words;
|
|
605
614
|
return unitDescriptor('Setup', { active, done, noun: 'session', effect: true });
|
|
606
615
|
}
|
|
607
|
-
if (action.endsWith('-worktree') || action.endsWith('-worktrees')) {
|
|
608
|
-
return unitDescriptor('Setup', {
|
|
609
|
-
active: 'Managing',
|
|
610
|
-
done: 'Managed',
|
|
611
|
-
noun: 'worktree',
|
|
612
|
-
effect: !action.startsWith('list'),
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
616
|
if (action === 'status') {
|
|
616
617
|
return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'session' });
|
|
617
618
|
}
|
|
@@ -715,7 +716,9 @@ const TOOL_UNITS = new Map([
|
|
|
715
716
|
(a) =>
|
|
716
717
|
a.action === 'stage'
|
|
717
718
|
? gitStageUnit(a)
|
|
718
|
-
:
|
|
719
|
+
: a.action === 'worktree'
|
|
720
|
+
? gitWorktreeUnit(a)
|
|
721
|
+
: unitDescriptor('Git', { count: queryCount(a, 'command', 'commands') || 1, noun: 'Git command' }),
|
|
719
722
|
],
|
|
720
723
|
['github', githubUnit],
|
|
721
724
|
// Preserve the staging work unit when rendering historical transcripts.
|