mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
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:
+77
-20
@@ -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';
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user