Skip to content

Commit 40957f0

Browse files
committed
feat: rich terminal detail pages, CLI probes expanded, vt100 dedup fix
- Add multi-paragraph descriptions for all terminals (Ghostty, Kitty, iTerm2, Terminal.app, Warp, cmux, VS Code, Cursor) with history, architecture, and notable features - Display body HTML on terminal detail pages with v-html - Add 17 new probes to CLI (device, input, extensions, modes, unicode) — CLI now tests 128+ features matching headless census - Add 17 new probes to app harness for real terminal testing - Fix app-runner to write results to app/ subdirectory with correct format - Remove duplicate vt100-0.1.0.json (only keep 0.2.1) - Remove misplaced app results from headless results directory - Extract shared CSS (result cells, tooltips) into theme files
1 parent 00115d0 commit 40957f0

23 files changed

Lines changed: 788 additions & 210 deletions

cli/src/probes/index.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,283 @@ const extSemanticPrompts: Probe = {
11881188
},
11891189
}
11901190

1191+
const extOsc633Vscode: Probe = {
1192+
id: "extensions.osc-633-vscode",
1193+
name: "VS Code shell integration (OSC 633)",
1194+
async run() {
1195+
// OSC 633 is VS Code's shell integration markers (prompt, command, output)
1196+
process.stdout.write("\x1b]633;A\x07") // prompt start
1197+
process.stdout.write("\x1b]633;B\x07") // prompt end
1198+
process.stdout.write("\x1b]633;C\x07") // pre-execution
1199+
process.stdout.write("\x1b]633;D;0\x07") // execution finished (exit code 0)
1200+
const pos = await queryCursorPosition()
1201+
return {
1202+
pass: pos !== null,
1203+
note: pos ? undefined : "No cursor response after OSC 633",
1204+
}
1205+
},
1206+
}
1207+
1208+
const extNotifications: Probe = {
1209+
id: "extensions.notifications",
1210+
name: "Desktop notifications (OSC 9)",
1211+
async run() {
1212+
// OSC 9 sends a desktop notification (used by ConEmu, iTerm2, etc.)
1213+
process.stdout.write("\x1b]9;Test\x07")
1214+
const pos = await queryCursorPosition()
1215+
return {
1216+
pass: pos !== null,
1217+
note: pos ? undefined : "No cursor response after OSC 9",
1218+
}
1219+
},
1220+
}
1221+
1222+
const extIterm2Images: Probe = {
1223+
id: "extensions.iterm2-images",
1224+
name: "iTerm2 inline images (OSC 1337)",
1225+
async run() {
1226+
// Send a minimal iTerm2 inline image sequence
1227+
process.stdout.write("\x1b]1337;File=inline=1:AAAA\x07")
1228+
const pos = await queryCursorPosition()
1229+
return {
1230+
pass: pos !== null,
1231+
note: pos ? undefined : "No cursor response after OSC 1337",
1232+
}
1233+
},
1234+
}
1235+
1236+
// ═══════════════════════════════════════════════════════════════════════════
1237+
// ── Device probes (additional) ──
1238+
// ═══════════════════════════════════════════════════════════════════════════
1239+
1240+
const secondaryDA: Probe = {
1241+
id: "device.secondary-da",
1242+
name: "Secondary device attributes (DA2)",
1243+
async run() {
1244+
const match = await query("\x1b[>c", /\x1b\[>([0-9;]+)c/, 1000)
1245+
if (!match) return { pass: false, note: "No DA2 response" }
1246+
return { pass: true, response: match[0] }
1247+
},
1248+
}
1249+
1250+
const tertiaryDA: Probe = {
1251+
id: "device.tertiary-da",
1252+
name: "Tertiary device attributes (DA3)",
1253+
async run() {
1254+
// DA3 responds with DCS ! | <unit-id> ST
1255+
const match = await queryWithSentinel("\x1b[=c", /\x1bP!?\|([^\x1b]*)\x1b\\/)
1256+
if (match) return { pass: true, response: match[1] }
1257+
return { pass: false, note: "No DA3 response" }
1258+
},
1259+
}
1260+
1261+
// ═══════════════════════════════════════════════════════════════════════════
1262+
// ── Input protocol probes ──
1263+
// ═══════════════════════════════════════════════════════════════════════════
1264+
1265+
const inputModifyOtherKeys: Probe = {
1266+
id: "input.modify-other-keys",
1267+
name: "Modify other keys (mode 2)",
1268+
async run() {
1269+
// Enable modifyOtherKeys mode 2 (xterm CSI u-style reporting)
1270+
process.stdout.write("\x1b[>4;2m")
1271+
const pos = await queryCursorPosition()
1272+
// Disable modifyOtherKeys
1273+
process.stdout.write("\x1b[>4;0m")
1274+
return {
1275+
pass: pos !== null,
1276+
note: pos ? undefined : "No cursor response after enabling modifyOtherKeys",
1277+
}
1278+
},
1279+
}
1280+
1281+
const inputPixelMouse: Probe = {
1282+
id: "input.pixel-mouse",
1283+
name: "Pixel mouse mode (1016)",
1284+
async run() {
1285+
process.stdout.write("\x1b[?1016h") // enable pixel mouse
1286+
const pos = await queryCursorPosition()
1287+
process.stdout.write("\x1b[?1016l") // disable
1288+
return {
1289+
pass: pos !== null,
1290+
note: pos ? undefined : "No cursor response after enabling pixel mouse",
1291+
}
1292+
},
1293+
}
1294+
1295+
const inputUrxvtMouse: Probe = {
1296+
id: "input.urxvt-mouse",
1297+
name: "urxvt mouse mode (1015)",
1298+
async run() {
1299+
process.stdout.write("\x1b[?1015h") // enable urxvt mouse
1300+
const pos = await queryCursorPosition()
1301+
process.stdout.write("\x1b[?1015l") // disable
1302+
return {
1303+
pass: pos !== null,
1304+
note: pos ? undefined : "No cursor response after enabling urxvt mouse",
1305+
}
1306+
},
1307+
}
1308+
1309+
const inputX10Mouse: Probe = {
1310+
id: "input.x10-mouse",
1311+
name: "X10 mouse mode (9)",
1312+
async run() {
1313+
process.stdout.write("\x1b[?9h") // enable X10 mouse
1314+
const pos = await queryCursorPosition()
1315+
process.stdout.write("\x1b[?9l") // disable
1316+
return {
1317+
pass: pos !== null,
1318+
note: pos ? undefined : "No cursor response after enabling X10 mouse",
1319+
}
1320+
},
1321+
}
1322+
1323+
const inputButtonEventMouse: Probe = {
1324+
id: "input.button-event-mouse",
1325+
name: "Button-event mouse mode (1002)",
1326+
async run() {
1327+
process.stdout.write("\x1b[?1002h") // enable button-event mouse
1328+
const pos = await queryCursorPosition()
1329+
process.stdout.write("\x1b[?1002l") // disable
1330+
return {
1331+
pass: pos !== null,
1332+
note: pos ? undefined : "No cursor response after enabling button-event mouse",
1333+
}
1334+
},
1335+
}
1336+
1337+
// ═══════════════════════════════════════════════════════════════════════════
1338+
// ── Mode probes (additional) ──
1339+
// ═══════════════════════════════════════════════════════════════════════════
1340+
1341+
const modesLeftRightMargin: Probe = {
1342+
id: "modes.left-right-margin",
1343+
name: "Left/right margin mode (DECLRMM)",
1344+
async run() {
1345+
process.stdout.write("\x1b[?69h") // enable DECLRMM
1346+
const pos = await queryCursorPosition()
1347+
process.stdout.write("\x1b[?69l") // disable
1348+
return {
1349+
pass: pos !== null,
1350+
note: pos ? undefined : "No cursor response after DECLRMM",
1351+
}
1352+
},
1353+
}
1354+
1355+
// ═══════════════════════════════════════════════════════════════════════════
1356+
// ── Cursor probes (additional) ──
1357+
// ═══════════════════════════════════════════════════════════════════════════
1358+
1359+
const cursorReverseWrap: Probe = {
1360+
id: "cursor.reverse-wrap",
1361+
name: "Reverse wrap mode (DECSET 45)",
1362+
async run() {
1363+
process.stdout.write("\x1b[?45h") // enable reverse wrap
1364+
const pos = await queryCursorPosition()
1365+
process.stdout.write("\x1b[?45l") // disable
1366+
return {
1367+
pass: pos !== null,
1368+
note: pos ? undefined : "No cursor response after enabling reverse wrap",
1369+
}
1370+
},
1371+
}
1372+
1373+
// ═══════════════════════════════════════════════════════════════════════════
1374+
// ── Erase probes (additional) ──
1375+
// ═══════════════════════════════════════════════════════════════════════════
1376+
1377+
const eraseSelective: Probe = {
1378+
id: "erase.selective",
1379+
name: "Selective erase (DECSED)",
1380+
async run() {
1381+
process.stdout.write("\x1b[1;1H\x1b[2K")
1382+
process.stdout.write("ABCDE")
1383+
process.stdout.write("\x1b[?2J") // DECSED — selective erase entire screen
1384+
const pos = await queryCursorPosition()
1385+
if (!pos) return { pass: false, note: "No cursor response after DECSED" }
1386+
return { pass: true }
1387+
},
1388+
}
1389+
1390+
// ═══════════════════════════════════════════════════════════════════════════
1391+
// ── Text probes (additional) ──
1392+
// ═══════════════════════════════════════════════════════════════════════════
1393+
1394+
const textReverseIndexScroll: Probe = {
1395+
id: "text.reverse-index-scroll",
1396+
name: "Reverse index scrolls at top of region",
1397+
async run() {
1398+
// Set scroll region, move to top of region, RI should scroll content down
1399+
process.stdout.write("\x1b[3;10r") // scroll region rows 3-10
1400+
process.stdout.write("\x1b[3;1H") // move to row 3 (top of region)
1401+
process.stdout.write("\x1bM") // RI — reverse index at top of region
1402+
const pos = await queryCursorPosition()
1403+
process.stdout.write("\x1b[r") // reset scroll region
1404+
if (!pos) return { pass: false, note: "No cursor response after RI in region" }
1405+
// Cursor should stay at row 3 (content scrolled down within region)
1406+
return {
1407+
pass: pos[0] === 3,
1408+
note: pos[0] === 3 ? undefined : `cursor at row ${pos[0]}, expected 3`,
1409+
}
1410+
},
1411+
}
1412+
1413+
// ═══════════════════════════════════════════════════════════════════════════
1414+
// ── Unicode probes ──
1415+
// ═══════════════════════════════════════════════════════════════════════════
1416+
1417+
const unicodeEastAsianAmbiguous: Probe = {
1418+
id: "unicode.east-asian-ambiguous",
1419+
name: "East Asian ambiguous width",
1420+
async run() {
1421+
// ● (U+25CF BLACK CIRCLE) is East Asian Ambiguous — most terminals render as 1 col
1422+
// Write ●X and check cursor position
1423+
const width = await measureRenderedWidth("●")
1424+
if (width === null) return { pass: false, note: "Cannot measure width" }
1425+
return {
1426+
pass: width === 1 || width === 2,
1427+
note: `width=${width} (ambiguous chars vary by terminal/locale)`,
1428+
response: String(width),
1429+
}
1430+
},
1431+
}
1432+
1433+
const unicodeWrapBoundary: Probe = {
1434+
id: "unicode.wrap-boundary",
1435+
name: "Wide char wrap at line boundary",
1436+
async run() {
1437+
const cols = process.stdout.columns || 80
1438+
// Write (cols-1) narrow chars, then a wide char — should wrap to next line
1439+
process.stdout.write("\x1b[1;1H\x1b[2J")
1440+
process.stdout.write("A".repeat(cols - 1))
1441+
process.stdout.write("\u4e2d") // CJK char (2 cols wide)
1442+
const pos = await queryCursorPosition()
1443+
if (!pos) return { pass: false, note: "No cursor response" }
1444+
// Wide char should wrap: cursor at row 2, col 3 (wide char on col 1-2 of row 2)
1445+
return {
1446+
pass: pos[0] === 2,
1447+
note: pos[0] === 2 ? undefined : `cursor at row ${pos[0]}, expected 2 (wide char should wrap)`,
1448+
}
1449+
},
1450+
}
1451+
1452+
const unicodeTabStops: Probe = {
1453+
id: "unicode.tab-stops",
1454+
name: "Tab stops with text",
1455+
async run() {
1456+
process.stdout.write("\x1b[1;1H\x1b[2K")
1457+
process.stdout.write("A\tB")
1458+
const pos = await queryCursorPosition()
1459+
if (!pos) return { pass: false, note: "No cursor response" }
1460+
// A at col 1, tab to col 9, B at col 9 → cursor at col 10
1461+
return {
1462+
pass: pos[1] === 10,
1463+
note: pos[1] === 10 ? undefined : `cursor at col ${pos[1]}, expected 10 (A + tab to 9 + B)`,
1464+
}
1465+
},
1466+
}
1467+
11911468
// ═══════════════════════════════════════════════════════════════════════════
11921469
// ── All probes ──
11931470
// ═══════════════════════════════════════════════════════════════════════════
@@ -1206,9 +1483,12 @@ export const ALL_PROBES: Probe[] = [
12061483
cursorMoveHome,
12071484
cursorHorizontalAbsolute,
12081485
cursorNextLine,
1486+
cursorReverseWrap,
12091487

12101488
// ── Device ──
12111489
primaryDA,
1490+
secondaryDA,
1491+
tertiaryDA,
12121492
deviceStatusReport,
12131493
deviceDecrpm,
12141494

@@ -1228,6 +1508,7 @@ export const ALL_PROBES: Probe[] = [
12281508
combiningChars,
12291509
tabStop,
12301510
backspace,
1511+
textReverseIndexScroll,
12311512

12321513
// ── Erase ──
12331514
eraseLineRight,
@@ -1238,6 +1519,7 @@ export const ALL_PROBES: Probe[] = [
12381519
eraseScreenAbove,
12391520
eraseScreenScrollback,
12401521
eraseCharacter,
1522+
eraseSelective,
12411523

12421524
// ── Editing ──
12431525
insertChars,
@@ -1252,6 +1534,7 @@ export const ALL_PROBES: Probe[] = [
12521534
modesBracketedPaste,
12531535
modesInsertReplace,
12541536
modesApplicationKeypad,
1537+
modesLeftRightMargin,
12551538

12561539
// ── Modes (DECRPM with behavioral fallback) ──
12571540
behavioralModeProbe(
@@ -1378,6 +1661,21 @@ export const ALL_PROBES: Probe[] = [
13781661
extOsc8Hyperlink,
13791662
extOsc0IconTitle,
13801663
extSemanticPrompts,
1664+
extOsc633Vscode,
1665+
extNotifications,
1666+
extIterm2Images,
1667+
1668+
// ── Input protocols ──
1669+
inputModifyOtherKeys,
1670+
inputPixelMouse,
1671+
inputUrxvtMouse,
1672+
inputX10Mouse,
1673+
inputButtonEventMouse,
1674+
1675+
// ── Unicode ──
1676+
unicodeEastAsianAmbiguous,
1677+
unicodeWrapBoundary,
1678+
unicodeTabStops,
13811679

13821680
// ── Previously "untestable" features ──
13831681

docs/.vitepress/config.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,27 +195,18 @@ function buildSidebar() {
195195
const slugA = appTerminalSlugs.get(a)
196196
const slugB = appTerminalSlugs.get(b)
197197
if (slugA && slugB) {
198+
// Sort slugs alphabetically to match paths.ts URL generation
199+
const [sortedSlugA, sortedSlugB, labelA, labelB] =
200+
slugA.localeCompare(slugB) <= 0 ? [slugA, slugB, a, b] : [slugB, slugA, b, a]
198201
compareItems.push({
199-
text: `${a} vs ${b}`,
200-
link: `/compare/${slugA}-vs-${slugB}`,
202+
text: `${labelA} vs ${labelB}`,
203+
link: `/compare/${sortedSlugA}-vs-${sortedSlugB}`,
201204
})
202205
}
203206
}
204207

205208
const sidebar = [
206209
{ text: "Matrix", link: "/" },
207-
{
208-
text: "Terminals",
209-
items: appTerminals,
210-
},
211-
{
212-
text: "Compare",
213-
items: compareItems,
214-
},
215-
{
216-
text: "Backends",
217-
items: terminals,
218-
},
219210
{
220211
text: "Categories",
221212
items: sortedCategories.map((cat) => ({
@@ -235,6 +226,18 @@ function buildSidebar() {
235226
link: `/${tag}`,
236227
})),
237228
},
229+
{
230+
text: "Terminals",
231+
items: appTerminals,
232+
},
233+
{
234+
text: "Compare",
235+
items: compareItems,
236+
},
237+
{
238+
text: "Backends",
239+
items: terminals,
240+
},
238241
{ text: "API", link: "/api" },
239242
{ text: "About", link: "/about" },
240243
]

0 commit comments

Comments
 (0)