Skip to content

Commit f1d944a

Browse files
committed
fix(tui): neutral prompt border and token speed on the model chip
The prompt box no longer tints by permission mode or thinking effort; it uses the neutral border color, and yolo stays visible as a status bar badge. Token speed returns to the model chip, matching the footer composition, instead of sitting mid-ladder in the extras.
1 parent fa436aa commit f1d944a

8 files changed

Lines changed: 71 additions & 27 deletions

File tree

.changeset/tui-signature-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@pythoughts/pythinker-code": minor
33
---
44

5-
Redesign core TUI surfaces: tool cards get state-tinted backgrounds with three new theme tokens, a status bar with a per-session accent color appears between the input box and footer, and the input border reflects yolo and auto permission modes. The working-label shimmer uses a calmer constant-velocity sweep with alternating mission-control highlights.
5+
Redesign core TUI surfaces: tool cards get state-tinted backgrounds with three new theme tokens, a status bar with a per-session accent color appears between the input box and footer, and the prompt box uses a neutral border while permission mode appears in the status bar. The working-label shimmer uses a calmer constant-velocity sweep with alternating mission-control highlights.

apps/pythinker-code/src/tui/components/chrome/status-bar.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { truncateToWidth, visibleWidth, type Component } from '@earendil-works/p
44
import chalk from 'chalk';
55

66
import type { StatusLineConfig } from '#/tui/config';
7-
import type { FooterStatus } from '#/tui/runtime/footer/footer-model';
7+
import {
8+
formatTokenSpeed,
9+
type FooterStatus,
10+
} from '#/tui/runtime/footer/footer-model';
811
import { currentTheme } from '#/tui/theme';
912
import { themeFromHexChannels } from '#/tui/theme/terminal-background';
1013
import { effortColorToken, shortEffortLabel } from '#/tui/utils/thinking-levels';
@@ -20,6 +23,8 @@ export type StatusBarStatus = Pick<
2023
| 'planMode'
2124
| 'fastMode'
2225
| 'dynamicWorkflowMode'
26+
| 'tokenSpeed'
27+
| 'tokenSpeedEstimated'
2328
> & {
2429
readonly extras: readonly string[];
2530
readonly sessionKey: string;
@@ -43,8 +48,16 @@ export class StatusBarComponent implements Component {
4348
shortEffortLabel(status.thinkingLevel),
4449
)}`
4550
: '';
51+
const fastSuffix = status.statusLine.showModes && status.fastMode
52+
? `${currentTheme.fg('textDim', ' · ')}${currentTheme.fg('modeFast', '↯ fast')}`
53+
: '';
54+
const speed = status.statusLine.showTokenSpeed ? formatTokenSpeed(status) : null;
4655
const modelChip = status.statusLine.showModel
47-
? chip(`${currentTheme.fg('text', status.model)}${effortSuffix}`)
56+
? chip(
57+
`${currentTheme.fg('text', status.model)}${effortSuffix}${fastSuffix}${
58+
speed === null ? '' : currentTheme.fg('textDim', ` · ${speed}`)
59+
}`,
60+
)
4861
: undefined;
4962
let modesChip = status.statusLine.showModes ? renderModesChip(status) : undefined;
5063
const extraChips = status.extras.map((extra) =>
@@ -101,7 +114,6 @@ function renderModesChip(status: StatusBarStatus): string | undefined {
101114
if (status.planMode) modes.push(currentTheme.fg('modePlan', 'plan'));
102115
if (status.permissionMode === 'auto') modes.push(currentTheme.fg('modePermission', 'auto'));
103116
if (status.permissionMode === 'yolo') modes.push(currentTheme.fg('error', 'yolo'));
104-
if (status.fastMode) modes.push(currentTheme.fg('modeFast', '↯ fast'));
105117
if (status.dynamicWorkflowMode) modes.push(currentTheme.fg('accent', 'workflow'));
106118
return modes.length === 0 ? undefined : chip(modes.join(' '));
107119
}

apps/pythinker-code/src/tui/constant/rendering.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ export const DYNAMIC_WORKFLOW_RENDERING = {
3030
memberProgressWidth: 8,
3131
/** Least width of the lifecycle STATE column in member rows. */
3232
stateColumnWidth: 6,
33-
/** Cadence for the live aggregate-label shimmer. */
34-
aggregateShimmerFrameMs: BRAILLE_SPINNER_INTERVAL_MS,
3533
/** Half-circle frames for a running row; all rows share one clock. */
3634
progressFrames: ['◐', '◓', '◑', '◒'],
3735
/** Rotation cadence in milliseconds — deliberately slow; this is ambience, not progress. */

apps/pythinker-code/src/tui/pythinker-tui.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { readUpdateInstallState } from '#/cli/update/install-state';
2727
import { detectInstallSource } from '#/cli/update/source';
2828
import type { InstallSource } from '#/cli/update/types';
2929
import { MigrationScreenComponent, type MigrationScreenResult } from '#/migration/index';
30-
import { effortColorToken } from '#/tui/utils/thinking-levels';
3130
import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text';
3231
import {
3332
appendInputHistory,
@@ -2128,19 +2127,9 @@ export class PythinkerTUI {
21282127
const highlighted =
21292128
this.state.appState.planMode || findSlashAutocompleteContext(currentLine, col) !== null;
21302129
this.state.editor.borderHighlighted = highlighted;
2131-
// Reads thinkingLevel at paint time so cycling effort (Shift-Tab/Ctrl-T)
2132-
// recolors the prompt box on the next render without re-wiring the closure.
21332130
this.state.editor.borderColor = (s: string) => {
21342131
if (highlighted) return currentTheme.fg('primary', s);
2135-
if (this.state.appState.permissionMode === 'yolo') {
2136-
return currentTheme.fg('modeAutoAccept', s);
2137-
}
2138-
if (this.state.appState.permissionMode === 'auto') {
2139-
return currentTheme.fg('modePermission', s);
2140-
}
2141-
const level = this.state.appState.thinkingLevel;
2142-
if (level === 'off' || level.trim().length === 0) return currentTheme.fg('border', s);
2143-
return currentTheme.fg(effortColorToken(level), s);
2132+
return currentTheme.fg('border', s);
21442133
};
21452134
this.state.ui.requestRender();
21462135
}

apps/pythinker-code/src/tui/runtime/footer/footer-model.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,6 @@ export function selectStatusBarExtras(
565565
parts.context,
566566
parts.git,
567567
parts.update,
568-
parts.speed,
569568
parts.spend,
570569
parts.elapsed,
571570
parts.goal,
@@ -638,7 +637,9 @@ function formatStatusElapsed(ms: number): string {
638637
return totalMinutes < 60 ? clock : `${String(Math.floor(totalMinutes / 60))}:${clock}`;
639638
}
640639

641-
function formatTokenSpeed(status: FooterStatus): string | null {
640+
export function formatTokenSpeed(
641+
status: Pick<FooterStatus, 'tokenSpeed' | 'tokenSpeedEstimated'>,
642+
): string | null {
642643
const speed = status.tokenSpeed;
643644
if (speed === null || !Number.isFinite(speed) || speed < 0) return null;
644645
return `${status.tokenSpeedEstimated ? '~' : ''}${speed.toFixed(1)} t/s`;

apps/pythinker-code/test/tui/components/status-bar.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ function status(overrides: Partial<StatusBarStatus> = {}): StatusBarStatus {
2323
planMode: true,
2424
fastMode: false,
2525
dynamicWorkflowMode: true,
26+
tokenSpeed: null,
27+
tokenSpeedEstimated: false,
2628
extras: [],
2729
sessionKey: 'session-alpha',
2830
statusLine: DEFAULT_STATUS_LINE_CONFIG,
@@ -145,6 +147,40 @@ describe('StatusBarComponent', () => {
145147
}
146148
});
147149

150+
it('renders token speed at the end of the model chip', () => {
151+
const component = new StatusBarComponent();
152+
component.update(status({
153+
fastMode: true,
154+
tokenSpeed: 75.7,
155+
tokenSpeedEstimated: true,
156+
}));
157+
158+
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
159+
160+
expect(modelChip).toBe('Model Alpha · high · ↯ fast · ~75.7 t/s');
161+
});
162+
163+
it('hides token speed when showTokenSpeed is false', () => {
164+
const component = new StatusBarComponent();
165+
component.update(status({
166+
tokenSpeed: 75.7,
167+
statusLine: { ...DEFAULT_STATUS_LINE_CONFIG, showTokenSpeed: false },
168+
}));
169+
170+
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
171+
172+
expect(modelChip).toBe('Model Alpha · high');
173+
});
174+
175+
it('does not leave a separator when token speed is null', () => {
176+
const component = new StatusBarComponent();
177+
component.update(status({ fastMode: true, tokenSpeed: null }));
178+
179+
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
180+
181+
expect(modelChip).toBe('Model Alpha · high · ↯ fast');
182+
});
183+
148184
it('renders extras in order between modes and cwd', () => {
149185
const component = new StatusBarComponent();
150186
component.update(status({ extras: ['6% · 55.6k/1M', 'main ± [PR#1]'] }));

apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
promptFeedbackInput,
5151
runModelSelector,
5252
} from '#/tui/commands/prompts';
53+
import { currentTheme } from '#/tui/theme';
5354
import type { QueuedMessage } from '#/tui/types';
5455
import type { ImageAttachmentStore } from '#/tui/utils/image-attachment-store';
5556
import { LEGACY_TEST_PATHS, PARITY_CASES } from './parity/feature-matrix';
@@ -5435,7 +5436,7 @@ command = "vim"
54355436
expect(driver.state.appState.thinkingLevel).toBe('off');
54365437
});
54375438

5438-
it('tints the prompt-box border by the current thinking effort', async () => {
5439+
it('keeps the prompt-box border neutral across thinking effort and permission mode', async () => {
54395440
const session = makeSession();
54405441
const { driver } = await makeDriver(session, {
54415442
getConfig: vi.fn(async () => ({
@@ -5459,16 +5460,21 @@ command = "vim"
54595460
chalk.level = 3;
54605461
try {
54615462
const tui = driver as unknown as PythinkerTUI;
5462-
const paintAt = (level: string): string => {
5463-
tui.setAppState({ thinkingLevel: level });
5463+
const paintAt = (thinkingLevel: string): string => {
5464+
tui.setAppState({ thinkingLevel });
54645465
return driver.state.editor.borderColor('─');
54655466
};
54665467
const offPaint = paintAt('off');
5468+
tui.setAppState({ permissionMode: 'yolo' });
5469+
expect(driver.state.editor.borderColor('─')).toBe(offPaint);
5470+
5471+
tui.setAppState({ permissionMode: 'manual' });
54675472
const perLevel = ['low', 'medium', 'high'].map(paintAt);
5468-
// Effort levels tint the border away from the default, each with the
5469-
// theme's own gradient stop for that level.
5470-
for (const painted of perLevel) expect(painted).not.toBe(offPaint);
5471-
expect(new Set(perLevel).size).toBe(perLevel.length);
5473+
for (const painted of perLevel) expect(painted).toBe(offPaint);
5474+
5475+
tui.setAppState({ planMode: true });
5476+
expect(driver.state.editor.borderColor('─')).toBe(currentTheme.fg('primary', '─'));
5477+
expect(driver.state.editor.borderColor('─')).not.toBe(offPaint);
54725478
} finally {
54735479
chalk.level = previousLevel;
54745480
}

apps/pythinker-code/test/tui/runtime/footer-model.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ describe('footer model', () => {
309309
contextUsage: 0.05,
310310
dynamicWorkflowMode: true,
311311
git: workflowStatus().git,
312+
tokenSpeed: 75.7,
313+
tokenSpeedEstimated: true,
312314
}),
313315
[
314316
{

0 commit comments

Comments
 (0)