mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 17:27:54 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -4,25 +4,25 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
expect,
|
||||
it,
|
||||
describe,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
MockInstance,
|
||||
} from 'vitest';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { getProjectHash } from '../utils/paths.js';
|
||||
import {
|
||||
ChatRecordingService,
|
||||
ConversationRecord,
|
||||
ToolCallRecord,
|
||||
type ConversationRecord,
|
||||
type ToolCallRecord,
|
||||
} from './chatRecordingService.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { getProjectHash } from '../utils/paths.js';
|
||||
|
||||
vi.mock('node:fs');
|
||||
vi.mock('node:path');
|
||||
@@ -40,9 +40,11 @@ describe('ChatRecordingService', () => {
|
||||
mockConfig = {
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
|
||||
getProjectTempDir: vi
|
||||
.fn()
|
||||
.mockReturnValue('/test/project/root/.gemini/tmp'),
|
||||
storage: {
|
||||
getProjectTempDir: vi
|
||||
.fn()
|
||||
.mockReturnValue('/test/project/root/.gemini/tmp'),
|
||||
},
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getProjectHash } from '../utils/paths.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { PartListUnion } from '@google/genai';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
|
||||
/**
|
||||
* Token usage summary for a message or conversation.
|
||||
@@ -136,7 +136,10 @@ export class ChatRecordingService {
|
||||
this.cachedLastConvData = null;
|
||||
} else {
|
||||
// Create new session
|
||||
const chatsDir = path.join(this.config.getProjectTempDir(), 'chats');
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
'chats',
|
||||
);
|
||||
fs.mkdirSync(chatsDir, { recursive: true });
|
||||
|
||||
const timestamp = new Date()
|
||||
@@ -422,7 +425,10 @@ export class ChatRecordingService {
|
||||
*/
|
||||
deleteSession(sessionId: string): void {
|
||||
try {
|
||||
const chatsDir = path.join(this.config.getProjectTempDir(), 'chats');
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
'chats',
|
||||
);
|
||||
const sessionPath = path.join(chatsDir, `${sessionId}.json`);
|
||||
fs.unlinkSync(sessionPath);
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { FileDiscoveryService } from './fileDiscoveryService.js';
|
||||
|
||||
describe('FileDiscoveryService', () => {
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { GitIgnoreParser, GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
||||
import type { GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
||||
import { GitIgnoreParser } from '../utils/gitIgnoreParser.js';
|
||||
import { isGitRepository } from '../utils/gitUtils.js';
|
||||
import * as path from 'path';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const GEMINI_IGNORE_FILE_NAME = '.geminiignore';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'node:fs/promises';
|
||||
import { StandardFileSystemService } from './fileSystemService.js';
|
||||
|
||||
vi.mock('fs/promises');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
/**
|
||||
* Interface for file system operations that may be delegated to different implementations
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { GitService } from './gitService.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as os from 'node:os';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||
|
||||
@@ -55,6 +56,7 @@ describe('GitService', () => {
|
||||
let projectRoot: string;
|
||||
let homedir: string;
|
||||
let hash: string;
|
||||
let storage: Storage;
|
||||
|
||||
beforeEach(async () => {
|
||||
testRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-service-test-'));
|
||||
@@ -100,6 +102,7 @@ describe('GitService', () => {
|
||||
hoistedMockCommit.mockResolvedValue({
|
||||
commit: 'initial',
|
||||
});
|
||||
storage = new Storage(projectRoot);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -109,13 +112,13 @@ describe('GitService', () => {
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should successfully create an instance', () => {
|
||||
expect(() => new GitService(projectRoot)).not.toThrow();
|
||||
expect(() => new GitService(projectRoot, storage)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyGitAvailability', () => {
|
||||
it('should resolve true if git --version command succeeds', async () => {
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await expect(service.verifyGitAvailability()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
@@ -124,7 +127,7 @@ describe('GitService', () => {
|
||||
callback(new Error('git not found'));
|
||||
return {} as ChildProcess;
|
||||
});
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await expect(service.verifyGitAvailability()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -135,14 +138,14 @@ describe('GitService', () => {
|
||||
callback(new Error('git not found'));
|
||||
return {} as ChildProcess;
|
||||
});
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await expect(service.initialize()).rejects.toThrow(
|
||||
'Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should call setupShadowGitRepository if Git is available', async () => {
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
const setupSpy = vi
|
||||
.spyOn(service, 'setupShadowGitRepository')
|
||||
.mockResolvedValue(undefined);
|
||||
@@ -162,14 +165,14 @@ describe('GitService', () => {
|
||||
});
|
||||
|
||||
it('should create history and repository directories', async () => {
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
const stats = await fs.stat(repoDir);
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('should create a .gitconfig file with the correct content', async () => {
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
|
||||
const expectedConfigContent =
|
||||
@@ -180,7 +183,7 @@ describe('GitService', () => {
|
||||
|
||||
it('should initialize git repo in historyDir if not already initialized', async () => {
|
||||
hoistedMockCheckIsRepo.mockResolvedValue(false);
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
|
||||
expect(hoistedMockInit).toHaveBeenCalled();
|
||||
@@ -188,7 +191,7 @@ describe('GitService', () => {
|
||||
|
||||
it('should not initialize git repo if already initialized', async () => {
|
||||
hoistedMockCheckIsRepo.mockResolvedValue(true);
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
expect(hoistedMockInit).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -198,7 +201,7 @@ describe('GitService', () => {
|
||||
const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
|
||||
await fs.writeFile(visibleGitIgnorePath, gitignoreContent);
|
||||
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
|
||||
const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
|
||||
@@ -207,7 +210,7 @@ describe('GitService', () => {
|
||||
});
|
||||
|
||||
it('should not create a .gitignore in shadow repo if project .gitignore does not exist', async () => {
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
|
||||
const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
|
||||
@@ -221,7 +224,7 @@ describe('GitService', () => {
|
||||
// Create a directory instead of a file to cause a read error
|
||||
await fs.mkdir(visibleGitIgnorePath);
|
||||
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
// EISDIR is the expected error code on Unix-like systems
|
||||
await expect(service.setupShadowGitRepository()).rejects.toThrow(
|
||||
/EISDIR: illegal operation on a directory, read|EBUSY: resource busy or locked, read/,
|
||||
@@ -230,7 +233,7 @@ describe('GitService', () => {
|
||||
|
||||
it('should make an initial commit if no commits exist in history repo', async () => {
|
||||
hoistedMockCheckIsRepo.mockResolvedValue(false);
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
|
||||
'--allow-empty': null,
|
||||
@@ -239,7 +242,7 @@ describe('GitService', () => {
|
||||
|
||||
it('should not make an initial commit if commits already exist', async () => {
|
||||
hoistedMockCheckIsRepo.mockResolvedValue(true);
|
||||
const service = new GitService(projectRoot);
|
||||
const service = new GitService(projectRoot, storage);
|
||||
await service.setupShadowGitRepository();
|
||||
expect(hoistedMockCommit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
import { exec } from 'node:child_process';
|
||||
import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git';
|
||||
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { CheckRepoActions, simpleGit, type SimpleGit } from 'simple-git';
|
||||
import type { Storage } from '../config/storage.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
|
||||
export class GitService {
|
||||
private projectRoot: string;
|
||||
private storage: Storage;
|
||||
|
||||
constructor(projectRoot: string) {
|
||||
constructor(projectRoot: string, storage: Storage) {
|
||||
this.projectRoot = path.resolve(projectRoot);
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
private getHistoryDir(): string {
|
||||
const hash = getProjectHash(this.projectRoot);
|
||||
return path.join(os.homedir(), QWEN_DIR, 'history', hash);
|
||||
return this.storage.getHistoryDir();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -31,7 +31,13 @@ export class GitService {
|
||||
'Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.',
|
||||
);
|
||||
}
|
||||
this.setupShadowGitRepository();
|
||||
try {
|
||||
await this.setupShadowGitRepository();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to initialize checkpointing: ${error instanceof Error ? error.message : 'Unknown error'}. Please check that Git is working properly or disable checkpointing.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
verifyGitAvailability(): Promise<boolean> {
|
||||
@@ -105,10 +111,16 @@ export class GitService {
|
||||
}
|
||||
|
||||
async createFileSnapshot(message: string): Promise<string> {
|
||||
const repo = this.shadowGitRepository;
|
||||
await repo.add('.');
|
||||
const commitResult = await repo.commit(message);
|
||||
return commitResult.commit;
|
||||
try {
|
||||
const repo = this.shadowGitRepository;
|
||||
await repo.add('.');
|
||||
const commitResult = await repo.commit(message);
|
||||
return commitResult.commit;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create checkpoint snapshot: ${error instanceof Error ? error.message : 'Unknown error'}. Checkpointing may not be working properly.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async restoreProjectFromSnapshot(commitHash: string): Promise<void> {
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Config } from '../config/config.js';
|
||||
import { GeminiClient } from '../core/client.js';
|
||||
import {
|
||||
GeminiEventType,
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { GeminiClient } from '../core/client.js';
|
||||
import type {
|
||||
ServerGeminiContentEvent,
|
||||
ServerGeminiStreamEvent,
|
||||
ServerGeminiToolCallRequestEvent,
|
||||
} from '../core/turn.js';
|
||||
import { GeminiEventType } from '../core/turn.js';
|
||||
import * as loggers from '../telemetry/loggers.js';
|
||||
import { LoopType } from '../telemetry/types.js';
|
||||
import { LoopDetectionService } from './loopDetectionService.js';
|
||||
@@ -560,6 +560,30 @@ describe('LoopDetectionService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Divider Content Detection', () => {
|
||||
it('should not detect a loop for repeating divider-like content', () => {
|
||||
service.reset('');
|
||||
const dividerContent = '-'.repeat(CONTENT_CHUNK_SIZE);
|
||||
let isLoop = false;
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD + 5; i++) {
|
||||
isLoop = service.addAndCheck(createContentEvent(dividerContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not detect a loop for repeating complex box-drawing dividers', () => {
|
||||
service.reset('');
|
||||
const dividerContent = '╭─'.repeat(CONTENT_CHUNK_SIZE / 2);
|
||||
let isLoop = false;
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD + 5; i++) {
|
||||
isLoop = service.addAndCheck(createContentEvent(dividerContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset Functionality', () => {
|
||||
it('tool call should reset content count', () => {
|
||||
const contentEvent = createContentEvent('Some content.');
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import { GeminiEventType, ServerGeminiStreamEvent } from '../core/turn.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { ServerGeminiStreamEvent } from '../core/turn.js';
|
||||
import { GeminiEventType } from '../core/turn.js';
|
||||
import { logLoopDetected } from '../telemetry/loggers.js';
|
||||
import { LoopDetectedEvent, LoopType } from '../telemetry/types.js';
|
||||
import { Config, DEFAULT_GEMINI_FLASH_MODEL } from '../config/config.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/config.js';
|
||||
import {
|
||||
isFunctionCall,
|
||||
isFunctionResponse,
|
||||
} from '../utils/messageInspectors.js';
|
||||
|
||||
const TOOL_CALL_LOOP_THRESHOLD = 5;
|
||||
const CONTENT_LOOP_THRESHOLD = 10;
|
||||
@@ -169,8 +176,16 @@ export class LoopDetectionService {
|
||||
/(^|\n)\s*[*-+]\s/.test(content) || /(^|\n)\s*\d+\.\s/.test(content);
|
||||
const hasHeading = /(^|\n)#+\s/.test(content);
|
||||
const hasBlockquote = /(^|\n)>\s/.test(content);
|
||||
const isDivider = /^[+-_=*\u2500-\u257F]+$/.test(content);
|
||||
|
||||
if (numFences || hasTable || hasListItem || hasHeading || hasBlockquote) {
|
||||
if (
|
||||
numFences ||
|
||||
hasTable ||
|
||||
hasListItem ||
|
||||
hasHeading ||
|
||||
hasBlockquote ||
|
||||
isDivider
|
||||
) {
|
||||
// Reset tracking when different content elements are detected to avoid analyzing content
|
||||
// that spans across different element boundaries.
|
||||
this.resetContentTracking();
|
||||
@@ -179,7 +194,7 @@ export class LoopDetectionService {
|
||||
const wasInCodeBlock = this.inCodeBlock;
|
||||
this.inCodeBlock =
|
||||
numFences % 2 === 0 ? this.inCodeBlock : !this.inCodeBlock;
|
||||
if (wasInCodeBlock || this.inCodeBlock) {
|
||||
if (wasInCodeBlock || this.inCodeBlock || isDivider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -318,12 +333,35 @@ export class LoopDetectionService {
|
||||
return originalChunk === currentChunk;
|
||||
}
|
||||
|
||||
private trimRecentHistory(recentHistory: Content[]): Content[] {
|
||||
// A function response must be preceded by a function call.
|
||||
// Continuously removes dangling function calls from the end of the history
|
||||
// until the last turn is not a function call.
|
||||
while (
|
||||
recentHistory.length > 0 &&
|
||||
isFunctionCall(recentHistory[recentHistory.length - 1])
|
||||
) {
|
||||
recentHistory.pop();
|
||||
}
|
||||
|
||||
// A function response should follow a function call.
|
||||
// Continuously removes leading function responses from the beginning of history
|
||||
// until the first turn is not a function response.
|
||||
while (recentHistory.length > 0 && isFunctionResponse(recentHistory[0])) {
|
||||
recentHistory.shift();
|
||||
}
|
||||
|
||||
return recentHistory;
|
||||
}
|
||||
|
||||
private async checkForLoopWithLLM(signal: AbortSignal) {
|
||||
const recentHistory = this.config
|
||||
.getGeminiClient()
|
||||
.getHistory()
|
||||
.slice(-LLM_LOOP_CHECK_HISTORY_COUNT);
|
||||
|
||||
const trimmedHistory = this.trimRecentHistory(recentHistory);
|
||||
|
||||
const prompt = `You are a sophisticated AI diagnostic agent specializing in identifying when a conversational AI is stuck in an unproductive state. Your task is to analyze the provided conversation history and determine if the assistant has ceased to make meaningful progress.
|
||||
|
||||
An unproductive state is characterized by one or more of the following patterns over the last 5 or more assistant turns:
|
||||
@@ -337,7 +375,7 @@ For example, a series of 'tool_A' or 'tool_B' tool calls that make small, distin
|
||||
|
||||
Please analyze the conversation history to determine the possibility that the conversation is stuck in a repetitive, non-productive state.`;
|
||||
const contents = [
|
||||
...recentHistory,
|
||||
...trimmedHistory,
|
||||
{ role: 'user', parts: [{ text: prompt }] },
|
||||
];
|
||||
const schema: Record<string, unknown> = {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import EventEmitter from 'events';
|
||||
import { Readable } from 'stream';
|
||||
import { type ChildProcess } from 'child_process';
|
||||
import EventEmitter from 'events';
|
||||
import type { Readable } from 'stream';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import {
|
||||
ShellExecutionService,
|
||||
ShellOutputEvent,
|
||||
type ShellOutputEvent,
|
||||
} from './shellExecutionService.js';
|
||||
|
||||
// Hoisted Mocks
|
||||
@@ -292,7 +292,7 @@ describe('ShellExecutionService', () => {
|
||||
|
||||
expect(mockPtySpawn).toHaveBeenCalledWith(
|
||||
'cmd.exe',
|
||||
['/c', 'dir "foo bar"'],
|
||||
'/c dir "foo bar"',
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { getPty, PtyImplementation } from '../utils/getPty.js';
|
||||
import pkg from '@xterm/headless';
|
||||
import { spawn as cpSpawn } from 'child_process';
|
||||
import { TextDecoder } from 'util';
|
||||
import os from 'os';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { TextDecoder } from 'util';
|
||||
import type { PtyImplementation } from '../utils/getPty.js';
|
||||
import { getPty } from '../utils/getPty.js';
|
||||
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
|
||||
import { isBinary } from '../utils/textUtils.js';
|
||||
import pkg from '@xterm/headless';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
const { Terminal } = pkg;
|
||||
|
||||
const SIGKILL_TIMEOUT_MS = 200;
|
||||
@@ -140,6 +141,7 @@ export class ShellExecutionService {
|
||||
const child = cpSpawn(commandToExecute, [], {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsVerbatimArguments: true,
|
||||
shell: isWindows ? true : 'bash',
|
||||
detached: !isWindows,
|
||||
env: {
|
||||
@@ -321,7 +323,7 @@ export class ShellExecutionService {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const shell = isWindows ? 'cmd.exe' : 'bash';
|
||||
const args = isWindows
|
||||
? ['/c', commandToExecute]
|
||||
? `/c ${commandToExecute}`
|
||||
: ['-c', commandToExecute];
|
||||
|
||||
const ptyProcess = ptyInfo?.module.spawn(shell, args, {
|
||||
|
||||
Reference in New Issue
Block a user