diff --git a/.changeset/stop-cron-after-close.md b/.changeset/stop-cron-after-close.md
new file mode 100644
index 000000000..c488f588a
--- /dev/null
+++ b/.changeset/stop-cron-after-close.md
@@ -0,0 +1,5 @@
+---
+"@pymodel/pythinker-code": patch
+---
+
+Prevent cron ticks from continuing after an agent shuts down.
diff --git a/.changeset/use-unicode-ellipsis.md b/.changeset/use-unicode-ellipsis.md
new file mode 100644
index 000000000..45d628ddb
--- /dev/null
+++ b/.changeset/use-unicode-ellipsis.md
@@ -0,0 +1,5 @@
+---
+"@pymodel/pythinker-code": patch
+---
+
+Use the Unicode ellipsis in terminal status and truncation text.
diff --git a/apps/pythinker-code/src/tui/commands/plugins.ts b/apps/pythinker-code/src/tui/commands/plugins.ts
index 2c25a8f06..6638a056f 100644
--- a/apps/pythinker-code/src/tui/commands/plugins.ts
+++ b/apps/pythinker-code/src/tui/commands/plugins.ts
@@ -629,7 +629,7 @@ async function installFromPanel(
if (official) {
panel.setInstalling(truncateForStatus(label));
} else {
- host.showStatus(`Installing or updating ${label} from marketplace...`);
+ host.showStatus(`Installing or updating ${label} from marketplace…`);
}
host.state.ui.requestRender();
try {
diff --git a/apps/pythinker-code/src/tui/components/dialogs/compaction.ts b/apps/pythinker-code/src/tui/components/dialogs/compaction.ts
index c0fa67ca2..a07a66dcf 100644
--- a/apps/pythinker-code/src/tui/components/dialogs/compaction.ts
+++ b/apps/pythinker-code/src/tui/components/dialogs/compaction.ts
@@ -165,7 +165,7 @@ export class CompactionComponent extends Container {
return `${bullet}${label}`;
}
const bullet = this.blinkOn ? currentTheme.fg('text', STATUS_BULLET) : ' ';
- const label = currentTheme.boldFg('primary', 'Compacting context...');
+ const label = currentTheme.boldFg('primary', 'Compacting context…');
const tip = this.tip ? currentTheme.fg('textDim', ` · Tip: ${this.tip}`) : '';
return `${bullet}${label}${tip}`;
}
diff --git a/apps/pythinker-code/src/tui/components/dialogs/question-dialog.ts b/apps/pythinker-code/src/tui/components/dialogs/question-dialog.ts
index 1da283ea3..f1d426077 100644
--- a/apps/pythinker-code/src/tui/components/dialogs/question-dialog.ts
+++ b/apps/pythinker-code/src/tui/components/dialogs/question-dialog.ts
@@ -469,7 +469,7 @@ export class QuestionDialogComponent extends Container implements Focusable {
appendWrapped(lines, ' ', ' ', bodyLine, renderWidth, dim);
}
if (bodyLines.length > visibleBodyLines.length) {
- lines.push(dim(` ... ${String(bodyLines.length - visibleBodyLines.length)} more lines`));
+ lines.push(dim(` … ${String(bodyLines.length - visibleBodyLines.length)} more lines`));
}
}
diff --git a/apps/pythinker-code/src/tui/components/dialogs/session-picker.ts b/apps/pythinker-code/src/tui/components/dialogs/session-picker.ts
index ce39f75e0..ae44ac6ae 100644
--- a/apps/pythinker-code/src/tui/components/dialogs/session-picker.ts
+++ b/apps/pythinker-code/src/tui/components/dialogs/session-picker.ts
@@ -264,7 +264,7 @@ export class SessionPickerComponent extends Container implements Focusable {
if (this.loading) {
lines.push(currentTheme.boldFg('primary', truncateToWidth(title, width, ELLIPSIS)));
lines.push(
- currentTheme.fg('textMuted', truncateToWidth('Loading sessions...', width, ELLIPSIS)),
+ currentTheme.fg('textMuted', truncateToWidth('Loading sessions…', width, ELLIPSIS)),
);
lines.push(currentTheme.fg('primary', '─'.repeat(width)));
return lines;
diff --git a/apps/pythinker-code/src/tui/components/messages/agent-dynamic-workflow-progress.ts b/apps/pythinker-code/src/tui/components/messages/agent-dynamic-workflow-progress.ts
index 047274b9e..f31f73488 100644
--- a/apps/pythinker-code/src/tui/components/messages/agent-dynamic-workflow-progress.ts
+++ b/apps/pythinker-code/src/tui/components/messages/agent-dynamic-workflow-progress.ts
@@ -33,15 +33,15 @@ const AGENT_DYNAMIC_WORKFLOW_LEFT_INDENT = ' ';
const AGENT_DYNAMIC_WORKFLOW_RIGHT_GAP = 1;
const AGENT_DYNAMIC_WORKFLOW_NON_GRID_LINES = 6;
const COMPACT_TERMINAL_MARK_WIDTH = 1;
-const ORCHESTRATING_LABEL = 'Orchestrating...';
-const PROMPTING_LABEL = 'Prompting...';
-const WORKING_LABEL = 'Working...';
+const ORCHESTRATING_LABEL = 'Orchestrating…';
+const PROMPTING_LABEL = 'Prompting…';
+const WORKING_LABEL = 'Working…';
const COMPLETED_LABEL = 'Completed.';
const FAILED_LABEL = 'Failed.';
const ABORTED_LABEL = 'Aborted.';
const CANCELLED_LABEL = 'Cancelled.';
-const QUEUED_LABEL = 'Queued...';
-const SUSPENDED_LABEL = 'Rate limited...';
+const QUEUED_LABEL = 'Queued…';
+const SUSPENDED_LABEL = 'Rate limited…';
const RESUMED_ITEM_LABEL = '(resumed)';
const CANCELLED_LABEL_DARKEN_FACTOR = 0.72;
const AGENT_ID_TOKENS = [
diff --git a/apps/pythinker-code/src/tui/components/messages/shell-run.ts b/apps/pythinker-code/src/tui/components/messages/shell-run.ts
index 3cb58db91..3821179f5 100644
--- a/apps/pythinker-code/src/tui/components/messages/shell-run.ts
+++ b/apps/pythinker-code/src/tui/components/messages/shell-run.ts
@@ -17,7 +17,7 @@ const TIMER_INTERVAL_MS = 1000;
const MAX_COMBINED_CHARS = 256 * 1024;
const KEEP_COMBINED_CHARS = 64 * 1024;
-const TRUNCATED_RUNNING_NOTICE = '... (output truncated)';
+const TRUNCATED_RUNNING_NOTICE = '… (output truncated)';
/**
* Live view for a user-initiated `!` shell command. Two phases:
diff --git a/apps/pythinker-code/src/tui/components/messages/thinking.ts b/apps/pythinker-code/src/tui/components/messages/thinking.ts
index c3a10fd6b..597204b17 100644
--- a/apps/pythinker-code/src/tui/components/messages/thinking.ts
+++ b/apps/pythinker-code/src/tui/components/messages/thinking.ts
@@ -123,7 +123,7 @@ export class ThinkingComponent implements Component {
rendered = [];
} else {
const contentLines = this.renderContent(width);
- const hint = `... (${String(contentLines.length)} more lines, ctrl+o to expand)`;
+ const hint = `… (${String(contentLines.length)} more lines, ctrl+o to expand)`;
const prefix = this.showMarker ? currentTheme.fg('textDim', STATUS_BULLET) : MESSAGE_INDENT;
const styledHint = currentTheme.fg(
'textDim',
diff --git a/apps/pythinker-code/src/tui/components/messages/tool-call.ts b/apps/pythinker-code/src/tui/components/messages/tool-call.ts
index 419bf63a7..bf1da147f 100644
--- a/apps/pythinker-code/src/tui/components/messages/tool-call.ts
+++ b/apps/pythinker-code/src/tui/components/messages/tool-call.ts
@@ -381,7 +381,7 @@ function truncateArgValue(key: string, value: string): string {
// still tell which file is being touched.
return '…' + value.slice(value.length - (MAX_ARG_LENGTH - 1));
}
- return value.slice(0, MAX_ARG_LENGTH - 3) + '...';
+ return value.slice(0, MAX_ARG_LENGTH - 1) + '…';
}
function makeWorkspaceRelativePath(filePath: string, workspaceDir: string | undefined): string {
@@ -813,7 +813,7 @@ export class ToolCallComponent extends Container {
if (this.result !== undefined || text.length === 0) return;
this.liveOutput += text;
if (this.liveOutput.length > MAX_LIVE_OUTPUT_CHARS) {
- this.liveOutput = `[...truncated]\n${this.liveOutput.slice(
+ this.liveOutput = `[…truncated]\n${this.liveOutput.slice(
this.liveOutput.length - MAX_LIVE_OUTPUT_CHARS,
)}`;
}
@@ -1422,7 +1422,7 @@ export class ToolCallComponent extends Container {
const existingOutput = options?.replace === true ? '' : (activity?.output ?? '');
let output = existingOutput + text;
if (output.length > MAX_LIVE_OUTPUT_CHARS) {
- output = `[...truncated]\n${output.slice(output.length - MAX_LIVE_OUTPUT_CHARS)}`;
+ output = `[…truncated]\n${output.slice(output.length - MAX_LIVE_OUTPUT_CHARS)}`;
}
this.upsertSubToolActivity(id, name, args, activity?.phase ?? 'ongoing', output);
this.rebuildContent();
@@ -1673,7 +1673,7 @@ export class ToolCallComponent extends Container {
const suffix = this.hiddenSubCallCount > 1 ? 's' : '';
this.addChild(
new Text(
- currentTheme.italic(currentTheme.dim(` ${String(this.hiddenSubCallCount)} more tool call${suffix} ...`)),
+ currentTheme.italic(currentTheme.dim(` ${String(this.hiddenSubCallCount)} more tool call${suffix} …`)),
0,
0,
),
@@ -2043,7 +2043,7 @@ export class ToolCallComponent extends Container {
this.addChild(
new Text(
currentTheme.dim(
- `... (${String(remaining)} more lines, ${String(allLines.length)} total, ctrl+o to expand)`,
+ `… (${String(remaining)} more lines, ${String(allLines.length)} total, ctrl+o to expand)`,
),
2,
0,
@@ -2130,7 +2130,7 @@ export class ToolCallComponent extends Container {
const elapsedSeconds =
startedAtMs === undefined ? 0 : Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000));
const target = filePath.length > 0 ? ` for ${filePath}` : '';
- const progress = `Preparing changes${target}... ${formatByteSize(bytes)} · ${formatElapsed(
+ const progress = `Preparing changes${target}… ${formatByteSize(bytes)} · ${formatElapsed(
elapsedSeconds,
)} elapsed`;
this.addChild(new Text(currentTheme.dim(progress), 2, 0));
diff --git a/apps/pythinker-code/src/tui/components/messages/tool-renderers/truncated.ts b/apps/pythinker-code/src/tui/components/messages/tool-renderers/truncated.ts
index c8bfb7588..db8f4d654 100644
--- a/apps/pythinker-code/src/tui/components/messages/tool-renderers/truncated.ts
+++ b/apps/pythinker-code/src/tui/components/messages/tool-renderers/truncated.ts
@@ -87,15 +87,15 @@ export class TruncatedOutputComponent implements Component {
if (this.tail) {
const shown = contentLines.slice(contentLines.length - this.maxLines);
return [
- this.renderHint(width, `... (${String(remaining)} earlier lines)`),
+ this.renderHint(width, `… (${String(remaining)} earlier lines)`),
...shown,
];
}
const shown = contentLines.slice(0, this.maxLines);
const hint = this.expandHint
- ? `... (${String(remaining)} more lines, ctrl+o to expand)`
- : `... (${String(remaining)} more lines)`;
+ ? `… (${String(remaining)} more lines, ctrl+o to expand)`
+ : `… (${String(remaining)} more lines)`;
return [...shown, this.renderHint(width, hint)];
}
}
diff --git a/apps/pythinker-code/src/tui/components/panes/btw-panel.ts b/apps/pythinker-code/src/tui/components/panes/btw-panel.ts
index f55713a0c..70de37037 100644
--- a/apps/pythinker-code/src/tui/components/panes/btw-panel.ts
+++ b/apps/pythinker-code/src/tui/components/panes/btw-panel.ts
@@ -145,7 +145,7 @@ export class BtwPanelComponent implements Component {
lines.push(...this.renderTurn(turn, width));
}
if (this.turns.length === 0) {
- lines.push(chalk.hex(currentTheme.palette.textDim)('Ready for a side question...'));
+ lines.push(chalk.hex(currentTheme.palette.textDim)('Ready for a side question…'));
}
lines.push(...this.renderTransientNotices(width));
return this.fitBodyLines(lines);
@@ -213,7 +213,7 @@ export class BtwPanelComponent implements Component {
: thinkingLines;
lines.push(...visibleThinking);
} else if (turn.error === undefined) {
- lines.push(chalk.hex(currentTheme.palette.textDim)('Waiting for answer...'));
+ lines.push(chalk.hex(currentTheme.palette.textDim)('Waiting for answer…'));
}
if (turn.error !== undefined) {
const error = chalk.hex(currentTheme.palette.error)(turn.error);
diff --git a/apps/pythinker-code/src/tui/utils/background-agent-status.ts b/apps/pythinker-code/src/tui/utils/background-agent-status.ts
index aa740fc6c..c2322e633 100644
--- a/apps/pythinker-code/src/tui/utils/background-agent-status.ts
+++ b/apps/pythinker-code/src/tui/utils/background-agent-status.ts
@@ -11,7 +11,7 @@ function normalizeBackgroundField(value: string | undefined): string | undefined
const collapsed = value.trim().replaceAll(/\s+/g, ' ');
if (collapsed.length === 0) return undefined;
if (collapsed.length <= MAX_BACKGROUND_FIELD_LENGTH) return collapsed;
- return `${collapsed.slice(0, MAX_BACKGROUND_FIELD_LENGTH - 3)}...`;
+ return `${collapsed.slice(0, MAX_BACKGROUND_FIELD_LENGTH - 1)}…`;
}
export function formatBackgroundAgentTranscript(
diff --git a/apps/pythinker-code/src/tui/utils/background-task-status.ts b/apps/pythinker-code/src/tui/utils/background-task-status.ts
index 20260b47f..422040761 100644
--- a/apps/pythinker-code/src/tui/utils/background-task-status.ts
+++ b/apps/pythinker-code/src/tui/utils/background-task-status.ts
@@ -20,7 +20,7 @@ function truncate(value: string | undefined): string | undefined {
const collapsed = value.trim().replaceAll(/\s+/g, ' ');
if (collapsed.length === 0) return undefined;
if (collapsed.length <= MAX_DETAIL_LENGTH) return collapsed;
- return `${collapsed.slice(0, MAX_DETAIL_LENGTH - 3)}...`;
+ return `${collapsed.slice(0, MAX_DETAIL_LENGTH - 1)}…`;
}
export type BackgroundTaskTranscriptPhase = 'started' | 'updated' | 'terminal';
diff --git a/apps/pythinker-code/test/tui/activity-pane.test.ts b/apps/pythinker-code/test/tui/activity-pane.test.ts
index d11f17a9f..997a488ee 100644
--- a/apps/pythinker-code/test/tui/activity-pane.test.ts
+++ b/apps/pythinker-code/test/tui/activity-pane.test.ts
@@ -181,7 +181,7 @@ describe('updateActivityPane terminal progress', () => {
expect(state.activitySpinner).not.toBeNull();
expect(state.activityContainer.children).toHaveLength(0);
const rendered = strip(progress.render(80).join('\n'));
- expect(rendered).toContain('Working...');
+ expect(rendered).toContain('Working…');
expect(BRAILLE_SPINNER_FRAMES.some((frame) => rendered.includes(frame))).toBe(true);
state.activitySpinner?.instance.stop();
@@ -212,8 +212,8 @@ describe('updateActivityPane terminal progress', () => {
expect(state.activitySpinner).not.toBeNull();
expect(state.activityContainer.children).toHaveLength(1);
const output = strip(progress.render(80).join('\n'));
- expect(output).toContain(' Working...');
- expect(output).not.toContain('⣷ Working...');
+ expect(output).toContain(' Working…');
+ expect(output).not.toContain('⣷ Working…');
state.activitySpinner?.instance.stop();
driver.sessionEventHandler.clearAgentDynamicWorkflowProgress();
diff --git a/apps/pythinker-code/test/tui/components/dialogs/compaction.test.ts b/apps/pythinker-code/test/tui/components/dialogs/compaction.test.ts
index 4f415bc32..eb213cfa2 100644
--- a/apps/pythinker-code/test/tui/components/dialogs/compaction.test.ts
+++ b/apps/pythinker-code/test/tui/components/dialogs/compaction.test.ts
@@ -20,7 +20,7 @@ describe('CompactionComponent', () => {
const lines = component.render(120).map(strip);
const text = lines.join('\n');
- expect(text).toContain('Compacting context...');
+ expect(text).toContain('Compacting context…');
expect(text).toContain(' keep the recent files only');
} finally {
component.dispose();
@@ -34,7 +34,7 @@ describe('CompactionComponent', () => {
const lines = component.render(120).map(strip);
const text = lines.join('\n');
- expect(text).toContain('Compacting context... · Tip: ctrl+s: steer mid-turn');
+ expect(text).toContain('Compacting context… · Tip: ctrl+s: steer mid-turn');
} finally {
component.dispose();
}
@@ -65,7 +65,7 @@ describe('CompactionComponent', () => {
const text = lines.join('\n');
expect(text).toContain('Compaction cancelled');
- expect(text).not.toContain('Compacting context...');
+ expect(text).not.toContain('Compacting context…');
} finally {
component.dispose();
}
@@ -157,7 +157,7 @@ describe('CompactionComponent', () => {
try {
const headerOf = (): string => {
- const line = component.render(120).find((l) => strip(l).includes('Compacting context...'));
+ const line = component.render(120).find((l) => strip(l).includes('Compacting context…'));
if (line === undefined) throw new Error('header line not found');
return line;
};
diff --git a/apps/pythinker-code/test/tui/components/messages/agent-dynamic-workflow-progress.test.ts b/apps/pythinker-code/test/tui/components/messages/agent-dynamic-workflow-progress.test.ts
index dfba906d1..29e014eb3 100644
--- a/apps/pythinker-code/test/tui/components/messages/agent-dynamic-workflow-progress.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/agent-dynamic-workflow-progress.test.ts
@@ -163,7 +163,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
expect(output).toContain('Agent DynamicWorkflow');
expect(output).toContain('Review changed files');
- expect(output).toContain('Orchestrating...');
+ expect(output).toContain('Orchestrating…');
expect(output).not.toContain('01');
});
@@ -229,7 +229,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
expect(strip(rendered)).toContain('Agent DynamicWorkflow');
expect(strip(rendered)).toContain('001 [');
- expect(strip(rendered)).toContain('Working...');
+ expect(strip(rendered)).toContain('Working…');
expect(rendered).toMatch(/\u001B\[38;2;\d+;\d+;\d+m━/);
expect(memberColor('001')).toBeDefined();
expect(memberColor('002')).toBeDefined();
@@ -273,8 +273,8 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
const output = renderText(component);
- expect(output).toContain('001 Queued...');
- expect(output).toContain('002 Queued...');
+ expect(output).toContain('001 Queued…');
+ expect(output).toContain('002 Queued…');
expect(output).not.toContain('001 [');
expect(output).not.toContain('002 [');
expect(output).not.toContain('agents=2');
@@ -286,11 +286,11 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
registerSubagents(component, 3);
const lines = renderLines(component, 97);
- const queuedLine = lines.find((line) => line.includes('001 Queued...'));
+ const queuedLine = lines.find((line) => line.includes('001 Queued…'));
expect(queuedLine).toBeDefined();
- expect(queuedLine).toContain('002 Queued...');
- expect(queuedLine).toContain('003 Queued...');
+ expect(queuedLine).toContain('002 Queued…');
+ expect(queuedLine).toContain('003 Queued…');
});
it('omits subagent text when the compact grid is needed to fit available height', () => {
@@ -388,7 +388,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
let output = renderText(component);
expect(output).toContain('001 [');
expect(output).toContain('Running');
- expect(output).toContain('002 Queued...');
+ expect(output).toContain('002 Queued…');
expect(output).not.toContain('002 [');
component.markCompleted('agent-1');
@@ -437,8 +437,8 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
});
let output = renderText(component);
- expect(output).toContain('Rate limited...');
- expect(output).not.toContain('Queued...');
+ expect(output).toContain('Rate limited…');
+ expect(output).not.toContain('Queued…');
expect(output).not.toContain('Provider rate limit');
expect(output).not.toContain('Failed');
@@ -446,7 +446,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
output = renderText(component);
expect(output).toContain('Running');
- expect(output).not.toContain('Rate limited...');
+ expect(output).not.toContain('Rate limited…');
});
it('renders rate-limited subagents as cancelled when cancelled', () => {
@@ -465,7 +465,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
expect(cellLine).toBeDefined();
expect(cellLine).toContain('⊘ Cancelled.');
- expect(cellLine).not.toContain('Rate limited...');
+ expect(cellLine).not.toContain('Rate limited…');
});
it('renders failure details from AgentDynamicWorkflow result output', () => {
@@ -630,7 +630,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
});
const promptLine = renderLines(prompting, 80)
- .find((line) => line.includes('Prompting...'));
+ .find((line) => line.includes('Prompting…'));
expect(promptLine).toBeDefined();
const working = createComponent();
@@ -638,15 +638,15 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
startSubagents(working, 1);
const workingLine = renderLines(working, 80)
- .find((line) => line.includes('Working...'));
+ .find((line) => line.includes('Working…'));
expect(workingLine).toBeDefined();
const promptTextIndex = promptLine?.indexOf('Review the changed') ?? -1;
const progressBarIndex = workingLine?.indexOf('━') ?? -1;
expect(promptTextIndex).toBeGreaterThan(0);
expect(progressBarIndex).toBeGreaterThan(0);
- expect(promptTextIndex).toBe(visibleWidth(' Prompting... '));
- expect(progressBarIndex).toBe(visibleWidth(' Working... '));
+ expect(promptTextIndex).toBe(visibleWidth(' Prompting… '));
+ expect(progressBarIndex).toBe(visibleWidth(' Working… '));
});
it('renders the activity spinner before the total status line', () => {
@@ -657,10 +657,10 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
component.setActivitySpinnerText(() => '⣷');
const statusLine = renderLines(component, 80)
- .find((line) => line.includes('Working...'));
+ .find((line) => line.includes('Working…'));
expect(statusLine).toBeDefined();
- expect(statusLine?.startsWith(' ⣷ Working...')).toBe(true);
+ expect(statusLine?.startsWith(' ⣷ Working…')).toBe(true);
});
it('keeps a two-cell placeholder after the AgentDynamicWorkflow tool call ends', () => {
@@ -673,10 +673,10 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
component.setActivitySpinnerText(() => '⣯');
const statusLine = renderLines(component, 80)
- .find((line) => line.includes('Working...'));
+ .find((line) => line.includes('Working…'));
expect(statusLine).toBeDefined();
- expect(statusLine?.startsWith(' Working...')).toBe(true);
+ expect(statusLine?.startsWith(' Working…')).toBe(true);
expect(statusLine).not.toContain('⣷');
expect(statusLine).not.toContain('⣯');
});
@@ -719,7 +719,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
});
const promptLine = renderLines(prompting, 50)
- .find((line) => line.includes('Prompting...'));
+ .find((line) => line.includes('Prompting…'));
expect(promptLine).toBeDefined();
expect(visibleWidth(promptLine ?? '')).toBeLessThan(50);
@@ -843,7 +843,7 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
registerSubagents(component, 1);
let output = renderText(component);
- expect(output).toContain('001 Queued...');
+ expect(output).toContain('001 Queued…');
expect(output).not.toContain('001 [');
expect(output).not.toContain('002');
@@ -852,15 +852,15 @@ describe('AgentDynamicWorkflowProgressComponent', () => {
description: `${DEFAULT_DESCRIPTION} #2 (coder)`,
});
output = renderText(component);
- expect(output).toContain('001 Queued...');
- expect(output).toContain('002 Queued...');
+ expect(output).toContain('001 Queued…');
+ expect(output).toContain('002 Queued…');
expect(output).not.toContain('001 [');
expect(output).not.toContain('002 [');
component.markInputComplete();
output = renderText(component);
- expect(output).toContain('001 Queued...');
- expect(output).toContain('002 Queued...');
+ expect(output).toContain('001 Queued…');
+ expect(output).toContain('002 Queued…');
expect(output).not.toContain('001 [');
});
diff --git a/apps/pythinker-code/test/tui/components/messages/shell-execution.test.ts b/apps/pythinker-code/test/tui/components/messages/shell-execution.test.ts
index bb501c05b..132737c86 100644
--- a/apps/pythinker-code/test/tui/components/messages/shell-execution.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/shell-execution.test.ts
@@ -35,7 +35,7 @@ describe('ShellExecutionComponent', () => {
expect(collapsedOutput).toContain('line1');
expect(collapsedOutput).toContain('line3');
expect(collapsedOutput).not.toContain('line4');
- expect(collapsedOutput).toContain('... (2 more lines, ctrl+o to expand)');
+ expect(collapsedOutput).toContain('… (2 more lines, ctrl+o to expand)');
const expanded = new ShellExecutionComponent({
result: {
@@ -76,7 +76,7 @@ describe('ShellExecutionComponent', () => {
const output = component.render(100).map(strip).join('\n');
expect(output).toContain('hello');
- expect(output).not.toContain('... (2 more lines');
+ expect(output).not.toContain('… (2 more lines');
});
it('preserves internal empty lines while trimming only trailing ones', () => {
@@ -91,7 +91,7 @@ describe('ShellExecutionComponent', () => {
const output = component.render(100).map(strip).join('\n');
expect(output).toContain('a');
expect(output).toContain('b');
- expect(output).not.toContain('... (2 more lines');
+ expect(output).not.toContain('… (2 more lines');
});
it('truncates long single-line output by wrapped visual lines', () => {
@@ -106,7 +106,7 @@ describe('ShellExecutionComponent', () => {
const out = strip(component.render(20).join('\n'));
expect(out).toContain('x');
expect(out).not.toContain('x'.repeat(500));
- expect(out).toContain('... (');
+ expect(out).toContain('… (');
});
describe('shellExecutionResultRenderer', () => {
diff --git a/apps/pythinker-code/test/tui/components/messages/shell-run.test.ts b/apps/pythinker-code/test/tui/components/messages/shell-run.test.ts
index be89c6f03..96a4bc110 100644
--- a/apps/pythinker-code/test/tui/components/messages/shell-run.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/shell-run.test.ts
@@ -90,7 +90,7 @@ describe('ShellRunComponent finished collapse', () => {
const c = create();
c.finish(rows(30), '', false);
const rendered = stripTheme(c.render(80).join('\n'));
- expect(rendered).toContain('... (20 more lines, ctrl+o to expand)');
+ expect(rendered).toContain('… (20 more lines, ctrl+o to expand)');
expect(rendered).toContain('row-01');
expect(rendered).toContain('row-10');
expect(rendered).not.toContain('row-11');
@@ -116,7 +116,7 @@ describe('ShellRunComponent finished collapse', () => {
c.setExpanded(false);
const collapsed = stripTheme(c.render(80).join('\n'));
- expect(collapsed).toContain('... (20 more lines, ctrl+o to expand)');
+ expect(collapsed).toContain('… (20 more lines, ctrl+o to expand)');
expect(collapsed).not.toContain('row-11');
});
@@ -153,7 +153,7 @@ describe('ShellRunComponent finished collapse', () => {
c.append('x'.repeat(300 * 1024));
c.setExpanded(true);
const rendered = stripTheme(c.render(80).join('\n'));
- expect(rendered).toContain('... (output truncated)');
+ expect(rendered).toContain('… (output truncated)');
});
it('keeps the backgrounded view when toggled', () => {
@@ -168,7 +168,7 @@ describe('ShellRunComponent finished collapse', () => {
const c = create();
c.finish(rows(30), 'boom', true);
const collapsed = stripTheme(c.render(80).join('\n'));
- expect(collapsed).toContain('... (21 more lines, ctrl+o to expand)');
+ expect(collapsed).toContain('… (21 more lines, ctrl+o to expand)');
c.setExpanded(true);
const expanded = stripTheme(c.render(80).join('\n'));
diff --git a/apps/pythinker-code/test/tui/components/messages/tool-call.test.ts b/apps/pythinker-code/test/tui/components/messages/tool-call.test.ts
index b6aa53174..9ea1d79be 100644
--- a/apps/pythinker-code/test/tui/components/messages/tool-call.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/tool-call.test.ts
@@ -225,7 +225,7 @@ describe('ToolCallComponent', () => {
expect(collapsed).toContain('line2');
expect(collapsed).toContain('line3');
expect(collapsed).not.toContain('line4');
- expect(collapsed).toContain('... (2 more lines, ctrl+o to expand)');
+ expect(collapsed).toContain('… (2 more lines, ctrl+o to expand)');
component.setExpanded(true);
@@ -254,6 +254,24 @@ describe('ToolCallComponent', () => {
expect(out).toContain('line2');
});
+ it('uses a Unicode ellipsis when truncating live Bash output', () => {
+ const component = new ToolCallComponent(
+ {
+ id: 'call_shell_live_truncated',
+ name: 'Bash',
+ args: { command: 'printf output' },
+ },
+ undefined,
+ );
+
+ component.setExpanded(true);
+ component.appendLiveOutput('x'.repeat(50_001));
+
+ const out = strip(component.render(1000).join('\n'));
+ expect(out).toContain('[…truncated]');
+ expect(out).not.toContain('[...truncated]');
+ });
+
it('clears live Bash output when the final result arrives', () => {
const component = new ToolCallComponent(
{
@@ -1751,7 +1769,7 @@ describe('ToolCallComponent', () => {
const out = strip(component.render(100).join('\n'));
expect(out).toContain('Using Edit');
expect(out).toContain('foo.ts');
- expect(out).toContain('Preparing changes for foo.ts...');
+ expect(out).toContain('Preparing changes for foo.ts…');
expect(out).toContain('4s elapsed');
expect(out).toMatch(/\d+(?:\.\d+)? (?:B|KB|MB)/);
expect(out).not.toContain('old20');
diff --git a/apps/pythinker-code/test/tui/components/messages/tool-renderers/registry.test.ts b/apps/pythinker-code/test/tui/components/messages/tool-renderers/registry.test.ts
index 04c3f73a9..9edafb94f 100644
--- a/apps/pythinker-code/test/tui/components/messages/tool-renderers/registry.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/tool-renderers/registry.test.ts
@@ -65,14 +65,14 @@ describe('tool-result registry', () => {
expect(out).toContain('b');
expect(out).toContain('c');
expect(out).not.toContain('\nd');
- expect(out).toContain('... (2 more lines, ctrl+o to expand)');
+ expect(out).toContain('… (2 more lines, ctrl+o to expand)');
});
it('uses truncated renderer for Bash to preserve raw output UX', () => {
const renderer = pickResultRenderer('Bash');
const out = strip(joinRender(renderer(call('Bash'), result('one\ntwo\nthree\nfour'), ctx)));
expect(out).toContain('one');
- expect(out).toContain('... (1 more lines, ctrl+o to expand)');
+ expect(out).toContain('… (1 more lines, ctrl+o to expand)');
});
it('Read renders no body when collapsed (header chip carries the count)', () => {
@@ -248,7 +248,7 @@ describe('tool-result registry', () => {
const out = strip(joinRender(renderer(call('SomethingUnknown'), result(longLine), ctx), 20));
expect(out).toContain('x');
expect(out).not.toContain(longLine);
- expect(out).toContain('... (');
+ expect(out).toContain('… (');
});
const waitForCompletedOutput = [
diff --git a/apps/pythinker-code/test/tui/components/messages/tool-renderers/truncated.test.ts b/apps/pythinker-code/test/tui/components/messages/tool-renderers/truncated.test.ts
index 5f248ecfa..6ed026343 100644
--- a/apps/pythinker-code/test/tui/components/messages/tool-renderers/truncated.test.ts
+++ b/apps/pythinker-code/test/tui/components/messages/tool-renderers/truncated.test.ts
@@ -20,7 +20,7 @@ describe('TruncatedOutputComponent', () => {
const lines = strip(component.render(80).join('\n')).split('\n');
expect(lines[0]?.startsWith(' a')).toBe(true);
expect(lines[1]?.startsWith(' b')).toBe(true);
- expect(lines[2]).toBe(' ... (3 more lines, ctrl+o to expand)');
+ expect(lines[2]).toBe(' … (3 more lines, ctrl+o to expand)');
});
it('defaults to a two-space indent for both content and hint', () => {
@@ -32,7 +32,7 @@ describe('TruncatedOutputComponent', () => {
const lines = strip(component.render(80).join('\n')).split('\n');
expect(lines[0]?.startsWith(' x')).toBe(true);
- expect(lines[1]).toBe(' ... (2 more lines, ctrl+o to expand)');
+ expect(lines[1]).toBe(' … (2 more lines, ctrl+o to expand)');
});
it('omits the ctrl+o promise when expandHint is false', () => {
@@ -45,7 +45,7 @@ describe('TruncatedOutputComponent', () => {
});
const lines = strip(component.render(80).join('\n')).split('\n');
- expect(lines[2]).toBe(' ... (2 more lines)');
+ expect(lines[2]).toBe(' … (2 more lines)');
});
it('renders all lines without a hint when expanded', () => {
diff --git a/apps/pythinker-code/test/tui/message-replay.test.ts b/apps/pythinker-code/test/tui/message-replay.test.ts
index 2d0d2f988..4e22ecdb8 100644
--- a/apps/pythinker-code/test/tui/message-replay.test.ts
+++ b/apps/pythinker-code/test/tui/message-replay.test.ts
@@ -395,7 +395,7 @@ describe('PythinkerTUI resume message replay', () => {
]);
const transcript = stripAnsi(driver.state.transcriptContainer.render(140).join('\n'));
- expect(transcript).toContain('... (20 more lines, ctrl+o to expand)');
+ expect(transcript).toContain('… (20 more lines, ctrl+o to expand)');
expect(transcript).toContain('row-01');
expect(transcript).not.toContain('row-11');
});
diff --git a/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts b/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts
index 41ac26325..bc9476f3e 100644
--- a/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts
+++ b/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts
@@ -3832,7 +3832,7 @@ command = "vim"
driver.handleUserInput('seq 30');
await vi.waitFor(() => {
const transcript = stripSgr(driver.state.transcriptContainer.render(120).join('\n'));
- expect(transcript).toContain('... (20 more lines, ctrl+o to expand)');
+ expect(transcript).toContain('… (20 more lines, ctrl+o to expand)');
});
let transcript = stripSgr(driver.state.transcriptContainer.render(120).join('\n'));
@@ -3846,7 +3846,7 @@ command = "vim"
driver.state.editor.onToggleToolExpand?.();
transcript = stripSgr(driver.state.transcriptContainer.render(120).join('\n'));
- expect(transcript).toContain('... (20 more lines, ctrl+o to expand)');
+ expect(transcript).toContain('… (20 more lines, ctrl+o to expand)');
expect(transcript).not.toContain('row-11');
});
@@ -4289,7 +4289,7 @@ command = "vim"
expect(session.startBtw).toHaveBeenCalledWith();
});
expect(session.prompt).not.toHaveBeenCalled();
- expect(stripSgr(renderBtwPanel(driver))).toContain('Ready for a side question...');
+ expect(stripSgr(renderBtwPanel(driver))).toContain('Ready for a side question…');
driver.handleUserInput('What are you working on right now?');
@@ -4330,7 +4330,7 @@ command = "vim"
await vi.waitFor(() => {
expect(session.startBtw).toHaveBeenCalledWith();
});
- expect(stripSgr(renderBtwPanel(driver))).toContain('Ready for a side question...');
+ expect(stripSgr(renderBtwPanel(driver))).toContain('Ready for a side question…');
driver.handleUserInput('check /skill:review');
@@ -4624,7 +4624,7 @@ command = "vim"
const lines = getMountedBtwPanel(driver).render(80).map(stripSgr);
expect(lines).toHaveLength(3);
expect(lines.join('\n')).toContain('Q: side question');
- expect(lines.join('\n')).toContain('Waiting for answer...');
+ expect(lines.join('\n')).toContain('Waiting for answer…');
});
it('keeps /btw panel height stable when final output is shorter than thinking', async () => {
@@ -5623,7 +5623,7 @@ command = "vim"
transcript = stripSgr(renderTranscript(driver));
expect(transcript).toContain('001 [');
- expect(transcript).toContain('Queued...');
+ expect(transcript).toContain('Queued…');
expect(transcript).not.toContain('Provider rate limit');
expect(transcript).not.toContain('Failed');
@@ -5662,7 +5662,7 @@ command = "vim"
expect(transcript).toContain('001 [');
expect(transcript).toContain('Reviewing src/a.ts');
expect(transcript).not.toContain('Completed');
- expect(transcript).toContain('002 Queued...');
+ expect(transcript).toContain('002 Queued…');
expect(transcript).not.toContain('002 [');
driver.sessionEventHandler.handleEvent(
@@ -5984,7 +5984,7 @@ command = "vim"
const renderDynamicWorkflow = (): string =>
stripSgr(dynamicWorkflowProgress.render(transcriptWidth).join('\n'));
- expect(renderDynamicWorkflow()).toContain('001 Queued...');
+ expect(renderDynamicWorkflow()).toContain('001 Queued…');
driver.sessionEventHandler.handleEvent(
{
@@ -6010,7 +6010,7 @@ command = "vim"
.reduce((sum, child) => sum + child.render(transcriptWidth).length, 0);
expect(rowsAfterDynamicWorkflowInTranscript).toBeGreaterThan(0);
- expect(renderDynamicWorkflow()).toContain('001 Queued...');
+ expect(renderDynamicWorkflow()).toContain('001 Queued…');
const transcript = stripSgr(
driver.state.transcriptContainer.render(terminalColumns).join('\n'),
);
@@ -6083,7 +6083,7 @@ command = "vim"
let transcript = stripSgr(renderTranscript(driver));
expect(transcript).toContain('Agent DynamicWorkflow');
- expect(transcript).toContain('Orchestrating...');
+ expect(transcript).toContain('Orchestrating…');
expect(transcript).not.toContain('01');
driver.sessionEventHandler.handleEvent(
@@ -6120,7 +6120,7 @@ command = "vim"
);
transcript = stripSgr(renderTranscript(driver));
- expect(transcript).toContain('001 Queued...');
+ expect(transcript).toContain('001 Queued…');
expect(transcript).not.toContain('001 [');
expect(transcript).toContain('002 src/b');
@@ -6142,8 +6142,8 @@ command = "vim"
);
transcript = stripSgr(renderTranscript(driver));
- expect(transcript).toContain('001 Queued...');
- expect(transcript).toContain('002 Queued...');
+ expect(transcript).toContain('001 Queued…');
+ expect(transcript).toContain('002 Queued…');
expect(transcript).not.toContain('001 [');
expect(transcript).not.toContain('002 [');
});
diff --git a/apps/vscode/webview-ui/src/components/ActionMenu.tsx b/apps/vscode/webview-ui/src/components/ActionMenu.tsx
index fe0586a66..d1e7ac56a 100644
--- a/apps/vscode/webview-ui/src/components/ActionMenu.tsx
+++ b/apps/vscode/webview-ui/src/components/ActionMenu.tsx
@@ -149,7 +149,7 @@ export function ActionMenu({ className, onAuthAction }: ActionMenuProps) {
danger={isLoggedIn}
>
{loading ?
A browser window should open automatically. Complete the sign-in process there.