feat(statusline): add opt-in context_position config for narrow terminals (#2937)
Extract composeStatusline() helper from duplicated inline template logic in
runStatusline() and renderStatusline(). Both call sites now route through the
helper, which accepts a position param ('end' | 'front', default 'end').
- 'end' (default) preserves byte-identical output to v1.38.x and earlier
- 'front' renders ctx immediately after model name, before the first │
- Invalid values silently coerce to 'end' at runtime (belt-and-suspenders;
config-set rejects invalid values upfront via enum validator)
Adds statusline.context_position to VALID_CONFIG_KEYS in both CJS and TS
schemas, enum validator in config.cjs, docs row in CONFIGURATION.md,
and a changeset. Closes #2937.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -419,10 +419,11 @@ function runStatusline() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Last-slash-command suffix (opt-in via statusline.show_last_command, #2538).
|
||||
// Last-slash-command suffix and context_position config (#2538, #2937).
|
||||
// Reads the active session transcript for the most recent <command-name> tag.
|
||||
// Failure here must never break the statusline — wrap the entire lookup.
|
||||
let lastCmdSuffix = '';
|
||||
let position = 'end';
|
||||
try {
|
||||
const cfg = readGsdConfig(dir);
|
||||
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
||||
@@ -432,6 +433,8 @@ function runStatusline() {
|
||||
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
||||
}
|
||||
}
|
||||
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
||||
if (cfgPos != null) position = cfgPos;
|
||||
} catch (e) {
|
||||
// Never break the statusline on config/transcript errors
|
||||
}
|
||||
@@ -444,21 +447,61 @@ function runStatusline() {
|
||||
? `\x1b[2m${gsdStateStr}\x1b[0m`
|
||||
: null;
|
||||
|
||||
if (middle) {
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ ${middle} │ \x1b[2m${dirname}\x1b[0m${ctx}${lastCmdSuffix}`);
|
||||
} else {
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}${lastCmdSuffix}`);
|
||||
}
|
||||
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, position }));
|
||||
} catch (e) {
|
||||
// Silent fail - don't break statusline on parse errors
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Layout composer --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Compose the statusline string from pre-built segments.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} [opts.gsdUpdate=''] - leading update/stale-hooks warning (already formatted)
|
||||
* @param {string} opts.model - model display name (plain text; dim styling applied here)
|
||||
* @param {string} [opts.ctx=''] - context-window meter segment (empty string = absent)
|
||||
* @param {string|null} [opts.middle=null] - middle segment (todo task or GSD state), null = absent
|
||||
* @param {string} opts.dirname - project directory basename (dim styling applied here)
|
||||
* @param {string} [opts.lastCmdSuffix=''] - last-command suffix, e.g. ' │ last: /foo'
|
||||
* @param {'end'|'front'} [opts.position='end']
|
||||
* - 'end' (default): ctx appended after dirname — preserved byte-for-byte
|
||||
* - 'front': ctx immediately after model name so the meter stays visible in narrow terminals
|
||||
*
|
||||
* Invalid position values are silently coerced to 'end' — config-set schema rejects
|
||||
* invalid values upfront; runtime fallback defends against stale/corrupt configs
|
||||
* without breaking the statusline.
|
||||
*/
|
||||
function composeStatusline({
|
||||
gsdUpdate = '',
|
||||
model,
|
||||
ctx = '',
|
||||
middle = null,
|
||||
dirname,
|
||||
lastCmdSuffix = '',
|
||||
position = 'end',
|
||||
} = {}) {
|
||||
const modelSeg = `\x1b[2m${model}\x1b[0m`;
|
||||
const dirSeg = `\x1b[2m${dirname}\x1b[0m`;
|
||||
// Coerce invalid values to 'end' (belt-and-suspenders; see JSDoc above)
|
||||
const pos = position === 'front' ? 'front' : 'end';
|
||||
|
||||
if (pos === 'front') {
|
||||
if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${lastCmdSuffix}`;
|
||||
return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${lastCmdSuffix}`;
|
||||
}
|
||||
// 'end' — preserved byte-for-byte relative to original inline templates
|
||||
if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
|
||||
return `${gsdUpdate}${modelSeg} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
|
||||
}
|
||||
|
||||
// Export helpers for unit tests. Harmless when run as a script.
|
||||
module.exports = {
|
||||
readGsdState, parseStateMd, formatGsdState,
|
||||
readGsdConfig, getConfigValue, readLastSlashCommand,
|
||||
composeStatusline,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -471,6 +514,7 @@ function renderStatusline(data) {
|
||||
const dirname = path.basename(dir);
|
||||
|
||||
let lastCmdSuffix = '';
|
||||
let position = 'end';
|
||||
try {
|
||||
const cfg = readGsdConfig(dir);
|
||||
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
||||
@@ -479,14 +523,13 @@ function renderStatusline(data) {
|
||||
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
||||
}
|
||||
}
|
||||
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
||||
if (cfgPos != null) position = cfgPos;
|
||||
} catch (e) { /* swallow */ }
|
||||
|
||||
const gsdStateStr = formatGsdState(readGsdState(dir) || {});
|
||||
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
||||
if (middle) {
|
||||
return `\x1b[2m${model}\x1b[0m │ ${middle} │ \x1b[2m${dirname}\x1b[0m${lastCmdSuffix}`;
|
||||
}
|
||||
return `\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${lastCmdSuffix}`;
|
||||
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, position });
|
||||
}
|
||||
|
||||
module.exports.renderStatusline = renderStatusline;
|
||||
|
||||
Reference in New Issue
Block a user