feat(setup): switch elapsed-time suffixes to "Xm Ys" past 60s

Adds a `fmtDuration(ms)` helper in `setup/lib/theme.ts` that returns
`47s` under a minute and `1m 34s` from 60s onward, then routes every
elapsed-time spinner suffix in the setup flow through it. Replaces
the inline `Math.round((Date.now() - start) / 1000)` + `(${elapsed}s)`
pattern at every site.

Format is consistent past 60s — `1m 0s` over `1m` — so the live
spinner doesn't change shape at every whole-minute crossing.

Sites updated: setup/auto.ts, setup/lib/{runner,tz-from-claude,
claude-assist}.ts, and setup/channels/{signal,whatsapp,telegram,
discord,slack}.ts. Pre-allocated suffix budgets in `fitToWidth`
calls bumped from `' (999s)'` to `' (99m 59s)'` so long-running
steps don't blow past the reserved width.
This commit is contained in:
exe.dev user
2026-04-29 13:32:27 +00:00
committed by gavrielc
parent bb1b41800c
commit a66cd545d5
10 changed files with 50 additions and 56 deletions

View File

@@ -53,7 +53,7 @@ import { claudeCliAvailable, resolveTimezoneViaClaude } from './lib/tz-from-clau
import * as setupLog from './logs.js'; import * as setupLog from './logs.js';
import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js'; import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js';
import { emit as phEmit } from './lib/diagnostics.js'; import { emit as phEmit } from './lib/diagnostics.js';
import { accentGreen, brandBody, brandBold, brandChip, dimWrap, fitToWidth, note, wrapForGutter } from './lib/theme.js'; import { accentGreen, brandBody, brandBold, brandChip, dimWrap, fitToWidth, fmtDuration, note, wrapForGutter } from './lib/theme.js';
import { isValidTimezone } from '../src/timezone.js'; import { isValidTimezone } from '../src/timezone.js';
const CLI_AGENT_NAME = 'Terminal Agent'; const CLI_AGENT_NAME = 'Terminal Agent';
@@ -579,18 +579,16 @@ async function confirmAssistantResponds(): Promise<PingResult> {
const s = p.spinner(); const s = p.spinner();
const start = Date.now(); const start = Date.now();
const label = 'Waking your assistant…'; const label = 'Waking your assistant…';
s.start(fitToWidth(label, ' (999s)')); s.start(fitToWidth(label, ' (99m 59s)'));
const tick = setInterval(() => { const tick = setInterval(() => {
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
s.message(`${fitToWidth(label, suffix)}${k.dim(suffix)}`); s.message(`${fitToWidth(label, suffix)}${k.dim(suffix)}`);
}, 1000); }, 1000);
const result = await pingCliAgent(); const result = await pingCliAgent();
clearInterval(tick); clearInterval(tick);
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
if (result === 'ok') { if (result === 'ok') {
s.stop(`${k.bold(fitToWidth('Your assistant is ready.', suffix))}${k.dim(suffix)}`); s.stop(`${k.bold(fitToWidth('Your assistant is ready.', suffix))}${k.dim(suffix)}`);
} else { } else {

View File

@@ -31,7 +31,7 @@ import { brightSelect } from '../lib/bright-select.js';
import { confirmThenOpen } from '../lib/browser.js'; import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { accentGreen, brandBody, note } from '../lib/theme.js'; import { accentGreen, brandBody, fmtDuration, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
const DISCORD_API = 'https://discord.com/api/v10'; const DISCORD_API = 'https://discord.com/api/v10';
@@ -289,9 +289,8 @@ async function validateDiscordToken(token: string): Promise<string> {
username?: string; username?: string;
message?: string; message?: string;
}; };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (res.ok && data.username) { if (res.ok && data.username) {
s.stop(`Found your bot: @${data.username}. ${k.dim(`(${elapsedS}s)`)}`); s.stop(`Found your bot: @${data.username}. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
setupLog.step('discord-validate', 'success', Date.now() - start, { setupLog.step('discord-validate', 'success', Date.now() - start, {
BOT_USERNAME: data.username, BOT_USERNAME: data.username,
BOT_ID: data.id ?? '', BOT_ID: data.id ?? '',
@@ -309,8 +308,7 @@ async function validateDiscordToken(token: string): Promise<string> {
'Copy the token again from the Developer Portal and retry setup.', 'Copy the token again from the Developer Portal and retry setup.',
); );
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Discord. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Discord. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('discord-validate', 'failed', Date.now() - start, { setupLog.step('discord-validate', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,
@@ -338,7 +336,6 @@ async function fetchApplicationInfo(token: string): Promise<AppInfo> {
team?: unknown; team?: unknown;
message?: string; message?: string;
}; };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (!res.ok || !data.id || !data.verify_key) { if (!res.ok || !data.id || !data.verify_key) {
const reason = data.message ?? `HTTP ${res.status}`; const reason = data.message ?? `HTTP ${res.status}`;
s.stop(`Couldn't read application info: ${reason}`, 1); s.stop(`Couldn't read application info: ${reason}`, 1);
@@ -351,7 +348,7 @@ async function fetchApplicationInfo(token: string): Promise<AppInfo> {
'Re-run setup. If it keeps failing, check the bot token has the right scopes.', 'Re-run setup. If it keeps failing, check the bot token has the right scopes.',
); );
} }
s.stop(`Got your application details. ${k.dim(`(${elapsedS}s)`)}`); s.stop(`Got your application details. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
// owner is populated for solo applications; team-owned apps return a // owner is populated for solo applications; team-owned apps return a
// team object instead and we'll fall back to a manual user-id prompt. // team object instead and we'll fall back to a manual user-id prompt.
const owner = const owner =
@@ -369,8 +366,7 @@ async function fetchApplicationInfo(token: string): Promise<AppInfo> {
owner, owner,
}; };
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Discord. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Discord. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('discord-app-info', 'failed', Date.now() - start, { setupLog.step('discord-app-info', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,
@@ -479,7 +475,6 @@ async function openDmChannel(token: string, userId: string): Promise<string> {
body: JSON.stringify({ recipient_id: userId }), body: JSON.stringify({ recipient_id: userId }),
}); });
const data = (await res.json()) as { id?: string; message?: string }; const data = (await res.json()) as { id?: string; message?: string };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (!res.ok || !data.id) { if (!res.ok || !data.id) {
const reason = data.message ?? `HTTP ${res.status}`; const reason = data.message ?? `HTTP ${res.status}`;
s.stop(`Couldn't open a DM channel: ${reason}`, 1); s.stop(`Couldn't open a DM channel: ${reason}`, 1);
@@ -492,14 +487,13 @@ async function openDmChannel(token: string, userId: string): Promise<string> {
'Make sure the bot is in a server you\'re also in, then retry setup.', 'Make sure the bot is in a server you\'re also in, then retry setup.',
); );
} }
s.stop(`DM channel ready. ${k.dim(`(${elapsedS}s)`)}`); s.stop(`DM channel ready. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
setupLog.step('discord-open-dm', 'success', Date.now() - start, { setupLog.step('discord-open-dm', 'success', Date.now() - start, {
DM_CHANNEL_ID: data.id, DM_CHANNEL_ID: data.id,
}); });
return data.id; return data.id;
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Discord. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Discord. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('discord-open-dm', 'failed', Date.now() - start, { setupLog.step('discord-open-dm', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,

View File

@@ -44,7 +44,7 @@ import {
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { accentGreen, note } from '../lib/theme.js'; import { accentGreen, fmtDuration, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
@@ -324,8 +324,7 @@ async function restartService(): Promise<void> {
// Give the adapter a moment to connect to signal-cli before // Give the adapter a moment to connect to signal-cli before
// init-first-agent's welcome DM hits the delivery path. // init-first-agent's welcome DM hits the delivery path.
await new Promise((r) => setTimeout(r, 5000)); await new Promise((r) => setTimeout(r, 5000));
const elapsed = Math.round((Date.now() - start) / 1000); s.stop(`NanoClaw restarted. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
s.stop(`NanoClaw restarted. ${k.dim(`(${elapsed}s)`)}`);
setupLog.step('signal-restart', 'success', Date.now() - start, { setupLog.step('signal-restart', 'success', Date.now() - start, {
PLATFORM: platform, PLATFORM: platform,
}); });

View File

@@ -28,7 +28,7 @@ import * as setupLog from '../logs.js';
import { confirmThenOpen } from '../lib/browser.js'; import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { accentGreen, note, wrapForGutter } from '../lib/theme.js'; import { accentGreen, fmtDuration, note, wrapForGutter } from '../lib/theme.js';
const SLACK_API = 'https://slack.com/api'; const SLACK_API = 'https://slack.com/api';
const SLACK_APPS_URL = 'https://api.slack.com/apps'; const SLACK_APPS_URL = 'https://api.slack.com/apps';
@@ -241,10 +241,9 @@ async function validateSlackToken(token: string): Promise<WorkspaceInfo> {
user_id?: string; user_id?: string;
error?: string; error?: string;
}; };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (data.ok && data.team && data.user) { if (data.ok && data.team && data.user) {
s.stop( s.stop(
`Connected to ${data.team} as @${data.user}. ${k.dim(`(${elapsedS}s)`)}`, `Connected to ${data.team} as @${data.user}. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`,
); );
const info: WorkspaceInfo = { const info: WorkspaceInfo = {
teamName: data.team, teamName: data.team,
@@ -273,8 +272,7 @@ async function validateSlackToken(token: string): Promise<WorkspaceInfo> {
: `Slack said "${reason}". Check the token scopes and workspace install, then retry.`, : `Slack said "${reason}". Check the token scopes and workspace install, then retry.`,
); );
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Slack. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Slack. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('slack-validate', 'failed', Date.now() - start, { setupLog.step('slack-validate', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,
@@ -334,9 +332,8 @@ async function openDmChannel(token: string, userId: string): Promise<string> {
channel?: { id?: string }; channel?: { id?: string };
error?: string; error?: string;
}; };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (data.ok && data.channel?.id) { if (data.ok && data.channel?.id) {
s.stop(`DM channel ready. ${k.dim(`(${elapsedS}s)`)}`); s.stop(`DM channel ready. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
setupLog.step('slack-open-dm', 'success', Date.now() - start, { setupLog.step('slack-open-dm', 'success', Date.now() - start, {
DM_CHANNEL_ID: data.channel.id, DM_CHANNEL_ID: data.channel.id,
}); });
@@ -360,8 +357,7 @@ async function openDmChannel(token: string, userId: string): Promise<string> {
`Slack said "${reason}". Check the member ID and app permissions, then retry.`, `Slack said "${reason}". Check the member ID and app permissions, then retry.`,
); );
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Slack. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Slack. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('slack-open-dm', 'failed', Date.now() - start, { setupLog.step('slack-open-dm', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,

View File

@@ -33,7 +33,7 @@ import {
spawnStep, spawnStep,
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { accentGreen, brandBold, fitToWidth, note } from '../lib/theme.js'; import { accentGreen, brandBold, fitToWidth, fmtDuration, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
@@ -191,10 +191,9 @@ async function validateTelegramToken(token: string): Promise<string> {
result?: { username?: string; id?: number }; result?: { username?: string; id?: number };
description?: string; description?: string;
}; };
const elapsedS = Math.round((Date.now() - start) / 1000);
if (data.ok && data.result?.username) { if (data.ok && data.result?.username) {
const username = data.result.username; const username = data.result.username;
s.stop(`Found your bot: @${username}. ${k.dim(`(${elapsedS}s)`)}`); s.stop(`Found your bot: @${username}. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
setupLog.step('telegram-validate', 'success', Date.now() - start, { setupLog.step('telegram-validate', 'success', Date.now() - start, {
BOT_USERNAME: username, BOT_USERNAME: username,
BOT_ID: data.result.id ?? '', BOT_ID: data.result.id ?? '',
@@ -212,8 +211,7 @@ async function validateTelegramToken(token: string): Promise<string> {
'Copy the token again from @BotFather and try setup once more.', 'Copy the token again from @BotFather and try setup once more.',
); );
} catch (err) { } catch (err) {
const elapsedS = Math.round((Date.now() - start) / 1000); s.stop(`Couldn't reach Telegram. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`, 1);
s.stop(`Couldn't reach Telegram. ${k.dim(`(${elapsedS}s)`)}`, 1);
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
setupLog.step('telegram-validate', 'failed', Date.now() - start, { setupLog.step('telegram-validate', 'failed', Date.now() - start, {
ERROR: message, ERROR: message,

View File

@@ -46,7 +46,7 @@ import {
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { accentGreen, brandBody, brandBold, note } from '../lib/theme.js'; import { accentGreen, brandBody, brandBold, fmtDuration, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json'); const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json');
@@ -379,8 +379,7 @@ async function restartService(): Promise<void> {
// Give the adapter a moment to reconnect before init-first-agent's // Give the adapter a moment to reconnect before init-first-agent's
// welcome DM hits the delivery path. // welcome DM hits the delivery path.
await new Promise((r) => setTimeout(r, 5000)); await new Promise((r) => setTimeout(r, 5000));
const elapsed = Math.round((Date.now() - start) / 1000); s.stop(`NanoClaw restarted. ${k.dim(`(${fmtDuration(Date.now() - start)})`)}`);
s.stop(`NanoClaw restarted. ${k.dim(`(${elapsed}s)`)}`);
setupLog.step('whatsapp-restart', 'success', Date.now() - start, { setupLog.step('whatsapp-restart', 'success', Date.now() - start, {
PLATFORM: platform, PLATFORM: platform,
}); });

View File

@@ -28,7 +28,7 @@ import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
import { ensureAnswer } from './runner.js'; import { ensureAnswer } from './runner.js';
import { brandBody, fitToWidth, note } from './theme.js'; import { brandBody, fitToWidth, fmtDuration, note } from './theme.js';
export interface AssistContext { export interface AssistContext {
stepName: string; stepName: string;
@@ -295,9 +295,8 @@ async function queryClaudeUnderSpinner(
// Move cursor back to the start of the block (WINDOW_SIZE + 1 = header + window). // Move cursor back to the start of the block (WINDOW_SIZE + 1 = header + window).
out.write(`\x1b[${WINDOW_SIZE + 1}A`); out.write(`\x1b[${WINDOW_SIZE + 1}A`);
const elapsed = Math.round((Date.now() - start) / 1000);
const icon = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length]; const icon = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
const suffix = ` (${elapsed}s)`; const suffix = ` (${fmtDuration(Date.now() - start)})`;
const header = fitToWidth('Asking Claude to diagnose…', suffix); const header = fitToWidth('Asking Claude to diagnose…', suffix);
out.write(`\x1b[2K${k.cyan(icon)} ${header}${k.dim(suffix)}\n`); out.write(`\x1b[2K${k.cyan(icon)} ${header}${k.dim(suffix)}\n`);
@@ -355,8 +354,7 @@ async function queryClaudeUnderSpinner(
clearBlock(); clearBlock();
out.write(SHOW_CURSOR); out.write(SHOW_CURSOR);
process.off('exit', restoreCursorOnExit); process.off('exit', restoreCursorOnExit);
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
if (kind === 'ok') { if (kind === 'ok') {
p.log.success(`${brandBody(fitToWidth('Claude replied.', suffix))}${k.dim(suffix)}`); p.log.success(`${brandBody(fitToWidth('Claude replied.', suffix))}${k.dim(suffix)}`);
resolve(payload); resolve(payload);

View File

@@ -20,7 +20,7 @@ import k from 'kleur';
import * as setupLog from '../logs.js'; import * as setupLog from '../logs.js';
import { offerClaudeAssist } from './claude-assist.js'; import { offerClaudeAssist } from './claude-assist.js';
import { emit as phEmit } from './diagnostics.js'; import { emit as phEmit } from './diagnostics.js';
import { brandBody, fitToWidth } from './theme.js'; import { brandBody, fitToWidth, fmtDuration } from './theme.js';
export type Fields = Record<string, string>; export type Fields = Record<string, string>;
export type Block = { type: string; fields: Fields }; export type Block = { type: string; fields: Fields };
@@ -307,18 +307,16 @@ async function runUnderSpinner<
): Promise<T> { ): Promise<T> {
const s = p.spinner(); const s = p.spinner();
const start = Date.now(); const start = Date.now();
s.start(fitToWidth(labels.running, ' (999s)')); s.start(fitToWidth(labels.running, ' (99m 59s)'));
const tick = setInterval(() => { const tick = setInterval(() => {
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
s.message(`${fitToWidth(labels.running, suffix)}${k.dim(suffix)}`); s.message(`${fitToWidth(labels.running, suffix)}${k.dim(suffix)}`);
}, 1000); }, 1000);
const result = await work(); const result = await work();
clearInterval(tick); clearInterval(tick);
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
if (result.ok) { if (result.ok) {
const isSkipped = result.terminal?.fields.STATUS === 'skipped'; const isSkipped = result.terminal?.fields.STATUS === 'skipped';
const msg = isSkipped && labels.skipped ? labels.skipped : labels.done; const msg = isSkipped && labels.skipped ? labels.skipped : labels.done;

View File

@@ -51,6 +51,22 @@ export function accentGreen(s: string): string {
return k.green(s); return k.green(s);
} }
/**
* Format an elapsed-time duration (in milliseconds) for the spinner
* suffixes setup writes everywhere. Sub-minute durations stay in plain
* seconds (`47s`); once the timer crosses 60 seconds we switch to the
* `Xm Ys` form (`2m 34s`) so a long step doesn't read as `247s` or
* similar. The format is consistent above 60s — `4m 0s` over `4m` —
* so live spinner output doesn't change shape at every whole minute.
*/
export function fmtDuration(ms: number): string {
const totalSec = Math.round(ms / 1000);
if (totalSec < 60) return `${totalSec}s`;
const m = Math.floor(totalSec / 60);
const s = totalSec % 60;
return `${m}m ${s}s`;
}
/** /**
* Brand body color for setup-flow prose. Used for card bodies (via the * Brand body color for setup-flow prose. Used for card bodies (via the
* `note()` formatter) and `p.log.*` body arguments — anywhere the * `note()` formatter) and `p.log.*` body arguments — anywhere the

View File

@@ -17,7 +17,7 @@ import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
import { isValidTimezone } from '../../src/timezone.js'; import { isValidTimezone } from '../../src/timezone.js';
import { fitToWidth } from './theme.js'; import { fitToWidth, fmtDuration } from './theme.js';
export function claudeCliAvailable(): boolean { export function claudeCliAvailable(): boolean {
try { try {
@@ -44,18 +44,16 @@ export async function resolveTimezoneViaClaude(
const s = p.spinner(); const s = p.spinner();
const start = Date.now(); const start = Date.now();
const label = 'Looking up that timezone…'; const label = 'Looking up that timezone…';
s.start(fitToWidth(label, ' (999s)')); s.start(fitToWidth(label, ' (99m 59s)'));
const tick = setInterval(() => { const tick = setInterval(() => {
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
s.message(`${fitToWidth(label, suffix)}${k.dim(suffix)}`); s.message(`${fitToWidth(label, suffix)}${k.dim(suffix)}`);
}, 1000); }, 1000);
const reply = await queryClaude(prompt); const reply = await queryClaude(prompt);
clearInterval(tick); clearInterval(tick);
const elapsed = Math.round((Date.now() - start) / 1000); const suffix = ` (${fmtDuration(Date.now() - start)})`;
const suffix = ` (${elapsed}s)`;
const resolved = reply ? extractTimezone(reply) : null; const resolved = reply ? extractTimezone(reply) : null;
if (resolved) { if (resolved) {