Merge pull request #469 from irisitymichaelgrundberg/feat/copy-dedent-pane-margin

feat(terminal): take the transcript gutter off a copy, at the width the CLI declares
This commit is contained in:
Codeman maintainer
2026-09-23 11:32:02 +02:00
16 changed files with 696 additions and 61 deletions
+17
View File
@@ -294,6 +294,23 @@ const capabilitiesSchema = z
effort: z.boolean(),
agentSkillInjection: z.boolean(),
statusLineTelemetry: z.boolean(),
// How many columns this CLI indents its transcript body by, so a copy can take
// that much off the clipboard. Bounded, because it is the whole strip: a copy
// never removes more than this, nor more than every selected line shares.
//
// ⚠ DECLARED, not measured off the pane, and two measured attempts are why.
// Asking whether the pane painted spaces across the unused part of each row
// separates a TUI from a shell perfectly where it fires and never
// over-stripped, but it is a function of pane WIDTH: that padding exists
// only while a rendered line stops short of the CLI's own layout width, and
// Claude Code's prose wraps to fill it — the share of padded rows on one
// live transcript ran 44%, 6%, 6%, 7% and 87% at 123, 160, 198, 235 and 298
// columns, so the strip did nothing at any ordinary size. Taking the
// narrowest indent on screen instead fires everywhere and over-strips, since
// a file listing inside the transcript can be the narrowest thing on it.
// A declared width cannot do either. Absent means no strip, so a CLI whose
// transcript layout nobody has measured is never touched.
transcriptGutter: z.number().int().min(1).max(8).optional(),
workDetect: z
.object({
promptGlyph: z.string().min(1).max(8),
+10
View File
@@ -210,6 +210,10 @@ const CLAUDE: CliEntry = {
},
capabilities: {
external: false,
// Claude indents its transcript body two columns and puts its own ●/✻/❯ markers
// in them, so a copy can drop two and paste flush. The only entry that declares
// this, because it is the only one whose gutter has been measured.
transcriptGutter: 2,
// The historical hard-coded pair, now stated as data. `workingLine` matches both the
// `✻ Actualizing… (39s · ↓ 2.0k tokens)` status line and the bare `esc to interrupt`
// footer, because tmux repaints partially and only one of the two may land in a chunk.
@@ -529,6 +533,12 @@ const CODEX: CliEntry = {
// braille spinner, and it never prints `esc to interrupt` at rest, so that phrase
// alone separates a running turn from an idle one.
workDetect: { promptGlyph: '›', workingLine: '[Ee]sc to interrupt' },
// Two columns, like claude's, measured on a live 0.154.0 answer: the `•`/`›`/`⚠`
// markers sit in the gutter, prose continuations sit at 2, and a nested YAML block
// the model wrote rendered at 2/4/6/8 for its own 0/2/4/6. Replayed at 100, 120,
// 160, 198, 235 and 282 columns the indents were 0, 2, 4, 6 and 8 at every one,
// never 1, so the width is not a function of the pane.
transcriptGutter: 2,
transcript: 'codex-rollout',
altScreen: 'strip-full',
echo: { policy: 'predict', anchor: { kind: 'cursor' }, predictProfile: 'codex' },
+23
View File
@@ -345,6 +345,29 @@ export interface CliCapabilities {
/** Source of a regex matching the status line this CLI draws while a turn runs. */
workingLine: string;
};
/**
* How many columns this CLI indents its transcript body by, so a copy taken from its
* pane can drop that much and paste flush. Claude Code indents two and puts its own
* markers in those columns.
*
* ⚠ DECLARED rather than measured off the pane, and two measured attempts are why.
* Asking whether the pane painted real spaces across the unused part of each row
* separates a TUI from a shell perfectly where it fires and never over-stripped; it
* is also a function of pane WIDTH, because that padding exists only while a
* rendered line stops short of the CLI's own layout width and Claude Code's prose
* wraps to fill it. On one live transcript the share of padded rows ran 44%, 6%, 6%,
* 7% and 87% at 123, 160, 198, 235 and 298 columns, so at any ordinary window size
* the strip silently did nothing. Taking the narrowest indent on the surrounding
* rows instead fires at every width and over-strips on roughly 1% of selections,
* because a file listing inside the transcript can be the narrowest thing on screen.
*
* A declared width can do neither. The strip is the lesser of this and what every
* selected line shares, so a block can only ever shift as a unit, and it can never
* shift further than the CLI itself says its gutter is.
*
* Absent means no strip at all, the same fail-safe direction `workDetect` takes.
*/
transcriptGutter?: number;
/** No direct-PTY fallback: the CLI must run inside tmux (secrets ride tmux setenv). */
requiresMux: boolean;
/**