mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -15,12 +15,12 @@ import type {
|
||||
RootContent,
|
||||
} from 'hast';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import { Theme } from '../themes/theme.js';
|
||||
import type { Theme } from '../themes/theme.js';
|
||||
import {
|
||||
MaxSizedBox,
|
||||
MINIMUM_MAX_HEIGHT,
|
||||
} from '../components/shared/MaxSizedBox.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
// Configure theming and parsing utilities.
|
||||
const lowlight = createLowlight(common);
|
||||
@@ -134,7 +134,7 @@ export function colorizeCode(
|
||||
): React.ReactNode {
|
||||
const codeToHighlight = code.replace(/\n$/, '');
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
const showLineNumbers = settings?.merged.showLineNumbers ?? true;
|
||||
const showLineNumbers = settings?.merged.ui?.showLineNumbers ?? true;
|
||||
|
||||
try {
|
||||
// Render the HAST tree using the adapted theme
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import util from 'util';
|
||||
import { ConsoleMessageItem } from '../types.js';
|
||||
import util from 'node:util';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
|
||||
interface ConsolePatcherParams {
|
||||
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { MarkdownDisplay } from './MarkdownDisplay.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import { EOL } from 'node:os';
|
||||
|
||||
describe('<MarkdownDisplay />', () => {
|
||||
const baseProps = {
|
||||
@@ -21,7 +22,10 @@ describe('<MarkdownDisplay />', () => {
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: {} },
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -53,7 +57,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
## Header 2
|
||||
### Header 3
|
||||
#### Header 4
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -63,7 +67,10 @@ describe('<MarkdownDisplay />', () => {
|
||||
});
|
||||
|
||||
it('renders a fenced code block with a language', () => {
|
||||
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```';
|
||||
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace(
|
||||
/\n/g,
|
||||
EOL,
|
||||
);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -73,7 +80,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
});
|
||||
|
||||
it('renders a fenced code block without a language', () => {
|
||||
const text = '```\nplain text\n```';
|
||||
const text = '```\nplain text\n```'.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -83,7 +90,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
});
|
||||
|
||||
it('handles unclosed (pending) code blocks', () => {
|
||||
const text = '```typescript\nlet y = 2;';
|
||||
const text = '```typescript\nlet y = 2;'.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} isPending={true} />
|
||||
@@ -97,7 +104,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
- item A
|
||||
* item B
|
||||
+ item C
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -111,7 +118,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
* Level 1
|
||||
* Level 2
|
||||
* Level 3
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -124,7 +131,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
const text = `
|
||||
1. First item
|
||||
2. Second item
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -140,7 +147,7 @@ Hello
|
||||
World
|
||||
***
|
||||
Test
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -155,7 +162,7 @@ Test
|
||||
|----------|:--------:|
|
||||
| Cell 1 | Cell 2 |
|
||||
| Cell 3 | Cell 4 |
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -169,7 +176,7 @@ Test
|
||||
Some text before.
|
||||
| A | B |
|
||||
|---|
|
||||
| 1 | 2 |`;
|
||||
| 1 | 2 |`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -181,7 +188,7 @@ Some text before.
|
||||
it('inserts a single space between paragraphs', () => {
|
||||
const text = `Paragraph 1.
|
||||
|
||||
Paragraph 2.`;
|
||||
Paragraph 2.`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -204,7 +211,7 @@ some code
|
||||
\`\`\`
|
||||
|
||||
Another paragraph.
|
||||
`;
|
||||
`.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
@@ -214,12 +221,15 @@ Another paragraph.
|
||||
});
|
||||
|
||||
it('hides line numbers in code blocks when showLineNumbers is false', () => {
|
||||
const text = '```javascript\nconst x = 1;\n```';
|
||||
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL);
|
||||
const settings = new LoadedSettings(
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: { showLineNumbers: false } },
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: { ui: { showLineNumbers: false } } },
|
||||
{ path: '', settings: {} },
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = render(
|
||||
@@ -232,7 +242,7 @@ Another paragraph.
|
||||
});
|
||||
|
||||
it('shows line numbers in code blocks by default', () => {
|
||||
const text = '```javascript\nconst x = 1;\n```';
|
||||
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL);
|
||||
const { lastFrame } = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<MarkdownDisplay {...baseProps} text={text} />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { EOL } from 'node:os';
|
||||
import { Colors } from '../colors.js';
|
||||
import { colorizeCode } from './CodeColorizer.js';
|
||||
import { TableRenderer } from './TableRenderer.js';
|
||||
@@ -34,7 +35,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
||||
}) => {
|
||||
if (!text) return <></>;
|
||||
|
||||
const lines = text.split('\n');
|
||||
const lines = text.split(EOL);
|
||||
const headerRegex = /^ *(#{1,4}) +(.*)/;
|
||||
const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/;
|
||||
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/;
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, Mock } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import type { spawn, SpawnOptions } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import {
|
||||
isAtCommand,
|
||||
isSlashCommand,
|
||||
@@ -44,7 +45,7 @@ describe('commandUtils', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// Dynamically import and set up spawn mock
|
||||
const { spawn } = await import('child_process');
|
||||
const { spawn } = await import('node:child_process');
|
||||
mockSpawn = spawn as Mock;
|
||||
|
||||
// Create mock child process with stdout/stderr emitters
|
||||
@@ -101,6 +102,20 @@ describe('commandUtils', () => {
|
||||
expect(isSlashCommand('path/to/file')).toBe(false);
|
||||
expect(isSlashCommand(' /help')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for line comments starting with //', () => {
|
||||
expect(isSlashCommand('// This is a comment')).toBe(false);
|
||||
expect(isSlashCommand('// check if variants base info all filled.')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isSlashCommand('//comment without space')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for block comments starting with /*', () => {
|
||||
expect(isSlashCommand('/* This is a block comment */')).toBe(false);
|
||||
expect(isSlashCommand('/*\n * Multi-line comment\n */')).toBe(false);
|
||||
expect(isSlashCommand('/*comment without space*/')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyToClipboard', () => {
|
||||
@@ -186,6 +201,9 @@ describe('commandUtils', () => {
|
||||
|
||||
it('should successfully copy text to clipboard using xclip', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
@@ -193,10 +211,11 @@ describe('commandUtils', () => {
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('xclip', [
|
||||
'-selection',
|
||||
'clipboard',
|
||||
]);
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
@@ -204,6 +223,9 @@ describe('commandUtils', () => {
|
||||
it('should fall back to xsel when xclip fails', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
@@ -217,7 +239,9 @@ describe('commandUtils', () => {
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails
|
||||
child.stderr.emit('data', 'xclip not found');
|
||||
const error = new Error('spawn xclip ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
@@ -232,19 +256,26 @@ describe('commandUtils', () => {
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(1, 'xclip', [
|
||||
'-selection',
|
||||
'clipboard',
|
||||
]);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(2, 'xsel', [
|
||||
'--clipboard',
|
||||
'--input',
|
||||
]);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when both xclip and xsel fail', async () => {
|
||||
it('should throw error when both xclip and xsel are not found', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
@@ -253,29 +284,99 @@ describe('commandUtils', () => {
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
});
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails
|
||||
child.stderr.emit('data', 'xclip command not found');
|
||||
// First call (xclip) fails with ENOENT
|
||||
const error = new Error('spawn xclip ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
// Second call (xsel) fails
|
||||
child.stderr.emit('data', 'xsel command not found');
|
||||
// Second call (xsel) fails with ENOENT
|
||||
const error = new Error('spawn xsel ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
/All copy commands failed/,
|
||||
'Please ensure xclip or xsel is installed and configured.',
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit error when xclip or xsel fail with stderr output (command installed)', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
const errorMsg = "Error: Can't open display:";
|
||||
const exitCode = 1;
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
// e.g., cannot connect to X server
|
||||
if (callCount === 0) {
|
||||
child.stderr.emit('data', errorMsg);
|
||||
child.emit('close', exitCode);
|
||||
callCount++;
|
||||
} else {
|
||||
child.stderr.emit('data', errorMsg);
|
||||
child.emit('close', exitCode);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
const xclipErrorMsg = `'xclip' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
|
||||
const xselErrorMsg = `'xsel' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
`All copy commands failed. "${xclipErrorMsg}", "${xselErrorMsg}". `,
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import type { SpawnOptions } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
/**
|
||||
* Checks if a query string potentially represents an '@' command.
|
||||
@@ -20,20 +21,38 @@ export const isAtCommand = (query: string): boolean =>
|
||||
|
||||
/**
|
||||
* Checks if a query string potentially represents an '/' command.
|
||||
* It triggers if the query starts with '/'
|
||||
* It triggers if the query starts with '/' but excludes code comments like '//' and '/*'.
|
||||
*
|
||||
* @param query The input query string.
|
||||
* @returns True if the query looks like an '/' command, false otherwise.
|
||||
*/
|
||||
export const isSlashCommand = (query: string): boolean => query.startsWith('/');
|
||||
export const isSlashCommand = (query: string): boolean => {
|
||||
if (!query.startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude line comments that start with '//'
|
||||
if (query.startsWith('//')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude block comments that start with '/*'
|
||||
if (query.startsWith('/*')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Copies a string snippet to the clipboard for different platforms
|
||||
export const copyToClipboard = async (text: string): Promise<void> => {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
const run = (cmd: string, args: string[], options?: SpawnOptions) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(cmd, args);
|
||||
const child = options ? spawn(cmd, args, options) : spawn(cmd, args);
|
||||
let stderr = '';
|
||||
child.stderr.on('data', (chunk) => (stderr += chunk.toString()));
|
||||
if (child.stderr) {
|
||||
child.stderr.on('data', (chunk) => (stderr += chunk.toString()));
|
||||
}
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) return resolve();
|
||||
@@ -44,11 +63,21 @@ export const copyToClipboard = async (text: string): Promise<void> => {
|
||||
),
|
||||
);
|
||||
});
|
||||
child.stdin.on('error', reject);
|
||||
child.stdin.write(text);
|
||||
child.stdin.end();
|
||||
if (child.stdin) {
|
||||
child.stdin.on('error', reject);
|
||||
child.stdin.write(text);
|
||||
child.stdin.end();
|
||||
} else {
|
||||
reject(new Error('Child process has no stdin stream to write to.'));
|
||||
}
|
||||
});
|
||||
|
||||
// Configure stdio for Linux clipboard commands.
|
||||
// - stdin: 'pipe' to write the text that needs to be copied.
|
||||
// - stdout: 'inherit' since we don't need to capture the command's output on success.
|
||||
// - stderr: 'pipe' to capture error messages (e.g., "command not found") for better error handling.
|
||||
const linuxOptions: SpawnOptions = { stdio: ['pipe', 'inherit', 'pipe'] };
|
||||
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return run('clip', []);
|
||||
@@ -56,22 +85,41 @@ export const copyToClipboard = async (text: string): Promise<void> => {
|
||||
return run('pbcopy', []);
|
||||
case 'linux':
|
||||
try {
|
||||
await run('xclip', ['-selection', 'clipboard']);
|
||||
await run('xclip', ['-selection', 'clipboard'], linuxOptions);
|
||||
} catch (primaryError) {
|
||||
try {
|
||||
// If xclip fails for any reason, try xsel as a fallback.
|
||||
await run('xsel', ['--clipboard', '--input']);
|
||||
await run('xsel', ['--clipboard', '--input'], linuxOptions);
|
||||
} catch (fallbackError) {
|
||||
const primaryMsg =
|
||||
const xclipNotFound =
|
||||
primaryError instanceof Error &&
|
||||
(primaryError as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
const xselNotFound =
|
||||
fallbackError instanceof Error &&
|
||||
(fallbackError as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
if (xclipNotFound && xselNotFound) {
|
||||
throw new Error(
|
||||
'Please ensure xclip or xsel is installed and configured.',
|
||||
);
|
||||
}
|
||||
|
||||
let primaryMsg =
|
||||
primaryError instanceof Error
|
||||
? primaryError.message
|
||||
: String(primaryError);
|
||||
const fallbackMsg =
|
||||
if (xclipNotFound) {
|
||||
primaryMsg = `xclip not found`;
|
||||
}
|
||||
let fallbackMsg =
|
||||
fallbackError instanceof Error
|
||||
? fallbackError.message
|
||||
: String(fallbackError);
|
||||
if (xselNotFound) {
|
||||
fallbackMsg = `xsel not found`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`All copy commands failed. xclip: "${primaryMsg}", xsel: "${fallbackMsg}". Please ensure xclip or xsel is installed and configured.`,
|
||||
`All copy commands failed. "${primaryMsg}", "${fallbackMsg}". `,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
calculateErrorRate,
|
||||
computeSessionStats,
|
||||
} from './computeStats.js';
|
||||
import { ModelMetrics, SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import type {
|
||||
ModelMetrics,
|
||||
SessionMetrics,
|
||||
} from '../contexts/SessionContext.js';
|
||||
|
||||
describe('calculateErrorRate', () => {
|
||||
it('should return 0 if totalRequests is 0', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
import type {
|
||||
SessionMetrics,
|
||||
ComputedSessionStats,
|
||||
ModelMetrics,
|
||||
|
||||
@@ -50,3 +50,11 @@ export const BACKSLASH_ENTER_DETECTION_WINDOW_MS = 5;
|
||||
* We use 12 to provide a small buffer.
|
||||
*/
|
||||
export const MAX_KITTY_SEQUENCE_LENGTH = 12;
|
||||
|
||||
/**
|
||||
* Character codes for common escape sequences
|
||||
*/
|
||||
export const CHAR_CODE_ESC = 27;
|
||||
export const CHAR_CODE_LEFT_BRACKET = 91;
|
||||
export const CHAR_CODE_1 = 49;
|
||||
export const CHAR_CODE_2 = 50;
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
* to avoid conflicts with user customizations.
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { isKittyProtocolEnabled } from './kittyProtocolDetector.js';
|
||||
import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js';
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { stripVTControlCharacters } from 'node:util';
|
||||
|
||||
/**
|
||||
* Calculates the maximum width of a multi-line ASCII art string.
|
||||
* @param asciiArt The ASCII art string.
|
||||
@@ -38,3 +41,48 @@ export function cpSlice(str: string, start: number, end?: number): string {
|
||||
const arr = toCodePoints(str).slice(start, end);
|
||||
return arr.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip characters that can break terminal rendering.
|
||||
*
|
||||
* Uses Node.js built-in stripVTControlCharacters to handle VT sequences,
|
||||
* then filters remaining control characters that can disrupt display.
|
||||
*
|
||||
* Characters stripped:
|
||||
* - ANSI escape sequences (via strip-ansi)
|
||||
* - VT control sequences (via Node.js util.stripVTControlCharacters)
|
||||
* - C0 control chars (0x00-0x1F) except CR/LF which are handled elsewhere
|
||||
* - C1 control chars (0x80-0x9F) that can cause display issues
|
||||
*
|
||||
* Characters preserved:
|
||||
* - All printable Unicode including emojis
|
||||
* - DEL (0x7F) - handled functionally by applyOperations, not a display issue
|
||||
* - CR/LF (0x0D/0x0A) - needed for line breaks
|
||||
*/
|
||||
export function stripUnsafeCharacters(str: string): string {
|
||||
const strippedAnsi = stripAnsi(str);
|
||||
const strippedVT = stripVTControlCharacters(strippedAnsi);
|
||||
|
||||
return toCodePoints(strippedVT)
|
||||
.filter((char) => {
|
||||
const code = char.codePointAt(0);
|
||||
if (code === undefined) return false;
|
||||
|
||||
// Preserve CR/LF for line handling
|
||||
if (code === 0x0a || code === 0x0d) return true;
|
||||
|
||||
// Remove C0 control chars (except CR/LF) that can break display
|
||||
// Examples: BELL(0x07) makes noise, BS(0x08) moves cursor, VT(0x0B), FF(0x0C)
|
||||
if (code >= 0x00 && code <= 0x1f) return false;
|
||||
|
||||
// Remove C1 control chars (0x80-0x9f) - legacy 8-bit control codes
|
||||
if (code >= 0x80 && code <= 0x9f) return false;
|
||||
|
||||
// Preserve DEL (0x7f) - it's handled functionally by applyOperations as backspace
|
||||
// and doesn't cause rendering issues when displayed
|
||||
|
||||
// Preserve all other characters including Unicode/emojis
|
||||
return true;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import updateNotifier, { UpdateInfo } from 'update-notifier';
|
||||
import type { UpdateInfo } from 'update-notifier';
|
||||
import updateNotifier from 'update-notifier';
|
||||
import semver from 'semver';
|
||||
import { getPackageJson } from '../../utils/package.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user