Skip to content

Commit 070a36b

Browse files
author
Eric Wheeler
committed
Revert "Smart truncation for terminal output"
This reverts commit 7eee3e0. Middle-out truncation is a really great feature and it should still be implemented, however it unnecessarily interferes with #1365 because it hooked into the low-level chunk management that comes directly from VSCE shell integration. The best place to hook OutputBuilder is as follows depending on the state of terminal interaction: 1. Foreground terminals: Cline.ts: executeCommandTool(...) { process.on("line", (line) => { lines.push(line) ... } } 2. For background terminals: hook in at the point that getUnretrievedOutput is consumed for active or inactive terminals in Cline.ts:getEnvironmentDetails() Please note: The Terminal classes are very sensitive to change, partially because of the complicated way that shell integration works with VSCE, and partially because of the way that Cline interacts with the Terminal* class abstractions that make VSCE shell integration easier to work with. At the point that PR#1365 is merged, it is unlikely that any Terminal* classes will need to be modified substantially. Generally speaking, we should think of this is a stable interface and minimize changes. Reverts: #1390
1 parent 13c75a1 commit 070a36b

22 files changed

+336
-737
lines changed

src/core/Cline.ts

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
addLineNumbers,
2626
stripLineNumbers,
2727
everyLineHasLineNumbers,
28+
truncateOutput,
2829
} from "../integrations/misc/extract-text"
2930
import { TerminalManager, ExitCodeDetails } from "../integrations/terminal/TerminalManager"
3031
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
@@ -59,7 +60,7 @@ import { calculateApiCostAnthropic } from "../utils/cost"
5960
import { fileExistsAtPath } from "../utils/fs"
6061
import { arePathsEqual, getReadablePath } from "../utils/path"
6162
import { parseMentions } from "./mentions"
62-
import { RooIgnoreController } from "./ignore/RooIgnoreController"
63+
import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/RooIgnoreController"
6364
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
6465
import { formatResponse } from "./prompts/responses"
6566
import { SYSTEM_PROMPT } from "./prompts/system"
@@ -70,7 +71,6 @@ import { BrowserSession } from "../services/browser/BrowserSession"
7071
import { McpHub } from "../services/mcp/McpHub"
7172
import crypto from "crypto"
7273
import { insertGroups } from "./diff/insert-groups"
73-
import { OutputBuilder } from "../integrations/terminal/OutputBuilder"
7474
import { telemetryService } from "../services/telemetry/TelemetryService"
7575

7676
const cwd =
@@ -919,52 +919,44 @@ export class Cline {
919919
// Tools
920920

921921
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
922-
const { terminalOutputLimit } = (await this.providerRef.deref()?.getState()) ?? {}
923-
924922
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
925-
// Weird visual bug when creating new terminals (even manually) where
926-
// there's an empty space at the top.
927-
terminalInfo.terminal.show()
928-
const process = this.terminalManager.runCommand(terminalInfo, command, terminalOutputLimit)
923+
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
924+
const process = this.terminalManager.runCommand(terminalInfo, command)
929925

930926
let userFeedback: { text?: string; images?: string[] } | undefined
931927
let didContinue = false
932-
933-
const sendCommandOutput = async (line: string) => {
928+
const sendCommandOutput = async (line: string): Promise<void> => {
934929
try {
935930
const { response, text, images } = await this.ask("command_output", line)
936-
937931
if (response === "yesButtonClicked") {
938-
// Proceed while running.
932+
// proceed while running
939933
} else {
940934
userFeedback = { text, images }
941935
}
942-
943936
didContinue = true
944-
process.continue() // Continue past the await.
937+
process.continue() // continue past the await
945938
} catch {
946-
// This can only happen if this ask promise was ignored, so ignore this error.
939+
// This can only happen if this ask promise was ignored, so ignore this error
947940
}
948941
}
949942

950-
let completed = false
951-
let exitDetails: ExitCodeDetails | undefined
952-
953-
let builder = new OutputBuilder({ maxSize: terminalOutputLimit })
954-
let output: string | undefined = undefined
955-
943+
let lines: string[] = []
956944
process.on("line", (line) => {
957-
builder.append(line)
958-
945+
lines.push(line)
959946
if (!didContinue) {
960947
sendCommandOutput(line)
961948
} else {
962949
this.say("command_output", line)
963950
}
964951
})
965952

966-
process.once("completed", (buffer?: string) => {
967-
output = buffer
953+
let completed = false
954+
let exitDetails: ExitCodeDetails | undefined
955+
process.once("completed", (output?: string) => {
956+
// Use provided output if available, otherwise keep existing result.
957+
if (output) {
958+
lines = output.split("\n")
959+
}
968960
completed = true
969961
})
970962

@@ -980,17 +972,19 @@ export class Cline {
980972

981973
await process
982974

983-
// Wait for a short delay to ensure all messages are sent to the webview.
975+
// Wait for a short delay to ensure all messages are sent to the webview
984976
// This delay allows time for non-awaited promises to be created and
985977
// for their associated messages to be sent to the webview, maintaining
986978
// the correct order of messages (although the webview is smart about
987-
// grouping command_output messages despite any gaps anyways).
979+
// grouping command_output messages despite any gaps anyways)
988980
await delay(50)
989-
const result = output || builder.content
981+
982+
const { terminalOutputLineLimit } = (await this.providerRef.deref()?.getState()) ?? {}
983+
const output = truncateOutput(lines.join("\n"), terminalOutputLineLimit)
984+
const result = output.trim()
990985

991986
if (userFeedback) {
992987
await this.say("user_feedback", userFeedback.text, userFeedback.images)
993-
994988
return [
995989
true,
996990
formatResponse.toolResult(
@@ -1004,28 +998,25 @@ export class Cline {
1004998

1005999
if (completed) {
10061000
let exitStatus = "No exit code available"
1007-
10081001
if (exitDetails !== undefined) {
10091002
if (exitDetails.signal) {
10101003
exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})`
1011-
10121004
if (exitDetails.coreDumpPossible) {
10131005
exitStatus += " - core dump possible"
10141006
}
10151007
} else {
10161008
exitStatus = `Exit code: ${exitDetails.exitCode}`
10171009
}
10181010
}
1019-
10201011
return [false, `Command executed. ${exitStatus}${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
1012+
} else {
1013+
return [
1014+
false,
1015+
`Command is still running in the user's terminal.${
1016+
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
1017+
}\n\nYou will be updated on the terminal status and new output in the future.`,
1018+
]
10211019
}
1022-
1023-
return [
1024-
false,
1025-
`Command is still running in the user's terminal.${
1026-
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
1027-
}\n\nYou will be updated on the terminal status and new output in the future.`,
1028-
]
10291020
}
10301021

10311022
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
@@ -3525,7 +3516,7 @@ export class Cline {
35253516
terminalDetails += "\n\n# Actively Running Terminals"
35263517
for (const busyTerminal of busyTerminals) {
35273518
terminalDetails += `\n## Original command: \`${busyTerminal.lastCommand}\``
3528-
const newOutput = this.terminalManager.readLine(busyTerminal.id)
3519+
const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
35293520
if (newOutput) {
35303521
terminalDetails += `\n### New Output\n${newOutput}`
35313522
} else {
@@ -3537,7 +3528,7 @@ export class Cline {
35373528
if (inactiveTerminals.length > 0) {
35383529
const inactiveTerminalOutputs = new Map<number, string>()
35393530
for (const inactiveTerminal of inactiveTerminals) {
3540-
const newOutput = this.terminalManager.readLine(inactiveTerminal.id)
3531+
const newOutput = this.terminalManager.getUnretrievedOutput(inactiveTerminal.id)
35413532
if (newOutput) {
35423533
inactiveTerminalOutputs.set(inactiveTerminal.id, newOutput)
35433534
}

src/core/mentions/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import * as vscode from "vscode"
22
import * as path from "path"
33
import { openFile } from "../../integrations/misc/open-file"
44
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
5-
import { mentionRegexGlobal } from "../../shared/context-mentions"
5+
import { mentionRegexGlobal, formatGitSuggestion, type MentionSuggestion } from "../../shared/context-mentions"
66
import fs from "fs/promises"
77
import { extractTextFromFile } from "../../integrations/misc/extract-text"
88
import { isBinaryFile } from "isbinaryfile"
99
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
1010
import { getCommitInfo, getWorkingState } from "../../utils/git"
11-
import { getLatestTerminalOutput } from "../../integrations/terminal/getLatestTerminalOutput"
11+
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
1212

1313
export async function openMention(mention?: string): Promise<void> {
1414
if (!mention) {

src/core/webview/ClineProvider.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,17 @@ import simpleGit from "simple-git"
1010

1111
import { setPanel } from "../../activate/registerCommands"
1212
import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
13+
import { CheckpointStorage } from "../../shared/checkpoints"
1314
import { findLast } from "../../shared/array"
14-
import { supportPrompt } from "../../shared/support-prompt"
15+
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
1516
import { GlobalFileNames } from "../../shared/globalFileNames"
1617
import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../../shared/globalState"
1718
import { HistoryItem } from "../../shared/HistoryItem"
18-
import { CheckpointStorage } from "../../shared/checkpoints"
1919
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
2020
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
21-
import { Mode, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
21+
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
2222
import { checkExistKey } from "../../shared/checkExistApiConfig"
2323
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
24-
import { TERMINAL_OUTPUT_LIMIT } from "../../shared/terminal"
2524
import { downloadTask } from "../../integrations/misc/export-markdown"
2625
import { openFile, openImage } from "../../integrations/misc/open-file"
2726
import { selectImages } from "../../integrations/misc/process-images"
@@ -1255,6 +1254,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12551254
await this.postStateToWebview()
12561255
break
12571256
case "checkpointStorage":
1257+
console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
12581258
const checkpointStorage = message.text ?? "task"
12591259
await this.updateGlobalState("checkpointStorage", checkpointStorage)
12601260
await this.postStateToWebview()
@@ -1387,8 +1387,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13871387
await this.updateGlobalState("writeDelayMs", message.value)
13881388
await this.postStateToWebview()
13891389
break
1390-
case "terminalOutputLimit":
1391-
await this.updateGlobalState("terminalOutputLimit", message.value)
1390+
case "terminalOutputLineLimit":
1391+
await this.updateGlobalState("terminalOutputLineLimit", message.value)
13921392
await this.postStateToWebview()
13931393
break
13941394
case "mode":
@@ -2315,7 +2315,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23152315
remoteBrowserEnabled,
23162316
preferredLanguage,
23172317
writeDelayMs,
2318-
terminalOutputLimit,
2318+
terminalOutputLineLimit,
23192319
fuzzyMatchThreshold,
23202320
mcpEnabled,
23212321
enableMcpServerCreation,
@@ -2375,7 +2375,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23752375
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
23762376
preferredLanguage: preferredLanguage ?? "English",
23772377
writeDelayMs: writeDelayMs ?? 1000,
2378-
terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
2378+
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
23792379
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
23802380
mcpEnabled: mcpEnabled ?? true,
23812381
enableMcpServerCreation: enableMcpServerCreation ?? true,
@@ -2530,7 +2530,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
25302530
remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
25312531
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
25322532
writeDelayMs: stateValues.writeDelayMs ?? 1000,
2533-
terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
2533+
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
25342534
mode: stateValues.mode ?? defaultModeSlug,
25352535
preferredLanguage:
25362536
stateValues.preferredLanguage ??

src/integrations/misc/__tests__/extract-text.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../extract-text"
1+
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers, truncateOutput } from "../extract-text"
22

33
describe("addLineNumbers", () => {
44
it("should add line numbers starting from 1 by default", () => {
@@ -101,3 +101,67 @@ describe("stripLineNumbers", () => {
101101
expect(stripLineNumbers(input)).toBe(expected)
102102
})
103103
})
104+
105+
describe("truncateOutput", () => {
106+
it("returns original content when no line limit provided", () => {
107+
const content = "line1\nline2\nline3"
108+
expect(truncateOutput(content)).toBe(content)
109+
})
110+
111+
it("returns original content when lines are under limit", () => {
112+
const content = "line1\nline2\nline3"
113+
expect(truncateOutput(content, 5)).toBe(content)
114+
})
115+
116+
it("truncates content with 20/80 split when over limit", () => {
117+
// Create 25 lines of content
118+
const lines = Array.from({ length: 25 }, (_, i) => `line${i + 1}`)
119+
const content = lines.join("\n")
120+
121+
// Set limit to 10 lines
122+
const result = truncateOutput(content, 10)
123+
124+
// Should keep:
125+
// - First 2 lines (20% of 10)
126+
// - Last 8 lines (80% of 10)
127+
// - Omission indicator in between
128+
const expectedLines = [
129+
"line1",
130+
"line2",
131+
"",
132+
"[...15 lines omitted...]",
133+
"",
134+
"line18",
135+
"line19",
136+
"line20",
137+
"line21",
138+
"line22",
139+
"line23",
140+
"line24",
141+
"line25",
142+
]
143+
expect(result).toBe(expectedLines.join("\n"))
144+
})
145+
146+
it("handles empty content", () => {
147+
expect(truncateOutput("", 10)).toBe("")
148+
})
149+
150+
it("handles single line content", () => {
151+
expect(truncateOutput("single line", 10)).toBe("single line")
152+
})
153+
154+
it("handles windows-style line endings", () => {
155+
// Create content with windows line endings
156+
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`)
157+
const content = lines.join("\r\n")
158+
159+
const result = truncateOutput(content, 5)
160+
161+
// Should keep first line (20% of 5 = 1) and last 4 lines (80% of 5 = 4)
162+
// Split result by either \r\n or \n to normalize line endings
163+
const resultLines = result.split(/\r?\n/)
164+
const expectedLines = ["line1", "", "[...10 lines omitted...]", "", "line12", "line13", "line14", "line15"]
165+
expect(resultLines).toEqual(expectedLines)
166+
})
167+
})

src/integrations/misc/extract-text.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,37 @@ export function stripLineNumbers(content: string): string {
8989
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n"
9090
return processedLines.join(lineEnding)
9191
}
92+
93+
/**
94+
* Truncates multi-line output while preserving context from both the beginning and end.
95+
* When truncation is needed, it keeps 20% of the lines from the start and 80% from the end,
96+
* with a clear indicator of how many lines were omitted in between.
97+
*
98+
* @param content The multi-line string to truncate
99+
* @param lineLimit Optional maximum number of lines to keep. If not provided or 0, returns the original content
100+
* @returns The truncated string with an indicator of omitted lines, or the original content if no truncation needed
101+
*
102+
* @example
103+
* // With 10 line limit on 25 lines of content:
104+
* // - Keeps first 2 lines (20% of 10)
105+
* // - Keeps last 8 lines (80% of 10)
106+
* // - Adds "[...15 lines omitted...]" in between
107+
*/
108+
export function truncateOutput(content: string, lineLimit?: number): string {
109+
if (!lineLimit) {
110+
return content
111+
}
112+
113+
const lines = content.split("\n")
114+
if (lines.length <= lineLimit) {
115+
return content
116+
}
117+
118+
const beforeLimit = Math.floor(lineLimit * 0.2) // 20% of lines before
119+
const afterLimit = lineLimit - beforeLimit // remaining 80% after
120+
return [
121+
...lines.slice(0, beforeLimit),
122+
`\n[...${lines.length - lineLimit} lines omitted...]\n`,
123+
...lines.slice(-afterLimit),
124+
].join("\n")
125+
}

0 commit comments

Comments
 (0)