ui(tui/assistant): remove inner top/bottom padding from code cards\n\n- Drop 1-row inner pads; keep only top/bottom border\n- Update height math: +2 (borders) instead of +4\n- Adjust render skip/visible calculations to match\n- Keeps left/right padding and language title logic intact

This commit is contained in:
James Peter
2025-08-29 17:44:33 +10:00
parent 986a764674
commit e4601bd4b3
2 changed files with 128 additions and 38 deletions
+77 -20
View File
@@ -30,6 +30,30 @@ function getTargetTriple() {
return `${rustArch}-${rustPlatform}`;
}
// Resolve a persistent user cache directory for binaries so that repeated
// npx installs can reuse a previously downloaded artifact and skip work.
function getCacheDir(version) {
const plt = platform();
const home = process.env.HOME || process.env.USERPROFILE || '';
let base = '';
if (plt === 'win32') {
base = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
} else if (plt === 'darwin') {
base = join(home, 'Library', 'Caches');
} else {
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
}
const dir = join(base, 'just-every', 'code', version);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
return dir;
}
function getCachedBinaryPath(version, targetTriple, isWindows) {
const ext = isWindows ? '.exe' : '';
const cacheDir = getCacheDir(version);
return join(cacheDir, `code-${targetTriple}${ext}`);
}
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
@@ -151,27 +175,33 @@ function validateDownloadedBinary(p) {
async function main() {
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
try {
const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
if (resolved) {
let contents = '';
try {
contents = readFileSync(resolved, 'utf8');
} catch {
contents = '';
}
const looksLikeOurs = contents.includes('@just-every/code') || contents.includes('bin/coder.js');
if (!looksLikeOurs) {
console.warn('[notice] Found an existing `code` on PATH at:');
console.warn(` ${resolved}`);
console.warn('[notice] We will still install our CLI, also available as `coder`.');
console.warn(' If `code` runs another tool, prefer using: coder');
console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
// Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
const ua = process.env.npm_config_user_agent || '';
const isNpx = ua.includes('npx');
const isGlobal = process.env.npm_config_global === 'true';
if (isGlobal && !isNpx) {
try {
const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
if (resolved) {
let contents = '';
try {
contents = readFileSync(resolved, 'utf8');
} catch {
contents = '';
}
const looksLikeOurs = contents.includes('@just-every/code') || contents.includes('bin/coder.js');
if (!looksLikeOurs) {
console.warn('[notice] Found an existing `code` on PATH at:');
console.warn(` ${resolved}`);
console.warn('[notice] We will still install our CLI, also available as `coder`.');
console.warn(' If `code` runs another tool, prefer using: coder');
console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
}
}
} catch {
// Ignore detection failures; proceed with install.
}
} catch {
// Ignore detection failures; proceed with install.
}
const targetTriple = getTargetTriple();
@@ -195,6 +225,7 @@ async function main() {
for (const binary of binaries) {
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
const localPath = join(binDir, binaryName);
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
// Skip if already exists and has correct permissions
if (existsSync(localPath)) {
@@ -211,6 +242,21 @@ async function main() {
}
continue;
}
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
try {
if (existsSync(cachePath)) {
const valid = validateDownloadedBinary(cachePath);
if (valid.ok) {
copyFileSync(cachePath, localPath);
if (!isWindows) chmodSync(localPath, 0o755);
console.log(`✓ Installed ${binaryName} from user cache`);
continue; // next binary
}
}
} catch {
// Ignore cache errors and fall through to normal paths
}
// First try platform package via npm optionalDependencies (fast path on npm CDN).
const require = createRequire(import.meta.url);
@@ -245,6 +291,12 @@ async function main() {
copyFileSync(src, localPath);
if (!isWindows) chmodSync(localPath, 0o755);
console.log(`✓ Installed ${binaryName} from ${platformPkg.name}`);
// Populate cache for future npx runs
try {
if (!existsSync(cachePath)) {
copyFileSync(localPath, cachePath);
}
} catch {}
continue; // next binary
} catch (e) {
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
@@ -317,6 +369,10 @@ async function main() {
}
console.log(`✓ Installed ${binaryName}`);
// Save into persistent cache for future fast installs
try {
copyFileSync(localPath, cachePath);
} catch {}
} catch (error) {
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
console.error(` Downloaded from: ${downloadUrl}`);
@@ -353,7 +409,8 @@ async function main() {
}
// With bin name = 'code', handle collisions (e.g., VS Code) and add legacy wrappers
try {
// Only do this for global installs; skip under npx/local to avoid extra work and collisions.
if (isGlobal && !isNpx) try {
const isTTY = process.stdout && process.stdout.isTTY;
const isWindows = platform() === 'win32';
+51 -18
View File
@@ -500,7 +500,7 @@ impl HistoryCell for AssistantMarkdownCell {
// - Apply bullet wrapping and horizontal rules transformation
// - One top padding row for the cell (+1)
// - Fenced code blocks: exclude language sentinel; trim blank padding lines;
// add 4 rows for borders + inner pads
// add 2 rows for borders (no inner top/bottom padding)
// Use full width for assistant text wrapping so bullets don't wrap early.
let text_wrap_width = width;
let mut all_lines = self.display_lines_trimmed();
@@ -528,8 +528,13 @@ impl HistoryCell for AssistantMarkdownCell {
// lines align under the content start.
let hang_cols =
unicode_width::UnicodeWidthStr::width(bullet_char.as_str()) + 2;
// Remove the leading bullet and its following single-space span so the
// first visual row starts with actual content. Without this, the bullet
// would still appear after the hanging indent and shift the first row's
// content to the right, causing misalignment with wrapped rows.
let stripped = strip_first_bullet_and_space(line.clone());
transformed.extend(wrap_hanging_line(
line,
stripped,
indent_spaces,
hang_cols,
text_wrap_width,
@@ -581,7 +586,7 @@ impl HistoryCell for AssistantMarkdownCell {
{
chunk.pop();
}
total = total.saturating_add(chunk.len() as u16 + 4);
total = total.saturating_add(chunk.len() as u16 + 2);
} else {
let text = Text::from(all_lines[start..i].to_vec());
let rows: u16 = Paragraph::new(text)
@@ -637,8 +642,11 @@ impl HistoryCell for AssistantMarkdownCell {
if is_first_output_line && bullet_char == "-" {
let hang_cols =
unicode_width::UnicodeWidthStr::width(bullet_char.as_str()) + 2;
// Remove the bullet + following space so the first row's text starts
// where the hanging indent expects, aligning with continuation rows.
let stripped = strip_first_bullet_and_space(line.clone());
transformed.extend(wrap_hanging_line(
line,
stripped,
indent_spaces,
hang_cols,
text_wrap_width,
@@ -771,7 +779,7 @@ impl HistoryCell for AssistantMarkdownCell {
let max_w = lines.iter().map(|l| measure_line(l)).max().unwrap_or(0) as u16;
let inner_w = max_w.max(1);
let card_w = inner_w.saturating_add(4).min(area.width.max(4));
let total = lines.len() as u16 + 4; // top/bottom border + inner padding rows
let total = lines.len() as u16 + 2; // top/bottom border only
if *skip >= total {
*skip -= total;
return;
@@ -788,19 +796,8 @@ impl HistoryCell for AssistantMarkdownCell {
top_border -= drop;
local_skip -= drop;
}
let mut top_pad = 1u16;
if local_skip > 0 {
let drop = local_skip.min(top_pad);
top_pad -= drop;
local_skip -= drop;
}
let code_skip = local_skip.min(lines.len() as u16);
local_skip -= code_skip;
let mut bottom_pad = 1u16;
if local_skip > 0 {
let drop = local_skip.min(bottom_pad);
bottom_pad -= drop;
}
let mut bottom_border = 1u16;
if local_skip > 0 {
let drop = local_skip.min(bottom_border);
@@ -808,9 +805,7 @@ impl HistoryCell for AssistantMarkdownCell {
}
// Compute drawable height in this pass
let visible = top_border
+ top_pad
+ (lines.len() as u16 - code_skip)
+ bottom_pad
+ bottom_border;
let draw_h = visible.min(avail);
if draw_h == 0 {
@@ -3347,6 +3342,44 @@ fn detect_bullet_prefix(line: &ratatui::text::Line<'_>) -> Option<(usize, String
None
}
/// For lines that begin with a markdown bullet produced by our renderer,
/// return a copy of the line with the bullet marker and its following single
/// space removed. Leading indent (spaces) is preserved.
fn strip_first_bullet_and_space(mut line: ratatui::text::Line<'static>) -> ratatui::text::Line<'static> {
let spans = &mut line.spans;
if spans.is_empty() {
return line;
}
// Compute index of bullet span after optional leading spaces
let mut idx = 0usize;
if let Some(s) = spans.get(0) {
let t = s.content.as_ref();
if !t.is_empty() && t.chars().all(|c| c == ' ') {
idx = 1;
}
}
// Must have bullet span and a following single-space span
if spans.len() <= idx + 1 {
return line;
}
let bullet = spans[idx].content.as_ref();
let space_is_single = spans[idx + 1].content.as_ref() == " ";
// Accept unordered bullets, checkboxes, and ordered bullets ("1.")
let unordered = ["-", "•", "◦", "·", "∘", "⋅", "☐", "✔"]; // same set as detect_bullet_prefix
let is_ordered = bullet.ends_with('.') && bullet[..bullet.len().saturating_sub(1)]
.chars()
.all(|c| c.is_ascii_digit());
if (unordered.contains(&bullet) || is_ordered) && space_is_single {
// Remove bullet and the following single space
spans.remove(idx); // bullet
// After removal, the former (idx + 1) becomes idx
if spans.len() > idx && spans[idx].content.as_ref() == " " {
spans.remove(idx);
}
}
line
}
// Wrap a bullet line with a hanging indent so wrapped lines align under the content start.
fn wrap_bullet_line(
mut line: ratatui::text::Line<'static>,