style: replaced ASCII ellipsis with Unicode character for improved typography

- Replaced ASCII ellipsis characters (three dots '...') with Unicode ellipsis character ('...') throughout the codebase for improved typography.
- Adjusted string truncation logic to account for single-character Unicode ellipsis instead of three-character ASCII ellipsis, reducing reserved space from 3 to 1 character in truncation calculations.
- Updated truncation offsets in multiple files (session-manager, agent, executor, footer) to preserve 2 additional characters before ellipsis due to more compact Unicode representation.
This commit is contained in:
can1357
2026-02-06 02:51:07 +01:00
parent 43473e3b0e
commit 5339369abd
30 changed files with 45 additions and 41 deletions
+1 -1
View File
@@ -261,7 +261,7 @@ Examples:
process.exit(1);
}
console.log(`Logging in to ${provider}...`);
console.log(`Logging in to ${provider}…`);
await login(provider);
return;
}
@@ -757,7 +757,7 @@ function getString(value: unknown): string | undefined {
function truncate(text: string, limit: number): string {
if (text.length <= limit) return text;
return `${text.slice(0, limit)}...[truncated ${text.length - limit}]`;
return `${text.slice(0, limit)}…[truncated ${text.length - limit}]`;
}
function formatCodexFailure(rawEvent: Record<string, unknown>): string | null {
+2
View File
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
### Added
- Support for `PI_PACKAGE_DIR` environment variable to override package directory (useful for Nix/Guix store paths)
@@ -29,6 +30,7 @@
### Changed
- Replaced ellipsis string (`...`) with Unicode ellipsis character (`…`) throughout UI text and truncation logic for improved typography
- Improved render performance by introducing caching for tool output blocks and search results to avoid redundant text width and padding computations
- Enhanced read tool grouping to reset when non-tool content (text/thinking blocks) appears between read calls, preventing unintended coalescing
- Improved string preview formatting in scalar values to show line counts and truncation indicators for multi-line strings
@@ -162,7 +162,7 @@ function cleanStepText(text: string): string {
// Truncate if too long
if (cleaned.length > 50) {
cleaned = `${cleaned.slice(0, 47)}...`;
cleaned = `${cleaned.slice(0, 49)}…`;
}
return cleaned;
+1 -1
View File
@@ -71,7 +71,7 @@ export default function (pi: HookAPI) {
// Run extraction with loader UI
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}…`);
loader.onAbort = () => done(null);
// Do the work
@@ -18,7 +18,7 @@ export default function (pi: HookAPI) {
turnCount++;
const theme = ctx.ui.theme;
const spinner = theme.fg("accent", "●");
const text = theme.fg("dim", ` Turn ${turnCount}...`);
const text = theme.fg("dim", ` Turn ${turnCount}…`);
ctx.ui.setStatus("status-demo", spinner + text);
});
@@ -28,7 +28,7 @@ console.log("Continued session:", continued.sessionFile);
const sessions = SessionManager.list(process.cwd());
console.log(`\nFound ${sessions.length} sessions:`);
for (const info of sessions.slice(0, 3)) {
console.log(` ${info.id.slice(0, 8)}... - "${info.firstMessage.slice(0, 30)}..."`);
console.log(` ${info.id.slice(0, 8)}… - "${info.firstMessage.slice(0, 30)}…"`);
}
if (sessions.length > 0) {
+2 -2
View File
@@ -190,7 +190,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
const nativePath = path.join(execDir, nativeAddonName);
const nativeTempPath = `${nativePath}.new`;
console.log(chalk.dim(`Downloading ${binaryName}...`));
console.log(chalk.dim(`Downloading ${binaryName}…`));
// Download to temp file
const response = await fetch(asset.url, { redirect: "follow" });
@@ -202,7 +202,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
await pipeline(response.body, fileStream);
// Download native addon
console.log(chalk.dim(`Downloading ${nativeAddonName}...`));
console.log(chalk.dim(`Downloading ${nativeAddonName}…`));
const nativeResponse = await fetch(nativeAsset.url, { redirect: "follow" });
if (!nativeResponse.ok || !nativeResponse.body) {
@@ -307,5 +307,5 @@ Call the missing tool(s) now.
function truncateToolArg(value: string): string {
if (value.length <= 40) return value;
return `${value.slice(0, 37)}...`;
return `${value.slice(0, 39)}…`;
}
@@ -54,7 +54,7 @@ export async function runChangelogFlow({
const updated: string[] = [];
for (const boundary of boundaries) {
onProgress?.(`Generating entries for ${boundary.changelogPath}...`);
onProgress?.(`Generating entries for ${boundary.changelogPath}…`);
const diff = await git.getDiffForFiles(boundary.files, true);
if (!diff.trim()) continue;
const stat = await git.getStatForFiles(boundary.files, true);
@@ -108,7 +108,7 @@ export async function applyChangelogProposals({
(!proposal.deletions || Object.keys(proposal.deletions).length === 0)
)
continue;
onProgress?.(`Applying entries for ${proposal.path}...`);
onProgress?.(`Applying entries for ${proposal.path}…`);
const exists = fs.existsSync(proposal.path);
if (!exists) {
logger.warn("commit changelog path missing", { path: proposal.path });
@@ -361,7 +361,7 @@ export class ReviewCommand implements CustomCommand {
const stats = parseDiff(diffResult.stdout);
// Even if all files filtered, include the custom instructions
return `${buildReviewPrompt(
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}...`,
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
stats,
diffResult.stdout,
)}\n\n### Additional Instructions\n\n${instructions}`;
+2 -2
View File
@@ -371,7 +371,7 @@ export function formatTextEdit(edit: TextEdit, maxLength = 50): string {
const range = `${edit.range.start.line + 1}:${edit.range.start.character + 1}`;
const preview =
edit.newText.length > maxLength
? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}...`
? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}…`
: edit.newText.replace(/\n/g, "\\n");
return `line ${range} ${theme.nav.cursor} "${preview}"`;
}
@@ -530,7 +530,7 @@ export function extractHoverText(
*/
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return `${str.slice(0, maxLength - 3)}...`;
return `${str.slice(0, maxLength - 1)}…`;
}
/**
@@ -211,11 +211,11 @@ export class FooterComponent implements Component {
// Truncate path if too long to fit width
if (pwd.length > width) {
const half = Math.floor(width / 2) - 2;
const half = Math.floor(width / 2) - 1;
if (half > 1) {
const start = pwd.slice(0, half);
const end = pwd.slice(-(half - 1));
pwd = `${start}...${end}`;
pwd = `${start}…${end}`;
} else {
pwd = pwd.slice(0, Math.max(1, width));
}
@@ -269,7 +269,7 @@ export class FooterComponent implements Component {
if (statsLeftWidth > width) {
// Truncate statsLeft to fit width (no room for right side)
const plainStatsLeft = statsLeft.replace(/\x1b\[[0-9;]*m/g, "");
statsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;
statsLeft = `${plainStatsLeft.substring(0, width - 1)}…`;
statsLeftWidth = visibleWidth(statsLeft);
}
@@ -547,7 +547,7 @@ export class SelectorController {
done();
if (mode === "login") {
this.ctx.showStatus(`Logging in to ${providerId}...`);
this.ctx.showStatus(`Logging in to ${providerId}…`);
try {
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
@@ -434,8 +434,7 @@ function formatSequenceMatchPreview(lines: string[], startIdx: number): string {
return previewLines
.map((line, i) => {
const num = start + i + 1;
const truncated =
line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 3)}...` : line;
const truncated = line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 1)}…` : line;
return ` ${num} | ${truncated}`;
})
.join("\n");
@@ -1103,7 +1102,7 @@ function computeReplacements(
return lines
.map((line, i) => {
const num = start + i + 1;
const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 3)}...` : line;
const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 1)}…` : line;
return ` ${num} | ${truncated}`;
})
.join("\n");
+1 -1
View File
@@ -241,7 +241,7 @@ export function findMatch(
const preview = previewLines
.map((line, idx) => {
const num = start + idx + 1;
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 3)}...` : line}`;
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 1)}…` : line}`;
})
.join("\n");
occurrencePreviews.push(preview);
+2 -2
View File
@@ -701,7 +701,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
onConnecting: serverNames => {
if (options.hasUI && serverNames.length > 0) {
process.stderr.write(
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}...
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…
`),
);
}
@@ -1076,7 +1076,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const result = await warmupLspServers(cwd, {
onConnecting: serverNames => {
if (options.hasUI && serverNames.length > 0) {
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}...\n`));
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}…\n`));
}
},
});
@@ -515,7 +515,7 @@ class RecentSessionInfo {
get name(): string {
if (this.#name) return this.#name;
const fullName = this.fullName;
this.#name = fullName.length <= 40 ? fullName : `${fullName.slice(0, 37)}...`;
this.#name = fullName.length <= 40 ? fullName : `${fullName.slice(0, 39)}…`;
return this.#name;
}
+1 -1
View File
@@ -270,7 +270,7 @@ function extractToolArgsPreview(args: Record<string, unknown>): string {
for (const key of previewKeys) {
if (args[key] && typeof args[key] === "string") {
const value = args[key] as string;
return value.length > 60 ? `${value.slice(0, 57)}...` : value;
return value.length > 60 ? `${value.slice(0, 59)}…` : value;
}
}
@@ -10,7 +10,7 @@ function toError(value: unknown): Error {
}
function truncate(content: string, maxLength: number): string {
return content.length > maxLength ? `${content.slice(0, maxLength)}...` : content;
return content.length > maxLength ? `${content.slice(0, maxLength)}…` : content;
}
export class FrontmatterError extends Error {
@@ -84,7 +84,7 @@ export async function generateSessionTitle(
// Truncate message if too long
const truncatedMessage =
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}…` : firstMessage;
const userMessage = `<user-message>\n${truncatedMessage}\n</user-message>`;
for (const model of candidates) {
@@ -306,7 +306,7 @@ export async function ensureTool(tool: ToolName, silentOrOptions?: EnsureToolOpt
if (!silent) {
logger.debug(`${pythonConfig.name} not found. Installing via uv/pip...`);
}
notify?.(`Installing ${pythonConfig.name}...`);
notify?.(`Installing ${pythonConfig.name}…`);
const success = await installPythonPackage(pythonConfig.package, signal);
if (success) {
// Re-check for the command after installation
@@ -331,7 +331,7 @@ export async function ensureTool(tool: ToolName, silentOrOptions?: EnsureToolOpt
if (!silent) {
logger.debug(`${config.name} not found. Downloading...`);
}
notify?.(`Downloading ${config.name}...`);
notify?.(`Downloading ${config.name}…`);
try {
const path = await downloadTool(tool, signal);
@@ -145,8 +145,11 @@ export const handleOsv: SpecialHandler = async (
if (affected.versions?.length) {
const versions =
affected.versions.length > 10
? `${affected.versions.slice(0, 10).join(", ")}... (${affected.versions.length} total)`
? `${affected.versions.slice(0, 10).join(", ")}… (${affected.versions.length} total)`
: affected.versions.join(", ");
affected.versions.length > 10
? `${affected.versions.slice(0, 10).join(", ")}… (${affected.versions.length} total)`
: affected.versions.join(", ");
md += `- **Versions:** ${versions}\n`;
}
@@ -287,7 +287,7 @@ export const handleYouTube: SpecialHandler = async (
if (description) {
// Truncate long descriptions
const descPreview = description.length > 1000 ? `${description.slice(0, 1000)}...` : description;
const descPreview = description.length > 1000 ? `${description.slice(0, 1000)}…` : description;
md += `---\n\n## Description\n\n${descPreview}\n\n`;
}
@@ -79,7 +79,7 @@ function formatProviderError(error: unknown, provider: SearchProvider): string {
/** Truncate text for tool output */
function truncateText(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
return `${text.slice(0, Math.max(0, maxLen - 1))}…`;
}
function formatCount(label: string, count: number): string {
+1 -1
View File
@@ -65,7 +65,7 @@ const cargoArgs = ["build"];
if (!isDev) cargoArgs.push("--release");
if (crossTarget) cargoArgs.push("--target", crossTarget);
console.log(`Building pi-natives for ${targetPlatform}-${targetArch}${isDev ? " (debug)" : ""}...`);
console.log(`Building pi-natives for ${targetPlatform}-${targetArch}${isDev ? " (debug)" : ""}…`);
const buildResult = await $`cargo ${cargoArgs}`.cwd(rustDir).nothrow();
if (buildResult.exitCode !== 0) {
const stderr =
+1 -1
View File
@@ -110,7 +110,7 @@ async function ensureReactSource(reactDir: string): Promise<void> {
throw new Error(`Directory exists but missing packages/: ${reactDir}`);
}
console.log(`Cloning React repository to ${reactDir}...`);
console.log(`Cloning React repository to ${reactDir}…`);
fs.mkdirSync(dirname(reactDir), { recursive: true });
const result = await $`git clone --depth 1 ${REACT_REPO_URL} ${reactDir}`.quiet().nothrow();
if (result.exitCode !== 0) {
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Component } from "../tui";
import { applyBackgroundToLine, padding, visibleWidth } from "../utils";
import { applyBackgroundToLine, padding, truncateToWidth, visibleWidth } from "../utils";
type Cache = {
key: bigint;
@@ -79,7 +79,7 @@ export class Box implements Component {
for (const child of this.children) {
const lines = child.render(contentWidth);
for (const line of lines) {
childLines.push(leftPad + line);
childLines.push(leftPad + truncateToWidth(line, contentWidth));
}
}
+2 -2
View File
@@ -175,7 +175,7 @@ function formatAttempt(attempt: EditAttempt, index: number, options: Options): s
if (options.context && attempt.thinkingContext) {
const truncated =
attempt.thinkingContext.length > 300
? `${attempt.thinkingContext.slice(0, 300)}...`
? `${attempt.thinkingContext.slice(0, 300)}…`
: attempt.thinkingContext;
lines.push(`${colorize("Thinking:", "dim")} ${truncated}`);
}
@@ -187,7 +187,7 @@ function formatAttempt(attempt: EditAttempt, index: number, options: Options): s
lines.push(``);
const resultPreview = attempt.resultText.slice(0, 200);
const truncatedResult = attempt.resultText.length > 200 ? `${resultPreview}...` : resultPreview;
const truncatedResult = attempt.resultText.length > 200 ? `${resultPreview}…` : resultPreview;
lines.push(`${colorize("Result:", "dim")} ${truncatedResult}`);
lines.push(colorize("-".repeat(80), "dim"));
+2 -2
View File
@@ -208,7 +208,7 @@ async function cmdRelease(version: string): Promise<void> {
console.log(` Version ${version} > ${latestTag}\n`);
// 2. Update package versions
console.log(`Updating package versions to ${version}...`);
console.log(`Updating package versions to ${version}…`);
const pkgJsonPaths = await Array.fromAsync(packageJsonGlob.scan("."));
// Filter out private packages
@@ -233,7 +233,7 @@ async function cmdRelease(version: string): Promise<void> {
console.log();
// 3. Update Rust workspace version
console.log(`Updating Rust workspace version to ${version}...`);
console.log(`Updating Rust workspace version to ${version}…`);
await $`sd '^version = "[^"]+"' ${`version = "${version}"`} Cargo.toml`;
// Verify