mixdog 0.9.25 → 0.9.26
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 +1 -1
- package/scripts/steering-fold-provenance-test.mjs +71 -0
- package/scripts/webhook-smoke.mjs +46 -53
- package/src/defaults/skills/setup/SKILL.md +6 -1
- package/src/rules/shared/01-tool.md +8 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +17 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +51 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +164 -9
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +7 -3
- package/src/runtime/channels/lib/config.mjs +13 -11
- package/src/runtime/channels/lib/memory-client.mjs +22 -2
- package/src/runtime/channels/lib/owned-runtime.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +226 -208
- package/src/runtime/channels/lib/status-snapshot.mjs +16 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +7 -318
- package/src/runtime/channels/lib/webhook.mjs +98 -150
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/memory/index.mjs +50 -25
- package/src/runtime/memory/lib/cycle-scheduler.mjs +3 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +13 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +6 -1
- package/src/runtime/memory/lib/pg/process.mjs +19 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +125 -14
- package/src/runtime/memory/lib/query-handlers.mjs +102 -0
- package/src/runtime/memory/lib/recall-format.mjs +60 -22
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +20 -6
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/child-guardian.mjs +2 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/schedules-db.mjs +350 -0
- package/src/runtime/shared/service-discovery.mjs +169 -0
- package/src/runtime/shared/spawn-flags.mjs +27 -0
- package/src/runtime/shared/tool-primitives.mjs +3 -1
- package/src/runtime/shared/tool-surface.mjs +19 -3
- package/src/runtime/shared/update-checker.mjs +54 -10
- package/src/runtime/shared/webhooks-db.mjs +405 -0
- package/src/session-runtime/channel-config-api.mjs +13 -13
- package/src/session-runtime/lifecycle-api.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +39 -20
- package/src/standalone/agent-tool.mjs +42 -11
- package/src/standalone/channel-admin.mjs +173 -121
- package/src/standalone/channel-worker.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +10 -5
- package/src/tui/App.jsx +32 -10
- package/src/tui/app/channel-pickers.mjs +9 -9
- package/src/tui/app/clipboard.mjs +14 -0
- package/src/tui/app/doctor.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +17 -7
- package/src/tui/app/settings-picker.mjs +2 -2
- package/src/tui/app/transcript-window.mjs +37 -1
- package/src/tui/app/use-mouse-input.mjs +19 -6
- package/src/tui/components/ToolExecution.jsx +12 -4
- package/src/tui/display-width.mjs +10 -4
- package/src/tui/dist/index.mjs +129 -55
- package/src/tui/engine/session-api-ext.mjs +12 -12
- package/src/tui/index.jsx +12 -2
- package/src/ui/statusline-segments.mjs +12 -1
- package/vendor/ink/build/display-width.js +5 -4
- package/src/runtime/shared/schedules-store.mjs +0 -82
package/src/tui/App.jsx
CHANGED
|
@@ -1338,7 +1338,14 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1338
1338
|
copySelection();
|
|
1339
1339
|
return;
|
|
1340
1340
|
}
|
|
1341
|
-
|
|
1341
|
+
// No app-owned selection. On Windows Terminal the same Ctrl+C is also the
|
|
1342
|
+
// native terminal's copy shortcut for a mouse selection we can't see — so
|
|
1343
|
+
// rendering a hint here fights that copy and flashes a spurious message.
|
|
1344
|
+
// Suppress the hint on win32 (interrupt routing is unchanged: Esc still
|
|
1345
|
+
// interrupts). Other platforms keep the guidance.
|
|
1346
|
+
if (process.platform !== 'win32') {
|
|
1347
|
+
showSelectionCopyHint('select text to copy · Esc interrupts', 'plain');
|
|
1348
|
+
}
|
|
1342
1349
|
return;
|
|
1343
1350
|
}
|
|
1344
1351
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1855,19 +1862,34 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1855
1862
|
}
|
|
1856
1863
|
if (channelPrompt.kind === 'schedule-add') {
|
|
1857
1864
|
const [name, time, instructions, channel, model] = parts;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1865
|
+
// saveSchedule is async (PG-backed). Only close the prompt / open the
|
|
1866
|
+
// list on success; surface a failure notice and keep the prompt open
|
|
1867
|
+
// so the input is not lost and the user can retry.
|
|
1868
|
+
Promise.resolve(store.saveSchedule({ name, time, instructions, channel, model }))
|
|
1869
|
+
.then(() => {
|
|
1870
|
+
setChannelPrompt(null);
|
|
1871
|
+
void openChannelSetupPicker('schedules');
|
|
1872
|
+
})
|
|
1873
|
+
.catch((e) => {
|
|
1874
|
+
store.pushNotice(`schedule save failed: ${e?.message || e}`, 'error');
|
|
1875
|
+
});
|
|
1861
1876
|
return true;
|
|
1862
1877
|
}
|
|
1863
1878
|
if (channelPrompt.kind === 'webhook-add') {
|
|
1864
1879
|
const [name, instructions, channel, model, parser] = parts;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1880
|
+
// saveWebhook is async (PG-backed). Only close the prompt / open the
|
|
1881
|
+
// list on success; surface a failure notice and keep the prompt open.
|
|
1882
|
+
Promise.resolve(store.saveWebhook({ name, instructions, channel, model, parser }))
|
|
1883
|
+
.then((result) => {
|
|
1884
|
+
if (result?.secret) {
|
|
1885
|
+
store.pushNotice(`webhook secret for ${result.name}: ${result.secret}`, 'info');
|
|
1886
|
+
}
|
|
1887
|
+
setChannelPrompt(null);
|
|
1888
|
+
void openChannelSetupPicker('webhooks');
|
|
1889
|
+
})
|
|
1890
|
+
.catch((e) => {
|
|
1891
|
+
store.pushNotice(`webhook save failed: ${e?.message || e}`, 'error');
|
|
1892
|
+
});
|
|
1871
1893
|
return true;
|
|
1872
1894
|
}
|
|
1873
1895
|
} catch (e) {
|
|
@@ -27,7 +27,7 @@ export function createChannelPickers({
|
|
|
27
27
|
// a long first-time install finishing later must not hijack whatever
|
|
28
28
|
// screen/picker the user has since navigated to.
|
|
29
29
|
let pickerGen = 0;
|
|
30
|
-
const openChannelTypeActionsPicker = (backend, options = {}) => {
|
|
30
|
+
const openChannelTypeActionsPicker = async (backend, options = {}) => {
|
|
31
31
|
pickerGen += 1;
|
|
32
32
|
const parentReturn = typeof options.returnTo === 'function'
|
|
33
33
|
? options.returnTo
|
|
@@ -38,7 +38,7 @@ export function createChannelPickers({
|
|
|
38
38
|
setContextPanel(null);
|
|
39
39
|
let setup;
|
|
40
40
|
try {
|
|
41
|
-
setup = store.getChannelSetup();
|
|
41
|
+
setup = await store.getChannelSetup();
|
|
42
42
|
} catch (e) {
|
|
43
43
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
44
44
|
return;
|
|
@@ -125,7 +125,7 @@ export function createChannelPickers({
|
|
|
125
125
|
});
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
const openChannelSettingTypePicker = (options = {}) => {
|
|
128
|
+
const openChannelSettingTypePicker = async (options = {}) => {
|
|
129
129
|
pickerGen += 1;
|
|
130
130
|
const returnTo = typeof options.returnTo === 'function' ? options.returnTo : () => {};
|
|
131
131
|
setProviderPrompt(null);
|
|
@@ -135,7 +135,7 @@ export function createChannelPickers({
|
|
|
135
135
|
setContextPanel(null);
|
|
136
136
|
let setup;
|
|
137
137
|
try {
|
|
138
|
-
setup = store.getChannelSetup();
|
|
138
|
+
setup = await store.getChannelSetup();
|
|
139
139
|
} catch (e) {
|
|
140
140
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
141
141
|
return;
|
|
@@ -203,7 +203,7 @@ export function createChannelPickers({
|
|
|
203
203
|
setContextPanel(null);
|
|
204
204
|
let setup;
|
|
205
205
|
try {
|
|
206
|
-
setup = store.getChannelSetup();
|
|
206
|
+
setup = await store.getChannelSetup();
|
|
207
207
|
} catch (e) {
|
|
208
208
|
store.pushNotice(`channels failed: ${e?.message || e}`, 'error');
|
|
209
209
|
return;
|
|
@@ -239,14 +239,14 @@ export function createChannelPickers({
|
|
|
239
239
|
_action: 'noop',
|
|
240
240
|
}]),
|
|
241
241
|
];
|
|
242
|
-
const toggleSchedule = (item) => {
|
|
242
|
+
const toggleSchedule = async (item) => {
|
|
243
243
|
if (item._action !== 'schedule-toggle') return;
|
|
244
244
|
if (!channelRemoteEnabled) {
|
|
245
245
|
store.pushNotice('enable channel first', 'warn');
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
try {
|
|
249
|
-
store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
249
|
+
await store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
250
250
|
void openChannelSetupPicker('schedules', { highlightValue: `schedule:${item._name}` });
|
|
251
251
|
} catch (e) {
|
|
252
252
|
store.pushNotice(`schedule toggle failed: ${e?.message || e}`, 'error');
|
|
@@ -346,14 +346,14 @@ export function createChannelPickers({
|
|
|
346
346
|
_action: 'noop',
|
|
347
347
|
}]),
|
|
348
348
|
];
|
|
349
|
-
const toggleWebhook = (item) => {
|
|
349
|
+
const toggleWebhook = async (item) => {
|
|
350
350
|
if (item._action !== 'webhook-toggle') return;
|
|
351
351
|
if (!channelRemoteEnabled) {
|
|
352
352
|
store.pushNotice('enable channel first', 'warn');
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
355
|
try {
|
|
356
|
-
store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
356
|
+
await store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
357
357
|
void openChannelSetupPicker('webhooks', { highlightValue: `webhook:${item._name}` });
|
|
358
358
|
} catch (e) {
|
|
359
359
|
store.pushNotice(`webhook toggle failed: ${e?.message || e}`, 'error');
|
|
@@ -18,8 +18,22 @@ export function osc52ClipboardSequence(text) {
|
|
|
18
18
|
// helper. ~256KB of clipboard text → ~350KB of base64.
|
|
19
19
|
const OSC52_MAX_BYTES = 256 * 1024;
|
|
20
20
|
|
|
21
|
+
// On native Windows (ConPTY) a large OSC 52 base64 payload written synchronously
|
|
22
|
+
// to stdout can stall for multiple seconds under terminal backpressure while a
|
|
23
|
+
// mouse selection is active — a debugger-confirmed Ctrl+C copy freeze. clip.exe
|
|
24
|
+
// alone is the reliable local clipboard writer, so skip OSC 52 entirely on
|
|
25
|
+
// win32 UNLESS we're in a remote/multiplexed session (SSH or tmux) where OSC 52
|
|
26
|
+
// is the only way to reach the user's real terminal.
|
|
27
|
+
function shouldSkipOsc52() {
|
|
28
|
+
if (process.platform !== 'win32') return false;
|
|
29
|
+
if (process.env.TMUX) return false;
|
|
30
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function writeOsc52Clipboard(text) {
|
|
22
35
|
const value = String(text ?? '');
|
|
36
|
+
if (shouldSkipOsc52()) return false;
|
|
23
37
|
if (Buffer.byteLength(value, 'utf8') > OSC52_MAX_BYTES) return false;
|
|
24
38
|
try {
|
|
25
39
|
process.stdout.write(osc52ClipboardSequence(value));
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -161,7 +161,7 @@ export async function buildDoctorReport(runtime = {}, getState = () => ({})) {
|
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
|
|
164
|
-
const setup = runtime.getChannelSetup?.() || {};
|
|
164
|
+
const setup = (await runtime.getChannelSetup?.()) || {};
|
|
165
165
|
const tokens = [];
|
|
166
166
|
if (setup.discord?.authenticated) tokens.push('discord');
|
|
167
167
|
if (setup.telegram?.authenticated) tokens.push('telegram');
|
|
@@ -31,6 +31,10 @@ export function createMaintenancePickers({
|
|
|
31
31
|
const upd = readSettings();
|
|
32
32
|
const status = readStatus();
|
|
33
33
|
const current = upd.currentVersion || 'unknown';
|
|
34
|
+
// After a successful in-place install the running process is still the
|
|
35
|
+
// old version; surface the pending version so "Current" doesn't look
|
|
36
|
+
// stale/broken until restart.
|
|
37
|
+
const installedVersion = status.phase === 'installed' ? (status.version || upd.latestVersion || null) : null;
|
|
34
38
|
const latestMeta = checking || status.phase === 'checking'
|
|
35
39
|
? 'checking…'
|
|
36
40
|
: (upd.latestVersion || 'unknown');
|
|
@@ -38,8 +42,10 @@ export function createMaintenancePickers({
|
|
|
38
42
|
{
|
|
39
43
|
value: 'current',
|
|
40
44
|
label: 'Current version',
|
|
41
|
-
meta: current,
|
|
42
|
-
description:
|
|
45
|
+
meta: installedVersion ? `${current} → ${installedVersion}` : current,
|
|
46
|
+
description: installedVersion
|
|
47
|
+
? `v${installedVersion} installed — restart mixdog to apply.`
|
|
48
|
+
: 'Installed mixdog version.',
|
|
43
49
|
_action: 'current',
|
|
44
50
|
},
|
|
45
51
|
{
|
|
@@ -73,13 +79,17 @@ export function createMaintenancePickers({
|
|
|
73
79
|
buttons: [
|
|
74
80
|
{
|
|
75
81
|
value: 'update-now',
|
|
76
|
-
label:
|
|
77
|
-
? `
|
|
78
|
-
:
|
|
82
|
+
label: installedVersion
|
|
83
|
+
? `v${installedVersion} installed — restart to apply`
|
|
84
|
+
: (status.phase === 'installing'
|
|
85
|
+
? 'Installing…'
|
|
86
|
+
: (upd.updateAvailable
|
|
87
|
+
? `Update to v${upd.latestVersion || 'latest'}`
|
|
88
|
+
: 'Update now')),
|
|
79
89
|
},
|
|
80
90
|
],
|
|
81
91
|
onConfirm: (button) => {
|
|
82
|
-
if (button?.value === 'update-now') runUpdate();
|
|
92
|
+
if (button?.value === 'update-now' && !installedVersion && status.phase !== 'installing') runUpdate();
|
|
83
93
|
},
|
|
84
94
|
},
|
|
85
95
|
onSelect: (_value, item) => {
|
|
@@ -118,7 +128,7 @@ export function createMaintenancePickers({
|
|
|
118
128
|
void Promise.resolve(store.runUpdateNow?.())
|
|
119
129
|
.then((result) => {
|
|
120
130
|
if (result?.ok) {
|
|
121
|
-
store.pushNotice(`v${result.version} installed — restart to apply`, '
|
|
131
|
+
store.pushNotice(`v${result.version} installed — restart to apply`, 'warn');
|
|
122
132
|
} else {
|
|
123
133
|
store.pushNotice(`Update failed: ${result?.error || 'unknown error'}`, 'error');
|
|
124
134
|
}
|
|
@@ -43,7 +43,7 @@ export function createSettingsPicker({
|
|
|
43
43
|
openUpdatePicker,
|
|
44
44
|
openChannelSettingTypePicker,
|
|
45
45
|
}) {
|
|
46
|
-
const openSettingsPicker = (opts = {}) => {
|
|
46
|
+
const openSettingsPicker = async (opts = {}) => {
|
|
47
47
|
const light = opts.light === true;
|
|
48
48
|
const overrides = opts.overrides || null;
|
|
49
49
|
const heavyCache = light ? settingsHeavyCacheRef.current : null;
|
|
@@ -63,7 +63,7 @@ export function createSettingsPicker({
|
|
|
63
63
|
channelBackend = heavyCache.channelBackend || 'discord';
|
|
64
64
|
} else {
|
|
65
65
|
try {
|
|
66
|
-
channelBackend = store.getChannelSetup?.()?.backend || 'discord';
|
|
66
|
+
channelBackend = (await store.getChannelSetup?.())?.backend || 'discord';
|
|
67
67
|
} catch {
|
|
68
68
|
channelBackend = 'discord';
|
|
69
69
|
}
|
|
@@ -272,6 +272,33 @@ function toolArgPathForRows(item) {
|
|
|
272
272
|
return a?.path ?? a?.file_path ?? a?.file ?? '';
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// Mirror ToolExecution's collapsed agent-card rule: an agent surface collapses
|
|
276
|
+
// to a SINGLE header row unless the detail row carries failure info (the call
|
|
277
|
+
// errored, or the brief/status reads as a failure/cancel). Pending agent cards
|
|
278
|
+
// never carry failure info yet, so they collapse to one row. A header-failure-
|
|
279
|
+
// only card is handled earlier by toolHeaderFailureOnlyForRows (also one row).
|
|
280
|
+
function agentCardKeepsCollapsedDetailForRows(item, normalizedName) {
|
|
281
|
+
if (normalizedName !== 'agent') return false;
|
|
282
|
+
if (toolItemPendingForRows(item)) return false;
|
|
283
|
+
const bgArgs = backgroundArgsForRows(item.args);
|
|
284
|
+
const isError = Boolean(item.isError);
|
|
285
|
+
const hasDisplayResult = toolHasDisplayResultForRows(item);
|
|
286
|
+
const displayedResultText = toolDisplayedResultTextForRows(item);
|
|
287
|
+
const rt = item.result == null ? '' : String(item.result).replace(/\s+$/, '');
|
|
288
|
+
const isAgentResponse = hasDisplayResult && isAgentResponseResultText(rt);
|
|
289
|
+
const briefRaw = summarizeAgentSurfaceBrief(item.name, bgArgs, displayedResultText, {
|
|
290
|
+
isError,
|
|
291
|
+
isResponse: isAgentResponse,
|
|
292
|
+
});
|
|
293
|
+
const brief = String(briefRaw || '').trim();
|
|
294
|
+
const status = String(bgArgs.status || '').toLowerCase();
|
|
295
|
+
const failureText = /\b(cancelled|canceled|failed)\b/i.test(brief)
|
|
296
|
+
|| /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status);
|
|
297
|
+
const agentHeaderFailure = isError && String(bgArgs.error || '').trim() && !hasDisplayResult;
|
|
298
|
+
if (agentHeaderFailure && !brief) return false;
|
|
299
|
+
return isError || failureText;
|
|
300
|
+
}
|
|
301
|
+
|
|
275
302
|
function isShellSurfaceForRows(normalizedName, label = '') {
|
|
276
303
|
const n = String(normalizedName || '').toLowerCase();
|
|
277
304
|
const l = String(label || '').toLowerCase();
|
|
@@ -332,6 +359,11 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
|
|
|
332
359
|
if (toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult)) {
|
|
333
360
|
return TOOL_MARGIN_TOP + 1;
|
|
334
361
|
}
|
|
362
|
+
if (isAgentSurface) {
|
|
363
|
+
return agentCardKeepsCollapsedDetailForRows(item, normalizedName)
|
|
364
|
+
? TOOL_MARGIN_TOP + 1 + 1
|
|
365
|
+
: TOOL_MARGIN_TOP + 1;
|
|
366
|
+
}
|
|
335
367
|
return TOOL_MARGIN_TOP + 1 + 1;
|
|
336
368
|
}
|
|
337
369
|
if (hasRawResult) {
|
|
@@ -341,7 +373,11 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
|
|
|
341
373
|
: { pathArg: toolArgPathForRows(item), isShell: isShellSurfaceForToolItem(item, normalizedName) };
|
|
342
374
|
return TOOL_MARGIN_TOP + 1 + estimateToolRenderedResultRows(estimateText, { ...rawOpts, columns });
|
|
343
375
|
}
|
|
344
|
-
if (isAgentSurface && !hasResult)
|
|
376
|
+
if (isAgentSurface && !hasResult) {
|
|
377
|
+
return agentCardKeepsCollapsedDetailForRows(item, normalizedName)
|
|
378
|
+
? TOOL_MARGIN_TOP + 1 + 1
|
|
379
|
+
: TOOL_MARGIN_TOP + 1;
|
|
380
|
+
}
|
|
345
381
|
if (isSkillSurface && !hasResult) return TOOL_MARGIN_TOP + 1;
|
|
346
382
|
if (item.aggregate) {
|
|
347
383
|
if (hasRawResult) {
|
|
@@ -18,8 +18,11 @@ const MOUSE_TRACKING_OFF = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
|
18
18
|
// conversion WINS over WT's native ctrl+wheel font zoom. During the zoom
|
|
19
19
|
// passthrough window we must turn BOTH mouse tracking and alternate scroll
|
|
20
20
|
// off, or ctrl+wheel lands as Up/Down (prompt history) instead of zooming.
|
|
21
|
+
// 1007 is kept OFF for the entire session (index.jsx boots with ?1007l and
|
|
22
|
+
// the restore below re-asserts it): if mouse tracking ever drops while 1007
|
|
23
|
+
// is on, every wheel notch turns into prompt-history Up/Down. Off means a
|
|
24
|
+
// degraded wheel is a no-op, never history navigation.
|
|
21
25
|
const ALT_SCROLL_OFF = '\x1b[?1007l';
|
|
22
|
-
const ALT_SCROLL_ON = '\x1b[?1007h';
|
|
23
26
|
const MOUSE_MODIFIER_MASK = 4 | 8 | 16;
|
|
24
27
|
const MOUSE_CTRL_MASK = 16;
|
|
25
28
|
// Bit 2 (4) of the SGR button byte = shift held during the click. Wheel/ctrl
|
|
@@ -76,15 +79,25 @@ export function useMouseInput({
|
|
|
76
79
|
} catch {
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
// Re-enable with RETRIES: a single swallowed write failure (EAGAIN
|
|
83
|
+
// backpressure is real on Windows console streams) used to leave mouse
|
|
84
|
+
// tracking off for the rest of the session — wheel scroll dead, and with
|
|
85
|
+
// alternate scroll enabled the wheel then typed into prompt history.
|
|
86
|
+
const restore = (attempt) => {
|
|
81
87
|
zoomTimerRef.current = null;
|
|
82
88
|
try {
|
|
83
|
-
stdout.write(
|
|
89
|
+
stdout.write(MOUSE_TRACKING_ON + ALT_SCROLL_OFF);
|
|
84
90
|
} catch {
|
|
85
|
-
// The terminal may
|
|
91
|
+
// The terminal may be closing — but it may also be transient
|
|
92
|
+
// backpressure; retry a few times before giving up.
|
|
93
|
+
if (attempt < 5) {
|
|
94
|
+
zoomTimerRef.current = setTimeout(() => restore(attempt + 1), 200);
|
|
95
|
+
zoomTimerRef.current.unref?.();
|
|
96
|
+
}
|
|
86
97
|
}
|
|
87
|
-
}
|
|
98
|
+
};
|
|
99
|
+
if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
|
|
100
|
+
zoomTimerRef.current = setTimeout(() => restore(0), 700);
|
|
88
101
|
zoomTimerRef.current.unref?.();
|
|
89
102
|
}, [stdout, zoomTimerRef]);
|
|
90
103
|
|
|
@@ -198,9 +198,10 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
198
198
|
// Mirror estimateTranscriptItemRows: a non-aggregate skill surface collapses
|
|
199
199
|
// to a single header row; everything else reserves header + one detail row.
|
|
200
200
|
const placeholderNormalizedName = String(formatToolSurface(name, args)?.normalizedName || '').toLowerCase();
|
|
201
|
-
// Skill surfaces collapse to a single header row
|
|
202
|
-
//
|
|
203
|
-
const placeholderSingleRow = !aggregate
|
|
201
|
+
// Skill AND agent surfaces collapse to a single header row when collapsed
|
|
202
|
+
// (see estimateTranscriptItemRows); reserve one row for both.
|
|
203
|
+
const placeholderSingleRow = !aggregate
|
|
204
|
+
&& (SKILL_SURFACE_NAMES.has(placeholderNormalizedName) || isAgentTool(placeholderNormalizedName));
|
|
204
205
|
return (
|
|
205
206
|
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
206
207
|
<Text> </Text>
|
|
@@ -443,11 +444,18 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
443
444
|
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure && !showRawResult) {
|
|
444
445
|
visibleDetailLines = [];
|
|
445
446
|
} else if (isAgentSurfaceCard && !showRawResult) {
|
|
447
|
+
// Agent cards collapse to a SINGLE header row like skill surfaces. Keep the
|
|
448
|
+
// ⎿ detail row ONLY when it carries failure info: the call errored, or the
|
|
449
|
+
// brief/summary reads as a failure/cancel. A header-failure-only card (no
|
|
450
|
+
// brief) shows the cause in the header, so it stays a single row too.
|
|
451
|
+
// Expanded (ctrl+o raw) still flows through the showRawResult path above.
|
|
446
452
|
const agentDetailFallback = collapsedDetail
|
|
447
453
|
|| (pending ? (pendingDetailPlaceholder || 'Running') : 'Finished');
|
|
448
454
|
const agentDetailLine = agentSurfaceBrief
|
|
449
455
|
|| truncateToWidth(String(agentDetailFallback), Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars));
|
|
450
|
-
|
|
456
|
+
const agentFailureText = /\b(Cancelled|Canceled|Failed)\b/i.test(agentSurfaceBrief || collapsedDetail || '');
|
|
457
|
+
const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
|
|
458
|
+
visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
|
|
451
459
|
}
|
|
452
460
|
const finalStatusColor = toolStatusColor({ pending, groupCount, failedCount, terminalStatus });
|
|
453
461
|
const dotColor = finalStatusColor;
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* - Widen ONLY the two problem ranges below. NEVER widen box-drawing
|
|
13
13
|
* (U+2500–U+257F └ │ ⎿ ─), block elements, or the figures.mjs glyphs —
|
|
14
14
|
* those are also EAW-ambiguous but must stay 1 cell.
|
|
15
|
-
* - Enabled by default
|
|
15
|
+
* - Enabled by default on Windows (process.platform === 'win32') or under
|
|
16
|
+
* Windows Terminal (WT_SESSION set — covers WT reached via e.g. SSH from
|
|
17
|
+
* a non-Windows host).
|
|
16
18
|
* - MIXDOG_TUI_AMBIGUOUS_WIDE overrides the default: '1' forces on, '0'
|
|
17
19
|
* forces off; the override always wins.
|
|
18
20
|
* - When OFF, behaviour is byte-for-byte identical to plain string-width.
|
|
@@ -43,12 +45,16 @@ export function isProblemCodePoint(cp) {
|
|
|
43
45
|
// vendor/ink/build/display-width.js.
|
|
44
46
|
const PROBLEM_RE = /[\u2190-\u21ff\u2460-\u24ff]/;
|
|
45
47
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the wide policy from env. Override wins over the default; the
|
|
50
|
+
* default is ON on Windows (WT_SESSION does not reliably propagate to child
|
|
51
|
+
* processes, so gate on the OS itself) or when WT_SESSION is present.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveAmbiguousWidePolicy(env = process.env, platform = process.platform) {
|
|
48
54
|
const override = env?.MIXDOG_TUI_AMBIGUOUS_WIDE;
|
|
49
55
|
if (override === '1') return true;
|
|
50
56
|
if (override === '0') return false;
|
|
51
|
-
return Boolean(env?.WT_SESSION);
|
|
57
|
+
return platform === 'win32' || Boolean(env?.WT_SESSION);
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** Resolved once at module load (matches the "computed once" requirement). */
|