From 29884f8d6f2c5f7b7f7764dc2effe30a754af255 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 04:23:33 +0100 Subject: [PATCH] fix: wrap clack notes for cleaner boxes --- src/commands/configure.ts | 4 +- src/commands/doctor-auth.ts | 7 +- src/commands/doctor-gateway-services.ts | 7 +- src/commands/doctor-legacy-config.ts | 7 +- src/commands/doctor-sandbox.ts | 7 +- src/commands/doctor-security.ts | 7 +- src/commands/doctor-state-integrity.ts | 7 +- src/commands/doctor.ts | 4 +- src/commands/systemd-linger.ts | 7 +- src/terminal/note.ts | 94 +++++++++++++++++++++++++ src/wizard/clack-prompter.ts | 4 +- 11 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 src/terminal/note.ts diff --git a/src/commands/configure.ts b/src/commands/configure.ts index cab220c46..835107fe1 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -4,7 +4,6 @@ import { confirm as clackConfirm, intro as clackIntro, multiselect as clackMultiselect, - note as clackNote, outro as clackOutro, select as clackSelect, text as clackText, @@ -26,6 +25,7 @@ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js"; import { listChatProviders } from "../providers/registry.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; +import { note } from "../terminal/note.js"; import { stylePromptHint, stylePromptMessage, @@ -90,8 +90,6 @@ const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message); const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message); -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); const text = (params: Parameters[0]) => clackText({ ...params, diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index 270acb7b1..f91aa46e3 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -1,5 +1,3 @@ -import { note as clackNote } from "@clack/prompts"; - import { buildAuthHealthSummary, DEFAULT_OAUTH_WARN_MS, @@ -14,12 +12,9 @@ import { resolveProfileUnusableUntilForDisplay, } from "../agents/auth-profiles.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); - export async function maybeRepairAnthropicOAuthProfileId( cfg: ClawdbotConfig, prompter: DoctorPrompter, diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 70535c998..b83c2067f 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -1,7 +1,5 @@ import path from "node:path"; -import { note as clackNote } from "@clack/prompts"; - import type { ClawdbotConfig } from "../config/config.js"; import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js"; import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; @@ -26,7 +24,7 @@ import { } from "../daemon/service-audit.js"; import { buildServiceEnvironment } from "../daemon/service-env.js"; import type { RuntimeEnv } from "../runtime.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, @@ -34,9 +32,6 @@ import { } from "./daemon-runtime.js"; import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js"; -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); - function detectGatewayRuntime( programArguments: string[] | undefined, ): GatewayDaemonRuntime { diff --git a/src/commands/doctor-legacy-config.ts b/src/commands/doctor-legacy-config.ts index 4e0312b51..a8b62d770 100644 --- a/src/commands/doctor-legacy-config.ts +++ b/src/commands/doctor-legacy-config.ts @@ -1,8 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { note as clackNote } from "@clack/prompts"; - import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, @@ -12,12 +10,9 @@ import { writeConfigFile, } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import { resolveUserPath } from "../utils.js"; -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); - function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string { const override = env.CLAWDIS_CONFIG_PATH?.trim(); if (override) return override; diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index 2d4b7d697..823f0c7d5 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -1,8 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { note as clackNote } from "@clack/prompts"; - import { DEFAULT_SANDBOX_BROWSER_IMAGE, DEFAULT_SANDBOX_COMMON_IMAGE, @@ -12,13 +10,10 @@ import { import type { ClawdbotConfig } from "../config/config.js"; import { runCommandWithTimeout, runExec } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import { replaceModernName } from "./doctor-legacy-config.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); - type SandboxScriptInfo = { scriptPath: string; cwd: string; diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index 1f593c429..d1c878a50 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -1,15 +1,10 @@ -import { note as clackNote } from "@clack/prompts"; - import type { ClawdbotConfig } from "../config/config.js"; import { readProviderAllowFromStore } from "../pairing/pairing-store.js"; import { readTelegramAllowFromStore } from "../telegram/pairing-store.js"; import { resolveTelegramToken } from "../telegram/token.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import { normalizeE164 } from "../utils.js"; -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); - export async function noteSecurityWarnings(cfg: ClawdbotConfig) { const warnings: string[] = []; diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index e667ec4a6..f5ee88e54 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -2,8 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { note as clackNote } from "@clack/prompts"; - import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveOAuthDir, resolveStateDir } from "../config/paths.js"; @@ -14,10 +12,7 @@ import { resolveSessionTranscriptsDirForAgent, resolveStorePath, } from "../config/sessions.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; - -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); +import { note } from "../terminal/note.js"; type DoctorPrompterLike = { confirmSkipInNonInteractive: (params: { diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 614901ed8..9a2acfbf4 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { intro as clackIntro, - note as clackNote, outro as clackOutro, } from "@clack/prompts"; import { @@ -41,6 +40,7 @@ import { runCommandWithTimeout } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; +import { note } from "../terminal/note.js"; import { sleep } from "../utils.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, @@ -97,8 +97,6 @@ const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message); const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message); -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); function resolveMode(cfg: ClawdbotConfig): "local" | "remote" { return cfg.gateway?.mode === "remote" ? "remote" : "local"; diff --git a/src/commands/systemd-linger.ts b/src/commands/systemd-linger.ts index 3619e2718..b470dac5a 100644 --- a/src/commands/systemd-linger.ts +++ b/src/commands/systemd-linger.ts @@ -1,15 +1,10 @@ -import { note as clackNote } from "@clack/prompts"; - import { enableSystemdUserLinger, isSystemdUserServiceAvailable, readSystemdUserLingerStatus, } from "../daemon/systemd.js"; import type { RuntimeEnv } from "../runtime.js"; -import { stylePromptTitle } from "../terminal/prompt-style.js"; - -const note = (message: string, title?: string) => - clackNote(message, stylePromptTitle(title)); +import { note } from "../terminal/note.js"; export type LingerPrompter = { confirm?: (params: { diff --git a/src/terminal/note.ts b/src/terminal/note.ts new file mode 100644 index 000000000..54ed49025 --- /dev/null +++ b/src/terminal/note.ts @@ -0,0 +1,94 @@ +import { note as clackNote } from "@clack/prompts"; +import { stylePromptTitle } from "./prompt-style.js"; + +const ANSI_ESCAPE = /\u001b\[[0-9;]*m/g; + +function visibleLength(value: string): number { + return Array.from(value.replace(ANSI_ESCAPE, "")).length; +} + +function splitLongWord(word: string, maxLen: number): string[] { + if (maxLen <= 0) return [word]; + const chars = Array.from(word); + const parts: string[] = []; + for (let i = 0; i < chars.length; i += maxLen) { + parts.push(chars.slice(i, i + maxLen).join("")); + } + return parts.length > 0 ? parts : [word]; +} + +function wrapLine(line: string, maxWidth: number): string[] { + if (line.trim().length === 0) return [line]; + const match = line.match(/^(\s*)([-*\u2022]\s+)?(.*)$/); + const indent = match?.[1] ?? ""; + const bullet = match?.[2] ?? ""; + const content = match?.[3] ?? ""; + const firstPrefix = `${indent}${bullet}`; + const nextPrefix = `${indent}${bullet ? " ".repeat(bullet.length) : ""}`; + const firstWidth = Math.max(10, maxWidth - visibleLength(firstPrefix)); + const nextWidth = Math.max(10, maxWidth - visibleLength(nextPrefix)); + + const words = content.split(/\s+/).filter(Boolean); + const lines: string[] = []; + let current = ""; + let prefix = firstPrefix; + let available = firstWidth; + + for (const word of words) { + if (!current) { + if (visibleLength(word) > available) { + const parts = splitLongWord(word, available); + const first = parts.shift() ?? ""; + lines.push(prefix + first); + prefix = nextPrefix; + available = nextWidth; + for (const part of parts) lines.push(prefix + part); + continue; + } + current = word; + continue; + } + + const candidate = `${current} ${word}`; + if (visibleLength(candidate) <= available) { + current = candidate; + continue; + } + + lines.push(prefix + current); + prefix = nextPrefix; + available = nextWidth; + + if (visibleLength(word) > available) { + const parts = splitLongWord(word, available); + const first = parts.shift() ?? ""; + lines.push(prefix + first); + for (const part of parts) lines.push(prefix + part); + current = ""; + continue; + } + current = word; + } + + if (current || words.length === 0) { + lines.push(prefix + current); + } + + return lines; +} + +export function wrapNoteMessage( + message: string, + options: { maxWidth?: number; columns?: number } = {}, +): string { + const columns = options.columns ?? process.stdout.columns ?? 80; + const maxWidth = options.maxWidth ?? Math.max(40, Math.min(88, columns - 10)); + return message + .split("\n") + .flatMap((line) => wrapLine(line, maxWidth)) + .join("\n"); +} + +export function note(message: string, title?: string) { + clackNote(wrapNoteMessage(message), stylePromptTitle(title)); +} diff --git a/src/wizard/clack-prompter.ts b/src/wizard/clack-prompter.ts index 923abc13b..0b5d135ef 100644 --- a/src/wizard/clack-prompter.ts +++ b/src/wizard/clack-prompter.ts @@ -4,7 +4,6 @@ import { intro, isCancel, multiselect, - note, type Option, outro, select, @@ -18,6 +17,7 @@ import { stylePromptTitle, } from "../terminal/prompt-style.js"; import { theme } from "../terminal/theme.js"; +import { note as emitNote } from "../terminal/note.js"; import type { WizardProgress, WizardPrompter } from "./prompts.js"; import { WizardCancelledError } from "./prompts.js"; @@ -38,7 +38,7 @@ export function createClackPrompter(): WizardPrompter { outro(stylePromptTitle(message) ?? message); }, note: async (message, title) => { - note(message, stylePromptTitle(title)); + emitNote(message, title); }, select: async (params) => guardCancel(