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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user