mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
9a3d96be76
* fix(#2697): automated Cursor CLI fix * fix: address Codex review on PR #2734 * fix(ai): keep MCP absolute approval deadline and Escape reject Canceling idle review no longer leaves MCP approvals unbounded past the external agent tool-call window, and Enter-only key stopPropagation lets Escape still reject from Copy/Expand controls. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ai): keep wrapped CLI target context on approval cards Parse --session from netcatty-tool-cli wrappers into execution context and still show invocation args when the pending card displays an unwrapped command. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ai): surface Codex reason on pending approval cards Keep agent justification visible in the execution-context strip and still show extra invocation args such as commandActions beside the command block. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ai): bound Catty approval cancel with absolute deadline Keep Catty confirm-mode approvals from going unbounded after review: cancelApprovalTimeout re-arms the creation absolute deadline (MCP-style) so a late approve cannot outlive the original timeout/stream tool budget. Also harden pending-card Enter: extract isNestedInteractiveApprovalTarget so Copy/Expand (and other nested controls) never Approve Once via bubble. * fix(ai): bound Codex approval cancel and unwrap CLI wrappers cancelInteractionTimeout now re-arms the absolute creation deadline (Catty/MCP style) instead of leaving review unbounded. Also treat netcatty-tool-cli.cjs/.cmd as CLI wrappers so approval Command/Copy shows the remote command after --. * fix: address Codex review on PR #2734 * fix(ai): separate Catty approval idle timer from hard deadline Review activity re-arms a full idle window from now instead of the original absolute remainder, capped by a hard deadline (3x idle, max 30m). Stream tool budgets use the hard deadline so mid-review approvals are not cut off. * fix(ai): re-arm Catty approval idle and sync-deny hard deadline Review interactions kept a one-shot timeoutCancelledRef, so later cancelApprovalTimeout calls no longer re-armed idle. Remove the one-shot and deny synchronously when the hard deadline has already elapsed so a late approve cannot race setTimeout(0). * fix(ai): re-arm approval idle on args-only scroll review Pending approvals without a display command (Codex file-change/permissions, write tools with JSON args) only show the overflow args block. Wire the same onScroll/onWheel markReviewing path used by the command pre so continuous wheel/trackpad review re-arms Catty and MCP/Codex idle timers. --------- Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com> Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
222 lines
7.4 KiB
TypeScript
222 lines
7.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
approvalArgsHaveExtraContext,
|
|
approvalCommandWasUnwrapped,
|
|
extractApprovalExecutionContext,
|
|
extractDisplayCommand,
|
|
isNestedInteractiveApprovalTarget,
|
|
MAX_TOOL_COMMAND_TOOLTIP_CHARS,
|
|
truncateToolCommandTooltip,
|
|
} from './tool-call';
|
|
|
|
const toolCallSource = readFileSync(
|
|
join(dirname(fileURLToPath(import.meta.url)), 'tool-call.tsx'),
|
|
'utf8',
|
|
);
|
|
|
|
// Codex (SDK) emits command_execution.command as a STRING that wraps the real
|
|
// command in `<shell> -lc '<full>'`. Under Skills + CLI the real command is a
|
|
// netcatty-tool-cli call. The title must unwrap the shell layer first, else the
|
|
// outer quote leaks (the "netcatty: \"" / "netcatty: …md\"" garbage titles).
|
|
|
|
test('unwraps a /bin/zsh -lc string wrapper (codex SDK shape)', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({ command: `/bin/zsh -lc 'echo "hi"'` }),
|
|
'echo "hi"',
|
|
);
|
|
});
|
|
|
|
test('codex Skills+CLI exec: unwrap shell + netcatty-cli -> remote command', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({
|
|
command: `/bin/zsh -lc '"/abs/netcatty-tool-cli" exec --session X -- "uptime"'`,
|
|
}),
|
|
'uptime',
|
|
);
|
|
});
|
|
|
|
test('codex Skills+CLI session subcommand -> friendly title', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({
|
|
command: `/bin/zsh -lc '"/abs/netcatty-tool-cli" session --session X'`,
|
|
}),
|
|
'netcatty: inspect session',
|
|
);
|
|
});
|
|
|
|
test('raw (unwrapped) netcatty-tool-cli exec still works', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({ command: `"/abs/netcatty-tool-cli" exec --session X -- "uptime"` }),
|
|
'uptime',
|
|
);
|
|
});
|
|
|
|
test('netcatty-tool-cli.cjs wrapper still unwraps to remote command', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({
|
|
command: `/bin/zsh -lc '"/abs/netcatty-tool-cli.cjs" exec --session X -- "uptime"'`,
|
|
}),
|
|
'uptime',
|
|
);
|
|
assert.equal(
|
|
extractDisplayCommand({
|
|
command: `"/Resources/netcatty-tool-cli.cjs" exec --session X -- "df -h"`,
|
|
}),
|
|
'df -h',
|
|
);
|
|
});
|
|
|
|
test('netcatty-tool-cli.cmd wrapper still unwraps to remote command', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({
|
|
command: `"C:\\\\App\\\\netcatty-tool-cli.cmd" exec --session X -- "whoami"`,
|
|
}),
|
|
'whoami',
|
|
);
|
|
});
|
|
|
|
test('netcatty-tool-cli env -> list sessions', () => {
|
|
assert.equal(extractDisplayCommand({ command: 'netcatty-tool-cli env' }), 'netcatty: list sessions');
|
|
});
|
|
|
|
test('array shell-wrap shape still unwraps (regression)', () => {
|
|
assert.equal(
|
|
extractDisplayCommand({ command: ['zsh', '-lc', 'ls -la /tmp'] }),
|
|
'ls -la /tmp',
|
|
);
|
|
});
|
|
|
|
test('plain command passes through unchanged', () => {
|
|
assert.equal(extractDisplayCommand({ command: 'ls -la /tmp' }), 'ls -la /tmp');
|
|
});
|
|
|
|
test('limits long command tooltips to a compact single-line preview', () => {
|
|
const tooltip = truncateToolCommandTooltip(` echo first\n${'x'.repeat(300)} `);
|
|
assert.equal(tooltip.length, MAX_TOOL_COMMAND_TOOLTIP_CHARS);
|
|
assert.equal(tooltip.endsWith('…'), true);
|
|
assert.equal(tooltip.includes('\n'), false);
|
|
});
|
|
|
|
test('empty / missing args -> null', () => {
|
|
assert.equal(extractDisplayCommand(undefined), null);
|
|
assert.equal(extractDisplayCommand({ command: '' }), null);
|
|
});
|
|
|
|
test('extractApprovalExecutionContext surfaces session/cwd/shell without rewriting command', () => {
|
|
assert.deepEqual(
|
|
extractApprovalExecutionContext({
|
|
sessionId: 'term-1',
|
|
cwd: '/var/log',
|
|
command: ['zsh', '-lc', 'df -h | sort'],
|
|
}),
|
|
{ sessionId: 'term-1', cwd: '/var/log', shell: 'zsh', reason: undefined },
|
|
);
|
|
assert.deepEqual(
|
|
extractApprovalExecutionContext({
|
|
command: `/bin/bash -lc 'uptime'`,
|
|
}),
|
|
{ sessionId: undefined, cwd: undefined, shell: 'bash', reason: undefined },
|
|
);
|
|
assert.equal(extractApprovalExecutionContext({ path: '/tmp' }), null);
|
|
});
|
|
|
|
test('extractApprovalExecutionContext reads --session from netcatty-tool-cli wrappers', () => {
|
|
assert.deepEqual(
|
|
extractApprovalExecutionContext({
|
|
command: `/bin/zsh -lc '"/abs/netcatty-tool-cli" exec --session term-9 --chat-session chat-1 -- "uptime"'`,
|
|
}),
|
|
{ sessionId: 'term-9', cwd: undefined, shell: 'zsh', reason: undefined },
|
|
);
|
|
});
|
|
|
|
test('extractApprovalExecutionContext surfaces Codex reason for approval review', () => {
|
|
assert.deepEqual(
|
|
extractApprovalExecutionContext({
|
|
command: 'rm -rf /tmp/x',
|
|
cwd: '/tmp',
|
|
reason: 'Clean stale build artifacts',
|
|
commandActions: [{ type: 'delete' }],
|
|
}),
|
|
{
|
|
sessionId: undefined,
|
|
cwd: '/tmp',
|
|
shell: undefined,
|
|
reason: 'Clean stale build artifacts',
|
|
},
|
|
);
|
|
});
|
|
|
|
test('approvalCommandWasUnwrapped detects Skills+CLI display unwrap', () => {
|
|
const args = {
|
|
command: `/bin/zsh -lc '"/abs/netcatty-tool-cli" exec --session X -- "uptime"'`,
|
|
};
|
|
const display = extractDisplayCommand(args);
|
|
assert.equal(display, 'uptime');
|
|
assert.equal(approvalCommandWasUnwrapped(args, display), true);
|
|
assert.equal(approvalCommandWasUnwrapped({ command: 'uptime' }, 'uptime'), false);
|
|
});
|
|
|
|
test('approvalArgsHaveExtraContext keeps commandActions visible beside the command block', () => {
|
|
assert.equal(
|
|
approvalArgsHaveExtraContext({
|
|
command: 'echo hi',
|
|
reason: 'demo',
|
|
commandActions: [{ type: 'read' }],
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
approvalArgsHaveExtraContext({ command: 'echo hi', cwd: '/tmp', reason: 'demo' }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('isNestedInteractiveApprovalTarget ignores Enter on Copy/Expand review controls', () => {
|
|
const card = { id: 'card' };
|
|
const copyBtn = {
|
|
closest: (selector: string) => (selector.includes('button') ? copyBtn : null),
|
|
};
|
|
const plainSpan = {
|
|
closest: () => null,
|
|
};
|
|
|
|
assert.equal(isNestedInteractiveApprovalTarget(copyBtn, card), true);
|
|
assert.equal(isNestedInteractiveApprovalTarget(plainSpan, card), false);
|
|
assert.equal(isNestedInteractiveApprovalTarget(card, card), false);
|
|
assert.equal(isNestedInteractiveApprovalTarget(null, card), false);
|
|
});
|
|
|
|
// Pending approvals with no display command (Codex file-change/permissions,
|
|
// write tools with JSON-only args) only render the overflow args <pre>. Wheel /
|
|
// trackpad scroll must re-arm idle the same way the command block does.
|
|
test('args overflow block re-arms review timers on scroll/wheel while pending', () => {
|
|
assert.match(
|
|
toolCallSource,
|
|
/JSON\.stringify\(args,\s*null,\s*2\)[\s\S]{0,200}?<\/pre>/,
|
|
);
|
|
// Both review surfaces (command + args) wire markReviewing for continuous scroll.
|
|
const markReviewingScrollBindings = toolCallSource.match(
|
|
/onScroll=\{(?:isPendingApproval \? markReviewing : undefined|markReviewing)\}/g,
|
|
);
|
|
const markReviewingWheelBindings = toolCallSource.match(
|
|
/onWheel=\{(?:isPendingApproval \? markReviewing : undefined|markReviewing)\}/g,
|
|
);
|
|
assert.ok(
|
|
markReviewingScrollBindings && markReviewingScrollBindings.length >= 2,
|
|
'command and args overflow blocks must both call markReviewing onScroll',
|
|
);
|
|
assert.ok(
|
|
markReviewingWheelBindings && markReviewingWheelBindings.length >= 2,
|
|
'command and args overflow blocks must both call markReviewing onWheel',
|
|
);
|
|
assert.match(
|
|
toolCallSource,
|
|
/onScroll=\{isPendingApproval \? markReviewing : undefined\}[\s\S]{0,80}onWheel=\{isPendingApproval \? markReviewing : undefined\}[\s\S]{0,80}JSON\.stringify\(args/,
|
|
);
|
|
});
|