Conversation
cli-highlight writes to stderr before throwing when a language isn't registered. The existing try/catch swallowed the throw but not the warning, so the text appeared inline in the alt buffer every time a code fence with an unrecognised language (e.g. jsonl) was rendered. Fix: gate the highlight() call behind supportsLanguage(). Unknown languages fall back to plain rendering with no stderr output at all. Also add a LANGUAGE_ALIASES map. jsonl maps to json so those fences get JSON syntax colouring rather than a plain fallback. The fence header still shows the original language name — only the highlighter sees the alias. Closes #216
f989bda to
ab6c2e7
Compare
bananabot9000
left a comment
There was a problem hiding this comment.
Clean highlight.js fix 🍌
Root cause: cli-highlight writes to stderr before throwing on unknown languages. Old try/catch caught the throw but the terminal flicker already happened. supportsLanguage gate eliminates stderr output at the source.
LANGUAGE_ALIASES map is a nice addition -- extensible, preserves original fence label in output, one-entry cost for now but ready for growth.
getHighlighted() extraction: Clean single-responsibility function. Try/catch retained inside for unexpected failures on supported languages -- belt and suspenders.
Tests: 3 new tests covering unknown language fallback, alias label preservation, and alias content rendering. All sensible.
Suggestion: Add a happy-path test for a known language (e.g. typescript or json) to guard against regressions in the primary highlighting path. All 3 current tests cover fallback/alias paths -- a supportsLanguage regression would go undetected.
No sensitive data, no reversions, no files that shouldn't be committed.
Reviewed by BananaBot9000 🍌
* Extract config utilities to claude-core, add config loading to claude-sdk-cli mergeRawConfigs, loadConfig, cleanSchema, and generateJsonSchema move to claude-core/src/config.ts so both apps can share them without duplication. The execPermissions.approve additive-array behaviour that was hardcoded in claude-cli's mergeRawConfigs is now expressed via an additivePaths option on the generic function. claude-cli passes ['execPermissions.approve']; the generic function knows nothing about the field name. claude-sdk-cli gains a proper cli-config layer (schema, consts, types, loadCliConfig) backed by ~/.claude/sdk-config.json and ./.claude/sdk-config.json. The hardcoded cliConfig.ts is deleted; model and historyReplay now come from the loaded config with Zod defaults as fallback. 9 tests cover the new schema (defaults, overrides, invalid-value fallbacks). * Session log: PRs #219, #220, #222, issues #208/#221 * Add --init-config command to claude-sdk-cli Creates ~/.claude/sdk-config.json with all defaults and a $schema reference if the file does not already exist. Matches the pattern from claude-cli. Also documented in --help output. * Fix * Generate sdk-config JSON schema during build Mirrors the pattern in claude-cli/build.ts. After the esbuild step, generateJsonSchema() runs against the sdk schema and writes schema/sdk-config.schema.json at the repo root. The SCHEMA_URL in consts.ts already pointed at this path on main; now the file actually exists. * Hot reload config between agent turns Watches CONFIG_PATH and LOCAL_CONFIG_PATH with fs.watch(). Changes are debounced 100ms then flagged as pendingReload. After each runAgent() completes the flag is checked — if set, config is reloaded and the status line model is updated before the next turn begins. The watcher variables are declared before cleanup() so SIGINT during startup (before the watch loop runs) doesn't hit the temporal dead zone. * Encapsulate config file watching in SdkConfigWatcher The inline watcher state in main.ts (pendingReload flag, reloadDebounce timer, watchers array, and scheduleReload callback) was managing three distinct concerns — loading, watching, and reload signalling — in the same scope as unrelated startup logic. It was readable but the coupling would only grow. SdkConfigWatcher bundles the three concerns into one place: it loads config on construction, watches both paths, debounces the FS events, and exposes checkReload() to poll for a change after each agent turn. main.ts now only needs to know about the watcher, not how watching works. * Update session log: hot reload and SdkConfigWatcher * Make config reload event-driven instead of polled The previous implementation set a pendingReload flag from the FS watch callback and only checked it after each runAgent() call. That meant if you edited the config file while sitting at the input prompt, nothing updated until you actually submitted a turn. The whole point of hot reload is that it happens hot. SdkConfigWatcher now takes an onChange callback in its constructor and invokes it from the debounced FS event itself. The callback runs as soon as the debounce window expires, regardless of what the agent loop is doing. main.ts passes a callback that updates the model display and logs the reload, then drops checkReload() entirely — the loop just reads watcher.config.model on each turn. Reload errors (e.g. invalid JSON, schema validation failure) are caught in #reload() and logged as a warning. The previous config is kept so the app keeps working with the last known good values. * Latch model display to the running turn With the previous event-driven reload, editing the config mid-turn would flip the model display to the new value while the API call was still running with the old model. The display would lie about which model was answering the current query. The model passed into runAgent is captured by value when the call starts, so the API call itself is unaffected by config changes — only the display was wrong. Fix is to track turnInProgress in main.ts and suppress display updates from the watcher callback while a turn is running. After runAgent returns, sync the display to the current config.model so any deferred change lands. The loop owns the busy flag, not the watcher — the watcher has no business knowing about agent turns. Its job is to publish config changes; the loop decides when those changes are visible. * Show the model name in the query block The model the current turn is using is interesting information, and the latch we just added means the status-bar model is the *next* turn's model once a config change comes in mid-turn. Putting the model name inside the query block itself ties it to the actual turn being recorded, so you can scroll back through history and see exactly which model produced each query without checking the log file. The handler already had #model in scope (it's used for cost calculations), so it's just a matter of prepending one line to the query_summary stream: 🤖 claude-sonnet-4-5 5 system · 37 user · 37 assistant · 11 thinking The robot emoji is in the same family as the existing block emojis (💬 💭 📝 🔧 🗜 ℹ️) so visually it fits. Updated the existing 'streams the parts joined by ·' test to include the model line, plus added a new test asserting that whatever model is configured ends up in the streamed output. * Use io:input for schema generation; drop isRoot from cleanSchema Passing io:'input' to toJSONSchema() generates the schema from the user-facing input perspective rather than the parsed output. This means fields with Zod defaults are not included in required[] — which is exactly what we want for a config file schema where every field is optional and defaults fill in the gaps. The previous approach stripped required[] and additionalProperties manually in cleanSchema() using an isRoot flag. That was doing it wrong way round: generating an output schema and then patching it to look like an input schema. io:input just generates it correctly from the start. The only post-processing still needed is stripping the Number.MAX_SAFE_INTEGER maximum that Zod emits for unbounded numbers. Removed STRIP_KEYS, the isRoot parameter, and all the conditional stripping logic from cleanSchema(). The function is now a single- purpose MAX_SAFE_INTEGER cleaner. Both generated schema files updated. * Fix hono vulnerabilities.
Fixes #216
What happened
cli-highlightwrites to stderr before throwing when a language isn’t registered with highlight.js. The existing try/catch inrenderBlockContentswallowed the throw but not the warning, so it appeared inline in the alt buffer every time a code fence with an unrecognised language (e.g.jsonl) was rendered.Fix
supportsLanguagefromcli-highlightand gate thehighlight()call behind it. Unknown languages fall back to plain rendering with no stderr output at all — the try/catch is now only reached for genuinely unexpected failures on a supported language.LANGUAGE_ALIASESmap (jsonl → json) so those fences get JSON syntax colouring rather than a silent plain fallback. The fence header still displays the original language name; only the highlighter sees the alias.Tests
Three new cases in
renderConversation.spec.ts:jsonlfence header preserves the original label (notjson)jsonlcode content is rendered