Skip to content

Commit

Permalink
feat(terminal): improve shell integration and output handling - Add s…
Browse files Browse the repository at this point in the history
…upport for various OS-specific prompts - Optimize output processing from first to last prompt - Add shell integration initialization delay
  • Loading branch information
coolcline committed Feb 17, 2025
1 parent 4ac6007 commit 1479fed
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 89 deletions.
57 changes: 39 additions & 18 deletions src/integrations/terminal/TerminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from "vscode"
import { arePathsEqual } from "../../utils/path"
import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
import { logger } from "../../utils/logging"

/*
TerminalManager:
Expand Down Expand Up @@ -81,15 +82,17 @@ declare module "vscode" {
}

// Extend the Terminal type to include our custom properties
type ExtendedTerminal = vscode.Terminal & {
shellIntegration?: {
cwd?: vscode.Uri
executeCommand?: (command: string) => {
read: () => AsyncIterable<string>
}
interface TerminalShellIntegration {
readonly cwd?: vscode.Uri
readonly executeCommand?: (command: string) => {
read: () => AsyncIterable<string>
}
}

type ExtendedTerminal = vscode.Terminal & {
readonly shellIntegration?: TerminalShellIntegration
}

export class TerminalManager {
private terminalIds: Set<number> = new Set()
private processes: Map<number, TerminalProcess> = new Map()
Expand All @@ -98,12 +101,23 @@ export class TerminalManager {
constructor() {
let disposable: vscode.Disposable | undefined
try {
// 尝试注册 shell execution 事件监听
disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
// Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
e?.execution?.read()
if (e?.execution?.read) {
// 创建读取流以确保更一致的输出
e.execution.read()
logger.debug("Shell integration 事件监听已注册", {
ctx: "terminal",
hasRead: !!e.execution.read,
event: e,
})
}
})
} catch (error) {
// console.error("Error setting up onDidEndTerminalShellExecution", error)
logger.warn("Shell integration 事件监听注册失败", {
ctx: "terminal",
error: error instanceof Error ? error.message : String(error),
})
}
if (disposable) {
this.disposables.push(disposable)
Expand All @@ -120,25 +134,32 @@ export class TerminalManager {
terminalInfo.busy = false
})

// 移除原有的 no_shell_integration 处理逻辑,因为我们现在有更可靠的后备方案
const promise = new Promise<void>((resolve, reject) => {
process.once("continue", () => {
resolve()
})
process.once("error", (error) => {
console.error(`Error in terminal ${terminalInfo.id}:`, error)
logger.error(`终端 ${terminalInfo.id} 执行出错:`, {
ctx: "terminal",
error: error instanceof Error ? error.message : String(error),
})
reject(error)
})
})

const terminal = terminalInfo.terminal as ExtendedTerminal
if (terminal.shellIntegration) {
process.run(terminal, command)
} else {
// 直接运行命令,使用后备方案获取输出
process.run(terminal, command)
}
// 检查 shell integration 支持
const hasShellIntegration = !!terminal.shellIntegration?.executeCommand
logger.debug("检查 shell integration 支持", {
ctx: "terminal",
hasShellIntegration,
terminalName: terminal.name,
shellIntegration: terminal.shellIntegration,
executeCommand: !!terminal.shellIntegration?.executeCommand,
terminal: terminal,
})

process.run(terminal, command)
return mergePromise(process, promise)
}

Expand Down Expand Up @@ -172,7 +193,7 @@ export class TerminalManager {
}

// If all terminals are busy, create a new one
const newTerminalInfo = TerminalRegistry.createTerminal(cwd)
const newTerminalInfo = await TerminalRegistry.createTerminal(cwd)
this.terminalIds.add(newTerminalInfo.id)
return newTerminalInfo
}
Expand Down
160 changes: 90 additions & 70 deletions src/integrations/terminal/TerminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class TerminalProcess extends EventEmitter {
zsh: ["%", "$", "➜", "❯"],
bash: ["$", "#", "@", "❯"],
fish: ["›", "$", "❯", "→"],
powershell: [">", "PS>", "PS❯"],
powershell: [">", "PS>", "PS❯", "PWD>"],
cmd: [">", "C:\\>", "D:\\>"],
generic: ["$", ">", "#", "❯", "→", "➜"],
}
Expand All @@ -54,6 +54,8 @@ export class TerminalProcess extends EventEmitter {
logger.debug("使用 shellIntegration 执行命令", {
ctx: "terminal",
terminalName: terminal.name,
hasShellIntegration: !!terminal.shellIntegration,
hasExecuteCommand: !!terminal.shellIntegration?.executeCommand,
})
const execution = terminal.shellIntegration.executeCommand(command)
const stream = execution.read()
Expand Down Expand Up @@ -273,6 +275,15 @@ export class TerminalProcess extends EventEmitter {
try {
const newOutput = await TerminalProcess.getTerminalContents()

// 如果检测到提示符,立即返回结果
if (newOutput && this.checkCommandCompletion(newOutput)) {
logger.debug("检测到提示符,命令执行完成", {
ctx: "terminal",
attempt: attemptCount + 1,
})
return newOutput
}

const outputPreview = newOutput?.length > 30 ? newOutput.substring(0, 30) + "..." : newOutput
logger.debug("轮询检查输出", {
ctx: "terminal",
Expand All @@ -292,36 +303,11 @@ export class TerminalProcess extends EventEmitter {

if (this.isCompiling(newOutput)) {
waitTime = Math.min(500, waitTime * 1.5)
logger.debug("检测到编译中,增加等待时间", {
ctx: "terminal",
newWaitTime: waitTime,
outputPreview: outputPreview,
})
} else {
waitTime = TerminalProcess.OUTPUT_CHECK_CONFIG.minWaitMs
}
} else {
stableCount++
logger.debug("输出稳定", {
ctx: "terminal",
stableCount,
outputPreview: outputPreview,
})
if (stableCount >= TerminalProcess.OUTPUT_CHECK_CONFIG.stableCount) {
await new Promise((resolve) => setTimeout(resolve, 100))
const finalCheck = await TerminalProcess.getTerminalContents()
const finalPreview =
finalCheck?.length > 30 ? finalCheck.substring(0, 30) + "..." : finalCheck
if (finalCheck === output && this.checkCommandCompletion(finalCheck)) {
logger.debug("命令执行完成", {
ctx: "terminal",
totalAttempts: attemptCount + 1,
finalOutputLength: output.length,
outputPreview: finalPreview,
})
return output
}
}
}
}

Expand All @@ -331,19 +317,12 @@ export class TerminalProcess extends EventEmitter {
logger.error("获取终端内容失败", {
ctx: "terminal",
error: error instanceof Error ? error.message : String(error),
command: this.command.length > 30 ? this.command.substring(0, 30) + "..." : this.command,
})
attemptCount++
waitTime = Math.min(waitTime * 1.5, TerminalProcess.OUTPUT_CHECK_CONFIG.maxWaitMs)
}
}

logger.debug("达到最大尝试次数", {
ctx: "terminal",
finalOutputLength: output.length,
totalAttempts: attemptCount,
outputPreview: output.length > 30 ? output.substring(0, 30) + "..." : output,
})
return output
}

Expand Down Expand Up @@ -466,17 +445,17 @@ export class TerminalProcess extends EventEmitter {
if (!data) return false

const lines = data.split("\n")
const lastLine = lines[lines.length - 1].trim()

// 如果最后一行是空的,检查倒数第二行
if (!lastLine && lines.length > 1) {
const secondLastLine = lines[lines.length - 2].trim()
if (this.isPromptLine(secondLastLine)) {
return true
// 从后向前查找第一个非空行
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim()
if (line) {
// 找到第一个非空行,检查是否是提示符
return this.isPromptLine(line)
}
}

return this.isPromptLine(lastLine)
return false
}

private isPromptLine(line: string): boolean {
Expand All @@ -486,20 +465,44 @@ export class TerminalProcess extends EventEmitter {
const hasPrompt = Object.values(TerminalProcess.SHELL_PROMPTS)
.flat()
.some((prompt) => {
// 添加更多提示符模式的检查
return line.endsWith(prompt) || line.endsWith(` ${prompt}`) || line === prompt
})

// 检查常见的用户@主机格式
const hasUserHostPrompt = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+:[~\w/.-]+[$#%>]/.test(line)

// 检查 Windows 路径格式
const hasWindowsPathPrompt = /^[A-Z]:\\[^>]*>/.test(line)

// 检查 Git bash 风格的提示符
const hasGitBashPrompt = /^[A-Z]:[\w\s/\\-]+[$#>]/.test(line)

return hasPrompt || hasUserHostPrompt || hasWindowsPathPrompt || hasGitBashPrompt
// 检查 macOS zsh 格式
const hasMacZshPrompt = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+\s+[a-zA-Z0-9_/.-]+\s+[%$#>]/.test(line)

// 检查 Linux/Unix 格式 (支持更多格式)
const hasUnixPrompt = [
// username@hostname:~/path$
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+:[~\w/.-]+[$#%>]/,
// [username@hostname path]$
/^\[[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\s+[~\w/.-]+\][$#%>]/,
// username@hostname ~/path$
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\s+[~\w/.-]+[$#%>]/,
].some((pattern) => pattern.test(line))

// 检查 PowerShell 格式 (支持更多格式)
const hasPowerShellPrompt = [
// PS C:\path>
/^PS\s+[A-Z]:\\[^>]*>/,
// PS /Users/path>
/^PS\s+\/[^>]*>/,
// username@hostname /path>
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\s+\/[^>]*>/,
].some((pattern) => pattern.test(line))

// 检查 Windows CMD 格式
const hasWindowsPrompt = /^[A-Z]:\\[^>]*>/.test(line)

// 检查 Fish shell 格式
const hasFishPrompt = [
// username@hostname ~/path>
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\s+~?\/[^>]*[>]/,
// ~/path>
/^~?\/[^>]*[>]/,
].some((pattern) => pattern.test(line))

return hasPrompt || hasMacZshPrompt || hasUnixPrompt || hasPowerShellPrompt || hasWindowsPrompt || hasFishPrompt
}

private processOutput(output: string): void {
Expand All @@ -509,32 +512,49 @@ export class TerminalProcess extends EventEmitter {
const normalizedOutput = this.normalizeLineEndings(output)
const lines = normalizedOutput.split("\n")

// 寻找最后一个提示符的位置
let lastPromptIndex = -1
for (let i = lines.length - 1; i >= 0; i--) {
// 寻找第一个提示符的位置
let firstPromptIndex = -1
for (let i = 0; i < lines.length; i++) {
if (this.isPromptLine(lines[i].trim())) {
lastPromptIndex = i
firstPromptIndex = i
break
}
}

// 获取相关行
const relevantLines = lastPromptIndex !== -1 ? lines.slice(0, lastPromptIndex) : lines
// 如果找到提示符,从提示符开始处理
if (firstPromptIndex !== -1) {
// 寻找最后一个提示符的位置
let lastPromptIndex = -1
for (let i = lines.length - 1; i > firstPromptIndex; i--) {
if (this.isPromptLine(lines[i].trim())) {
lastPromptIndex = i
break
}
}

// 清理和过滤输出行
const cleanedLines = relevantLines
.map((line) => this.cleanLine(line))
.filter((line) => {
return line && !this.isCommandEcho(line) && !this.isPromptLine(line) && !this.isSystemPrompt(line)
})
// 获取相关行(从第一个提示符到最后一个提示符之间的内容)
const relevantLines =
lastPromptIndex !== -1
? lines.slice(firstPromptIndex, lastPromptIndex)
: lines.slice(firstPromptIndex)

// 清理和过滤输出行
const cleanedLines = relevantLines
.map((line) => this.cleanLine(line))
.filter((line) => {
return (
line && !this.isCommandEcho(line) && !this.isPromptLine(line) && !this.isSystemPrompt(line)
)
})

// 如果有新的输出内容
if (cleanedLines.length > 0) {
const processedOutput = cleanedLines.join("\n")
if (!this.outputBuffer.includes(processedOutput)) {
this.outputBuffer.push(processedOutput)
this.fullOutput = this.outputBuffer.join("\n")
this.emit("line", processedOutput)
// 如果有新的输出内容
if (cleanedLines.length > 0) {
const processedOutput = cleanedLines.join("\n")
if (!this.outputBuffer.includes(processedOutput)) {
this.outputBuffer.push(processedOutput)
this.fullOutput = this.outputBuffer.join("\n")
this.emit("line", processedOutput)
}
}
}
} catch (error) {
Expand Down
22 changes: 21 additions & 1 deletion src/integrations/terminal/TerminalRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as vscode from "vscode"

// 扩展 VSCode 的类型定义
declare module "vscode" {
interface TerminalOptions {
shellIntegration?: boolean
}
}

export interface TerminalInfo {
terminal: vscode.Terminal
busy: boolean
Expand All @@ -13,15 +20,28 @@ export class TerminalRegistry {
private static terminals: TerminalInfo[] = []
private static nextTerminalId = 1

static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo {
static async createTerminal(cwd?: string | vscode.Uri | undefined): Promise<TerminalInfo> {
// 创建终端时启用 shell integration
const terminal = vscode.window.createTerminal({
cwd,
name: "CoolCline",
iconPath: new vscode.ThemeIcon("webhook"),
env: {
PAGER: "cat",
VSCODE_SHELL_INTEGRATION: "1",
SHELL_INTEGRATION: "1",
ENABLE_SHELL_INTEGRATION: "1",
TERM_PROGRAM: "vscode",
},
shellIntegration: true,
})

// 确保终端显示并激活
terminal.show(true)

// 等待 shell integration 初始化
await new Promise((resolve) => setTimeout(resolve, 1000))

const newInfo: TerminalInfo = {
terminal,
busy: false,
Expand Down

0 comments on commit 1479fed

Please sign in to comment.