Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
ab9cbb2
feat: implement osc listener that renders a divider in the terminal
Fangoling Oct 28, 2025
ea0d391
feat: separator is now between lines instead of hovering over a line …
Fangoling Nov 11, 2025
f8fd8a6
feat: implement command history
Fangoling Nov 11, 2025
fedf41f
feat: implement zsh config to inject OSC133
Fangoling Nov 12, 2025
399cc0e
feat: implement bash config to inject OSC133
Fangoling Nov 12, 2025
ba3323e
feat: copy over shell config into browser build
Fangoling Nov 12, 2025
f133ca2
feat: add logging of command blocks
Fangoling Nov 12, 2025
0acc841
feat: implement shell config injection for bash and zsh
Fangoling Nov 12, 2025
8b67392
fix: copy over . prefixed files
Fangoling Dec 7, 2025
1afab1a
fix: use correct zdotdir folder path and pass env variables correctly…
Fangoling Dec 8, 2025
05dc50c
feat: add sources to license banner
Fangoling Dec 9, 2025
0e7867a
fix: timing issues with command history
Fangoling Dec 9, 2025
404a089
fix: use appropiate width for command separator
Fangoling Dec 9, 2025
8449f74
fix: return issue in shell config injector
Fangoling Dec 9, 2025
56a26ca
feat: add terminal preference to enable command separator
Fangoling Dec 9, 2025
f6f480b
feat: delay first write of debug session to avoid double input of deb…
Fangoling Dec 9, 2025
fcc27cb
chore: add updated package lock json with new copyfiles dep
Fangoling Dec 10, 2025
6dd1b24
feat: add 'test' logs in ai-terminal extension
Fangoling Dec 10, 2025
937da95
fix: add missing license header and semicolons
Fangoling Dec 10, 2025
baf3791
fix: add missing internalization of command separator preference
Fangoling Dec 10, 2025
56142c1
fix: remove manual edit of nls.metadata.json
Fangoling Dec 10, 2025
5390b13
fix: resolve issue where terminal input before debug session command …
Fangoling Dec 10, 2025
c1699a2
chore: correct localization call for terminal preference
Fangoling Dec 10, 2025
a9c7b1f
fix: remove unused echo at start of script
Fangoling Dec 11, 2025
d51097e
fix: add comment to explain terminal creation delay
Fangoling Dec 11, 2025
f42e796
fix: use perma links for source references
Fangoling Dec 11, 2025
9a0e031
chore: add comment to explain osc133 sequences
Fangoling Dec 11, 2025
51f0cdf
fix: improve comment for OSC133 support
Fangoling Dec 11, 2025
3307f8c
fix: remove unused echo from bash integration
Fangoling Dec 11, 2025
54bd06c
fix: command separator preference updated onChange and not on creatio…
Fangoling Dec 11, 2025
2993f1d
fix: rename variable names and stripping of login flag
Fangoling Dec 11, 2025
7813dcf
chore: add changes list to all shell scripts by jetbrains
Fangoling Dec 11, 2025
7c6d60b
fix: show command separator intialization with pereference instead of…
Fangoling Dec 12, 2025
ae0d236
feat: use copywebpack plugin in webpack.config instead of copyfiles c…
Fangoling Dec 12, 2025
63f319c
fix: set static constants in shellinjector to readonly
Fangoling Dec 12, 2025
63d2827
fix: remove console.logs in ai-terminal ext and add console debug to …
Fangoling Dec 12, 2025
57bc810
chore: add console.debug for current history after each executed command
Fangoling Dec 13, 2025
7029331
style: remove trailing whitespace
Fangoling Dec 13, 2025
8997f5f
fix: move reset command history state into execute command function
Fangoling Dec 15, 2025
871e600
feat: expose command start and on prompt shown events
Fangoling Dec 15, 2025
7ae6ab9
fix: use correct event to determine prompt apperance in the terminal
Fangoling Dec 15, 2025
8c4170b
fix: change comment; delaying terminal creation -> opening
Fangoling Dec 16, 2025
46434ff
fix: add info to description of the command separator preference
Fangoling Dec 16, 2025
49e7df5
chore: change license to EclipseSource and others
Fangoling Dec 17, 2025
69cf24d
chore: update NOTICE.md
Fangoling Dec 17, 2025
5caa9fd
feat: implement terminal history for task terminals
Fangoling Jan 13, 2026
59dc5d6
feat: add preference 'disable terminal history' and set to experimental
Fangoling Jan 14, 2026
49a6a16
chore: remove dependency for copyfiles and update package-lock.json
Fangoling Jan 14, 2026
85aa82a
feat: add term history preference in ai terminal agent
Fangoling Jan 14, 2026
76b1755
chore: fix lint
Fangoling Jan 14, 2026
86fd4ad
fix: use named logger
Fangoling Jan 15, 2026
e31d182
fix: rename resetcommandhistorystate -> clear command collection state
Fangoling Jan 15, 2026
d135ed0
fix: cange divider length to 100%
Fangoling Jan 15, 2026
62255e4
chore: change intellij ref in comment to theia
Fangoling Jan 15, 2026
edc3050
fix: add missing emitter to be disposed
Fangoling Jan 15, 2026
69ce00a
fix: prevent write to buffer when history disabled
Fangoling Jan 15, 2026
81ef5e1
fix: remove unused command history variable (overwritten each time)
Fangoling Jan 15, 2026
928e096
fix: set allowedPropsedApi to dependant on preference of history
Fangoling Jan 15, 2026
fc24101
fix: use browser compatible hex to utf-8 decoder
Fangoling Jan 15, 2026
febae89
feat: pass preference to enable termina history from client side
Fangoling Jan 15, 2026
a2ff4a7
feat: disable command-block-support for powerlevel10k zsh plugin
Fangoling Jan 19, 2026
c3a1811
fix: implement state class that handles command history
Fangoling Jan 19, 2026
92ea1be
feat: implement preference handling for task terminal
Fangoling Jan 19, 2026
67bba06
chore: fix lint
Fangoling Jan 19, 2026
164e39f
feat: extract terminal command blocks till reaching character count
Fangoling Jan 20, 2026
49aeed0
fix: prevent command history clear when command is running
Fangoling Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ b73945c70f1117c4e65939dd3e10bdd623cb4ef3 (n/a)

* License: CC-BY-4.0

intellij-community (2025.2.4)

* License: Apache-2.0
* Project: <https://github.com/JetBrains/intellij-community>
* Source: <https://github.com/JetBrains/intellij-community>

inversify (5.0.1)

* License: MIT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ const plugins = [
from: path.join(resolvePackagePath('@theia/plugin-ext-vscode', __dirname), '..', 'lib', 'node', 'context', 'plugin-vscode-init-fe.js'),
to: path.resolve(__dirname, 'lib', 'frontend', 'context', 'plugin-vscode-init-fe.js')
}`)}
${this.ifPackage('@theia/terminal', `,
{
// copy shell integration scripts
from: path.join(resolvePackagePath('@theia/terminal', __dirname), '..', 'src', 'node', 'shell-integrations'),
to: path.resolve(__dirname, 'lib', 'backend', 'shell-integrations')
}`)}
]
}),
new webpack.ProvidePlugin({
Expand Down
22 changes: 0 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 44 additions & 2 deletions packages/ai-terminal/src/browser/ai-terminal-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { TerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl';
import { TerminalPreferences } from '@theia/terminal/lib/common/terminal-preferences';
import { AiTerminalAgent } from './ai-terminal-agent';
import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-handler-factory';
import { AgentService } from '@theia/ai-core';
import { nls } from '@theia/core/lib/common/nls';
import { TerminalBlock } from '@theia/terminal/lib/browser/base/terminal-widget';

const AI_TERMINAL_COMMAND = Command.toLocalizedCommand({
id: 'ai-terminal:open',
Expand All @@ -50,6 +52,9 @@ export class AiTerminalCommandContribution implements CommandContribution, MenuC
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;

@inject(TerminalPreferences)
protected readonly terminalPreferences: TerminalPreferences;

registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: AI_TERMINAL_COMMAND.id,
Expand All @@ -71,7 +76,8 @@ export class AiTerminalCommandContribution implements CommandContribution, MenuC
if (currentTerminal instanceof TerminalWidgetImpl && currentTerminal.kind === 'user') {
new AiTerminalChatWidget(
currentTerminal,
this.terminalAgent
this.terminalAgent,
() => this.terminalPreferences['terminal.integrated.enableCommandHistory'] ?? false
);
}
},
Expand All @@ -96,7 +102,8 @@ class AiTerminalChatWidget {

constructor(
protected terminalWidget: TerminalWidgetImpl,
protected terminalAgent: AiTerminalAgent
protected terminalAgent: AiTerminalAgent,
protected getEnableCommandHistory: () => boolean
) {
this.chatContainer = document.createElement('div');
this.chatContainer.className = 'ai-terminal-chat-container';
Expand Down Expand Up @@ -185,12 +192,47 @@ class AiTerminalChatWidget {
}

protected getRecentTerminalCommands(): string[] {
// Character count for recent context when one line is 120 characters long.
const characterLimit = 1200;
if (this.getEnableCommandHistory()) {
const commandHistory = this.terminalWidget.commandHistoryState.commandHistory;
return this.extractContextFromTerminalOutput(commandHistory, characterLimit);
}

const maxLines = 100;
return this.terminalWidget.buffer.getLines(0,
this.terminalWidget.buffer.length > maxLines ? maxLines : this.terminalWidget.buffer.length
);
}

protected extractContextFromTerminalOutput(commandBlocks: TerminalBlock[], characterLimit: number): string[] {
const context: string[] = [];
let currentCharacters = 0;

for (let i = commandBlocks.length - 1; i >= 0; i--) {
const block = commandBlocks[i];
const blockCharacters = block.command.length + block.output.length;

if (currentCharacters + blockCharacters <= characterLimit) {
context.unshift(`${block.command}\n${block.output}`);
currentCharacters += blockCharacters;
} else {
const remainingCharacters = characterLimit - currentCharacters;
if (block.command.length <= remainingCharacters) {
const outputLimit = remainingCharacters - block.command.length;
const trimmedOutput = block.output.substring(0, outputLimit);
context.unshift(`${block.command}\n${trimmedOutput}`);
} else {
const trimmedCommand = block.command.substring(0, remainingCharacters);
context.unshift(trimmedCommand);
}
break;
}
}

return context;
}

protected getNextCommandIndex(step: number): number {
const currentIndex = this.commands.indexOf(this.chatResultParagraph.innerText);
const nextIndex = (currentIndex + step + this.commands.length) % this.commands.length;
Expand Down
6 changes: 6 additions & 0 deletions packages/debug/src/browser/debug-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,12 @@ export class DebugSession implements CompositeTreeElement {
if (!terminal) {
terminal = await this.terminalServer.newTerminal(options);
await terminal.start();
try {
// delay opening of the terminal until the terminal prompt appears to prevent duplicate commands in the terminal buffer
await waitForEvent(terminal.commandHistoryState.onTerminalPromptShown, 3000);
} catch (error) {
console.warn(`Terminal did not emit prompt in time, using it anyway: ${error}`);
}
}
this.terminalServer.open(terminal);
return terminal;
Expand Down
34 changes: 34 additions & 0 deletions packages/process/src/node/task-terminal-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,29 @@ export class TaskTerminalProcess extends TerminalProcess {

public exited = false;
public attachmentAttempted = false;
/**
* Controls whether OSC (Operating System Command) sequences are injected
* into the terminal stream for command history tracking.
*/
protected _enableCommandHistory = false;

setEnableCommandHistory(enable: boolean): void {
this._enableCommandHistory = enable;
}

/**
* injects the command to be tracked into the terminal output stream
* only if command history tracking is enabled
*/
injectCommandStartOsc(command: string): void {
if (this._enableCommandHistory) {
const encoded = Buffer.from(command).toString('hex');
this.ringBuffer.enq(`\x1b]133;command_started;${encoded}\x07`);
}
}

protected override onTerminalExit(code: number | undefined, signal: string | undefined): void {
this.injectCommandEndOsc();
this.emitOnExit(code, signal);
this.exited = true;
// Unregister process only if task terminal already attached (or failed attach),
Expand All @@ -38,4 +59,17 @@ export class TaskTerminalProcess extends TerminalProcess {
}
}

override kill(signal?: string): void {
this.injectCommandEndOsc();
super.kill(signal);
}

protected injectCommandEndOsc(): void {
if (this._enableCommandHistory) {
// Mark the task command as finished in command history tracking.
// OSC 133 'prompt_started' signals the end of command execution.
this.ringBuffer.enq('\x1b]133;prompt_started\x07');
}
}

}
5 changes: 5 additions & 0 deletions packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { TaskTerminalWidgetManager } from './task-terminal-widget-manager';
import { ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { Mutex } from 'async-mutex';
import { TaskContextKeyService } from './task-context-key-service';
import { TerminalPreferences } from '@theia/terminal/lib/common/terminal-preferences';

export interface QuickPickProblemMatcherItem {
problemMatchers: NamedProblemMatcher[] | undefined;
Expand Down Expand Up @@ -197,6 +198,9 @@ export class TaskService implements TaskConfigurationClient {
@inject(TaskContextKeyService)
protected readonly taskContextKeyService: TaskContextKeyService;

@inject(TerminalPreferences)
protected readonly terminalPreferences: TerminalPreferences;

@postConstruct()
protected init(): void {
this.getRunningTasks().then(tasks =>
Expand Down Expand Up @@ -999,6 +1003,7 @@ export class TaskService implements TaskConfigurationClient {
const taskLabel = resolvedTask.label;
let taskInfo: TaskInfo | undefined;
try {
resolvedTask.enableCommandHistory = this.terminalPreferences['terminal.integrated.enableCommandHistory'] ?? false;
taskInfo = await this.taskServer.run(resolvedTask, this.getContext(), option);
this.lastTask = { resolvedTask, option };
this.logger.debug(`Task created. Task id: ${taskInfo.taskId}`);
Expand Down
6 changes: 6 additions & 0 deletions packages/task/src/common/task-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ export interface TaskConfiguration extends TaskCustomization {
readonly label: string;
readonly _scope: TaskConfigurationScope;
readonly executionType?: 'shell' | 'process' | 'customExecution';
/**
* Whether to enable command history tracking for this task's terminal.
* When enabled, OSC sequences are injected to mark command boundaries.
* Defaults to false if not specified.
*/
enableCommandHistory?: boolean;
}

export interface ContributedTaskConfiguration extends TaskConfiguration {
Expand Down
21 changes: 19 additions & 2 deletions packages/task/src/node/process/process-task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
Process,
TerminalProcessOptions,
TaskTerminalProcessFactory,
TaskTerminalProcess,
} from '@theia/process/lib/node';
import {
ShellQuotedString, ShellQuotingFunctions, BashQuotingFunctions, CmdQuotingFunctions, PowershellQuotingFunctions, createShellCommandLine, ShellQuoting,
Expand Down Expand Up @@ -86,6 +87,9 @@ export class ProcessTaskRunner implements TaskRunner {
// - shell: defer the spawning to a shell that will evaluate a command line with our executable.
const terminalProcessOptions = this.getResolvedCommand(taskConfig);
const terminal: Process = this.taskTerminalProcessFactory(terminalProcessOptions);
const processType = (taskConfig.executionType || taskConfig.type) as 'process' | 'shell';
const command = this.getCommand(processType, terminalProcessOptions);
this.setupTaskTerminalCommandHistory(terminal, taskConfig.enableCommandHistory ?? false, command);

// Wait for the confirmation that the process is successfully started, or has failed to start.
await new Promise((resolve, reject) => {
Expand All @@ -95,21 +99,34 @@ export class ProcessTaskRunner implements TaskRunner {
});
});

const processType = (taskConfig.executionType || taskConfig.type) as 'process' | 'shell';
return this.taskFactory({
label: taskConfig.label,
process: terminal,
processType,
context: ctx,
config: taskConfig,
command: this.getCommand(processType, terminalProcessOptions)
command: command,
});
} catch (error) {
this.logger.error(`Error occurred while creating task: ${error}`);
throw error;
}
}

/**
* Enables or disables command history tracking for the task terminal.
* When enabled, OSC sequences are injected into the terminal output stream
* to mark command boundaries for history tracking.
*/
protected setupTaskTerminalCommandHistory(terminal: Process, enable: boolean, command?: string): void {
if (terminal instanceof TaskTerminalProcess) {
terminal.setEnableCommandHistory(enable);
}
if (terminal instanceof TaskTerminalProcess && command) {
terminal.injectCommandStartOsc(command);
}
}

protected getResolvedCommand(taskConfig: TaskConfiguration): TerminalProcessOptions {
const osSpecificCommand = this.getOsSpecificCommand(taskConfig);

Expand Down
Loading
Loading