Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9e27c7d
filepart handling in IIP
jerch Apr 29, 2026
05ea5ac
implement ReportCellSize
jerch Apr 30, 2026
51c8d8a
implement ReportCellSize
jerch Apr 30, 2026
53554a0
use _coreBrowserService.dpr
jerch Apr 30, 2026
83e3657
fix _mouseCoordsService access in demo
jerch Apr 30, 2026
645865e
fix cursor placement in IIP
jerch Apr 30, 2026
494cf1f
make linter happy
jerch Apr 30, 2026
dc1ae45
package upgrades & linter fixes
jerch May 9, 2026
44a27dc
make webfont loading tests more resilient
jerch May 9, 2026
636baa8
make webfont loading tests more resilient
jerch May 9, 2026
740fa44
make webfont loading tests more resilient
jerch May 9, 2026
224167b
Merge branch 'master' into iip_enhancements
jerch May 9, 2026
d92ca87
add qoi metrics test
jerch May 10, 2026
eb58cef
add integration tests for filepart
jerch May 10, 2026
a2f321c
add multipart demo
jerch May 10, 2026
f82a802
fix README
jerch May 10, 2026
aa93189
Merge branch 'master' into package_upgrades
jerch May 11, 2026
772df0c
Merge branch 'master' into iip_enhancements
jerch May 11, 2026
ae61169
Merge branch 'master' into iip_enhancements
jerch May 21, 2026
8519ac6
Merge branch 'master' into package_upgrades
jerch May 21, 2026
5a6427e
Merge branch 'master' into package_upgrades
jerch May 21, 2026
acd2472
Merge remote-tracking branch 'upstream/master' into package_upgrades
Tyriar May 28, 2026
2069176
Fix lint
Tyriar May 28, 2026
743852b
Remove unneeded comment
Tyriar May 28, 2026
0c4ec34
Remove unnecessary @types/lru-cache
Tyriar May 28, 2026
d3352cf
Merge pull request #5873 from xtermjs/package_upgrades
Tyriar May 28, 2026
38f77fd
Merge branch 'master' into iip_enhancements
Tyriar May 28, 2026
b200386
Merge pull request #5859 from xtermjs/iip_enhancements
Tyriar May 28, 2026
e961336
Add Cursor Cloud specific instructions to AGENTS.md
cursoragent May 28, 2026
00e5453
Merge pull request #5959 from xtermjs/cursor/setup-dev-environment-dc0c
Tyriar May 28, 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
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,15 @@ const cell = line?.getCell(0);
## Writing unit tests

- Unit tests live alongside the source code file of the thing it's testing with a .test.ts suffix.

## Cursor Cloud specific instructions

**Demo server**: Start with `npm start` (port 3000). The demo server uses node-pty to spawn real shell sessions over WebSocket. Integration tests auto-start it via Playwright's `webServer` config, so you don't need to start it manually for `npm run test-integration`.

**Build before testing**: Always run `npm run build && npm run esbuild` before `npm run test-unit`. Integration tests also need `npm run esbuild-demo-client` and `npm run esbuild-demo-server`. The update script handles this automatically on session start.

**No external services**: This project has zero external dependencies (no databases, Docker, or APIs). Everything runs locally with Node.js.

**TypeScript compiler**: The project uses `tsgo` (native TypeScript compiler preview) rather than standard `tsc`. It's installed via the `@typescript/native-preview` package.

**Lint only changed files**: Prefer `npm run lint-changes` over `npm run lint` when iterating on code changes — it's significantly faster.
51 changes: 20 additions & 31 deletions addons/addon-image/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## @xterm/addon-image

Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).
Inline image output in xterm.js. Supports SIXEL, iTerm's inline image protocol (IIP)
and partially Kitty's terminal graphics prtocol (TGP).


![](fixture/example.png)
Expand Down Expand Up @@ -43,9 +44,6 @@ terminal.loadAddon(imageAddon);

### General Notes

- *IMPORTANT:* The worker approach as done in previous releases got removed.
The addon contructor no longer expects a worker path as first argument.

- By default the addon will activate these `windowOptions` reports on the terminal:
- getWinSizePixels (CSI 14 t)
- getCellSizePixels (CSI 16 t)
Expand Down Expand Up @@ -73,7 +71,7 @@ terminal.loadAddon(imageAddon);

- **Cursor Positioning**
If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode).
Other cursor positioning modes as used by xterm or mintty are not supported.
Other cursor positioning modes as used by xterm or mintty are not supported for sixel.

- **SIXEL Palette Handling**
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
Expand All @@ -100,17 +98,24 @@ terminal.loadAddon(imageAddon);
Set by default, change it with `{iipSupport: true}`.

The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
- Supported formats: PNG, JPEG and GIF
- Supported formats: PNG, JPEG, GIF and QOI
- No animation support.
- Image type hinting is not supported (always deducted from data header).
- File download is not supported.
- Filename gets parsed but not used.
- Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed).
- Payload size may not exceed CEIL(sizeParameter * 4 / 3).
- Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`).
- Image pixel size is restricted by `pixelLimit` (pre- and post resizing).
- Size parameter is restricted by `iipSizeLimit`.
- Cursor positioning behaves the same as for sixel (see above).
- Playload size is restricted by `iipSizeLimit`.
- size parameter is not mandatory anymore.
- Cursor gets positioned at the next cell of the last image line.
- ReportCellSize sequence is supported.
- Hi-res display support is currently limited (image gets blurred).

- **Kitty Graphics Support (TGP)**
Set by default, change it with `{kittySupport: true}`.

Note that the kitty graphics support is still WIP.


### Storage and Drawing Settings
Expand All @@ -127,6 +132,7 @@ The addon exposes two properties to interact with the storage limits at runtime:
By default the addon will show a placeholder pattern for evicted images that are still part
of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`.


### Image Data Retrieval

The addon provides the following API endpoints to retrieve raw image data as canvas:
Expand All @@ -140,6 +146,7 @@ The addon provides the following API endpoints to retrieve raw image data as can
The buffer position is the 0-based absolute index (including scrollback at top).
Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions.


### Memory Usage

The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
Expand Down Expand Up @@ -180,8 +187,8 @@ _How can I adjust the memory usage?_
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
- `storageLimit`
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
- `sixelSizeLimit`
A constructor setting. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken SIXEL sequences at an earlier phase, thus may stop the decoder from entering its memory intensive task for potentially invalid data.
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
Constructor settings. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.


### Terminal Interaction
Expand Down Expand Up @@ -211,23 +218,5 @@ _How can I adjust the memory usage?_

### Status

- Sixel support and image handling in xterm.js is considered beta quality.
- IIP support is in alpha stage. Please file a bug for any awkwardities.


### Changelog

- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
- 0.4.3 defer canvas creation
- 0.4.2 fix image canvas resize
- 0.4.1 compat release for xterm.js 5.2.0
- 0.4.0 IIP support
- 0.3.1 compat release for xterm.js 5.1.0
- 0.3.0 important change: worker removed from addon
- 0.2.0 compat release for xterm.js 5.0.0
- 0.1.3 bugfix: avoid striping
- 0.1.2 bugfix: reset clear flag
- 0.1.1 bugfixes:
- clear sticky image tiles on render
- create folder from bootstrap.sh
- fix peer dependency in package.json
- Sixel and IIP support and image handling in xterm.js is considered beta quality.
- Kitty support is in alpha stage. Please file a bug for any awkwardities or missing features.
Binary file added addons/addon-image/fixture/testimages/dice.qoi
Binary file not shown.
86 changes: 70 additions & 16 deletions addons/addon-image/src/IIPHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IIPImageStorage } from './IIPImageStorage';
import { CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';

// Local const enum mirror - esbuild can't inline const enums from external packages
Expand All @@ -23,6 +23,7 @@ const enum DecoderConst {

// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
type: SequenceType.INVALID,
name: 'Unnamed file',
size: 0,
width: 'auto',
Expand All @@ -39,6 +40,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _dec: Base64Decoder;
private _qoiDec: QoiDecoder;
private _metrics = UNSUPPORTED_TYPE;
private _isMultipart = false;
private _abortMulti = false;

constructor(
private readonly _opts: IImageAddonOptions,
Expand All @@ -52,11 +55,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
}

public reset(): void {}
public reset(): void {
this._hp.reset();
this._dec.release();
this._qoiDec.release();
}

public start(): void {
this._aborted = false;
this._header = DEFAULT_HEADER;
this._metrics = UNSUPPORTED_TYPE;
this._hp.reset();
}
Expand All @@ -76,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return;
}
if (dataPos > 0) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
const seqType = this._hp.fields.type;
if (seqType === SequenceType.FILE) {
if (this._isMultipart) {
this._isMultipart = false;
this._abortMulti = false;
this._dec.release();
}
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline) {
this._aborted = true;
return;
}
this._dec.init();
} else if (this._abortMulti) {
this._aborted = true;
return;
}
this._dec.init();
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
if (this._isMultipart) this._abortMulti = true;
}
}
}
Expand All @@ -93,22 +111,58 @@ export class IIPHandler implements IOscHandler, IResetHandler {
public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted) return true;

if (this._hp.state !== HeaderState.END) {
if (this._hp.end()) return true;
}
const seqType = this._hp.fields.type;

if (seqType === SequenceType.FILEPART) return true;

if (seqType === SequenceType.REPORTCELLSIZE) {
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
let width = CELL_SIZE_DEFAULT.width;
let height = CELL_SIZE_DEFAULT.height;
if (this._renderer.dimensions) {
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
}
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
this._coreTerminal.input(report, false);
return true;
}

if (seqType === SequenceType.MULTIPARTFILE) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
this._isMultipart = true;
this._abortMulti = false;
this._dec.release();
this._dec.init();
return true;
}

if (seqType === SequenceType.FILEEND) {
if (!this._isMultipart) return true;
this._isMultipart = false;
if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
}

// fallthrough for SequenceType.FILE & SequenceType.FILEEND

let w = 0;
let h = 0;

// early exit condition chain
let cond: number | boolean = true;
let cond: number | boolean;
if (cond = success) {
if (cond = !this._dec.end()) {
if (cond = this._dec.data8.length === this._header.size) {
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
}
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
}
}
}
Expand Down
Loading
Loading