InputHistoryHandler.executeNodeScript and ClaudeSessionQueryService
.runSessionQuery still read stdout to EOF before calling waitFor(30s),
so a child that hung with stdout open blocked the caller forever.
InputHistoryHandler also wrote stdin synchronously before the timer
started, which blocked forever once the payload outgrew the pipe buffer
of a child that never read it.
Extract the #1778 pattern into bridge/NodeProcessRunner and use it from
NodeJsServiceCaller, InputHistoryHandler and ClaudeSessionQueryService:
- stdout is drained and stdin written on named daemon threads, so
waitFor(timeout) is the only blocking step and also covers stdin;
- on timeout the child is terminated and a TimeoutException is thrown;
- the output is only used once the reader reached EOF: if stdout is
still open 2s after exit, or the reader failed, the call fails instead
of returning possibly truncated output (the #1778 code joined the
reader without checking isAlive);
- the child is registered with ProcessManager and unregistered in
finally, as before.
ClaudeSessionQueryService now has its 30s deadline cover the whole run,
including streaming the output.
Follow-up to the review of the in-place Claude SDK update:
- Replace the shutdown of the current window's daemon with a shared SDK
generation. Once the SDK changes, every Claude daemon in the IDE restarts
before its next request, and a daemon serving a request is left alone, so
an update from the Dependencies page no longer aborts a reply with
"Daemon stopped".
- Run installs of the same SDK one after another, so the chat button and
the Dependencies page cannot run npm into the same directory at once.
- Report a run where every npm attempt timed out as a failure instead of
falling through to success.
- Add the previously installed version and the Claude CLI override to the
install result. The card says when the update left the newest supported
SDK in place, or when the custom CLI is what needs updating.
- Keep the update state in a shared store, so a card that remounts still
shows the running update and its result. The same error shown after the
update gets the button again.
- Show npm's latest output while updating, give up after 15 minutes so a
lost reply cannot block retries, and explain failures that come without
a reason.
- Use the Android Studio plugin text only for Android Studio 2026.2+ on
Windows/macOS; other IDEs get new IDE-neutral module text instead of
Boot Runtime advice, which cannot restore missing platform classes.
JBCefBrowserFactory now shares the same Android Studio predicate.
- Quick Fix and New Tab show a sticky balloon with the steps instead of
a status-bar tooltip; Quick Fix checks before its prompt popup.
- Say "install, enable, or update" Web Browser (JCEF) in all locales.
- Test that guarded entry points link with the JCEF module hidden, that
the same setup reproduces the #1912 link failure, and that every
locale defines the guidance keys.
Dialogs close as soon as the page hands an answer to Java. When Java
could not deliver it, the failure ack reached the page but nothing
handled it, so Codex kept waiting on a request the user could no longer
see and the turn looked stuck until it was stopped.
- Track each Codex answer the page sends; on a failure ack show a toast
and reopen the dialog once. Skip the reopen when Java marks the
failure as not retryable, the deadline has passed, it was already
retried, or the runtime closed the dialog in the meantime.
- Give control-command timeouts a readable error. A null message became
JsonNull, which Gson drops, so timeouts read as success on the page.
- Mark unknown or stale interactions as retryable: false.
- Log undelivered interaction responses in the IDE log.
- Route the nine Codex send sites through two shared helpers.
When the app-server advertises availableDecisions, that list already
carries the exec-policy amendment it will accept. The webview appended
proposedExecpolicyAmendment on top, so the dialog showed two identical
"allow with this command rule" buttons. Use the advertised list as-is
and only synthesize amendments when the server sends no list.
Rule buttons now show the command prefix or host they would allow,
truncated with the full text on hover.
parseOptionalTurnCursor rejected integers above Number.MAX_SAFE_INTEGER, so
a cursor like 1e21 came back as the latest page with cursorReset=false while
6 or 1000 against a five-turn history still came back with cursorReset=true.
The 2^53 line is not a meaningful boundary: only
buildSessionMessagesPagePayload knows the history length, and comparing 1e21
against it is exact.
The flag also matters to the only caller that sends a cursor.
SessionMessageOrchestrator.loadEarlierClaudeHistoryPage replaces its
transcript when cursorReset is set and prepends the page otherwise, so a
latest page returned without the flag would duplicate every visible message.
The Java cursor is a 32-bit int, so this cannot occur from the plugin today,
but the parser should not encode the opposite rule.
Use Number.isInteger again, document why the upper bound is left to the
payload builder, and replace the safe-range tests with ones asserting
cursorReset for any cursor beyond the history. Also drop the redundant
empty/undefined guard on args[2]; the parser already treats both as absent.
common.retry is not defined in any locale, so the button rendered the raw
key. Switch to settings.cli.retry and assert the lookup-failure keys exist
in the shipped en/zh locales, since the i18n mock echoes unknown keys.
Remote JCEF can replace a hidden chat page without delivering onLoadEnd.
Java still reports frontendReady for the old document, so onTabActivated
skipped recovery and the new page waited forever for sendToJava, leaving
the tab on "Checking SDK status...".
Tab activation now runs the existing idempotent bootstrap for the current
browser and page generation before scheduling retries. Disposed, inactive
and mismatched pages are still skipped, and ready pages start no retries.
A frontend_ready that arrives while Java still holds the ready flag can
only come from such a replaced page, which booted from the tab's original
HTML snapshot. Treat it as runtime recovery so Java pushes the current
provider, model and reasoning state before replaying the transcript.
getSessionPage parsed the optional beforeTurn cursor inline and accepted any
integer-valued number. Values outside the safe integer range (1e21, 2^53 and
above, or their string forms) pass Number.isInteger, reach
buildSessionMessagesPagePayload and are read there as a cursor beyond the
history length, so the response carries cursorReset=true: the client discards
its cached history and reloads from scratch for a value that was never a usable
cursor.
parseOptionalTurnCursor() now owns the parsing and applies
Number.isSafeInteger, so those values degrade to the latest page like every
other unusable cursor. Absent cursors (null, undefined, '', whitespace) still
return null, and an explicit 0 is still preserved as "the page before the first
turn". limit is untouched: its guard is "> 0", so a polluted 0 already falls
back to 30.
This builds on #1867 (2dc9ecd5), which fixed the empty-cursor coercion, and
carries over the follow-up work requested when #1868 was closed as superseded:
the extracted pure function, the safe-integer tightening and unit tests. Adds
ai-bridge/channels/claude-channel.test.js covering absent, explicit-zero,
numeric, unusable and unsafe-integer inputs, plus the cursorReset contrast at
the payload layer. No behavior changes for valid cursors.
Changing editor state used to be pasted straight into the message text,
spending the user message's native budget and polluting history replay.
Each turn now freezes structured additionalContext fragments instead
(ide_context, open_tabs, workspace_context, referenced_files_<index>,
terminal_context_<index>), so every fragment enjoys its own native budget
and the request text stays pristine.
The service sniffs the selected CLI once per child process via
app-server generate-json-schema --experimental to learn whether
turn/start accepts additionalContext, and re-sniffs after a runtime
replacement; discovery never creates a thread or turn and cancels leave
it early. Older CLIs get the official desktop text envelope with UTF-8
text-element byte ranges shifted accordingly, and history replay strips
that envelope so users only ever see their own request.
Cover the flow with schema-probe, fallback, cancellation and daemon
tests on the bridge side plus context-budget, payload-copy and display
sanitizer tests on the Java side; document the contract in
docs/codex/app-server.md.
Codex streaming could omit commentary, reasoning, and edit cards between
batches until the conversation was reloaded. Historical exec wrappers also
collapsed supported parallel calls into generic exec cards and used command
labels that differed from live rendering.
Preserve snapshot ordering and complete message corrections:
- Compare complete raw message content when selecting the transported suffix;
keep structural signatures limited to snapshot scheduling.
- Apply a buffered full snapshot before its dependent tail update advances
the sequence barrier, retaining intermediate items during React batching
and stream completion.
Restore recorded tool batches without guessing result ownership:
- Associate Promise.all and Promise.allSettled results with their original
array slots, including independently printed index labels in forEach and
bounded for loops.
- Retain labeled arrays, primitives, plain text, and label-like values.
- Restore mixed command and write_stdin batches while keeping polling tied
to the original process card.
- Match identical commands through confirmed process identities or unique
explicit absolute working directories, including file URIs and Windows
paths, while preserving literal POSIX backslashes.
- Never assign unindexed parallel process receipts by arrival order; retain
the original wrapper whenever ownership cannot be established.
Keep local Promise computations out of the visible tool timeline:
- Preserve Promise.resolve expressions and indexed results as internal data
without executing historical source.
- Filter these entries through the shared rendering predicate so they neither
create cards nor interrupt command groups in live or historical messages.
- Separate rejection metadata from result fields, distinguish Promise.all
values from settled envelopes, and decode nested local receipts only when
the source proves that the complete settled receipt was printed.
- Keep flattened local Promise.all output in its original wrapper rather
than interpreting data fields named status or error as execution outcomes.
Align historical command rows with live presentation:
- Preserve recorded summaries, approval reasons, and parsed command actions.
- Accept both list_files and listFiles directory action names.
- Remove generated English command summaries and retain Bash tool identities
for legacy reads and searches so the shared renderer controls row labels.
Extend Java, React, and browser regression coverage for snapshot ordering,
process ownership, mixed batches, local result preservation, command labels,
and continuous tool grouping. Update the Codex runtime documentation and
browser fixtures to describe the resulting behavior.
Validation:
- 199 Java tests passed across CodexKnownExecReplayTest,
HistoryMessageInjectorTest, CodexHistoryPageIndexTest, and
StreamMessageCoalescerStreamEndHookTest.
- Gradle checkstyleMain passed.
- 67 targeted frontend tests passed across snapshot callbacks, MessageItem,
GenericToolBlock, tool constants, and command presentation.
- TypeScript test-project checking and git diff --check passed.
ClaudeSettingsSyncPlan.CLAUDE_MANAGED_ENV_KEYS listed API_TIMEOUT_MS as a
plugin-managed key, so every provider sync deleted any user-configured
value from ~/.claude/settings.json. Users running slow local backends
(Ollama with 80k+ token contexts) had no way to extend the request
timeout - the CLI's built-in default cut the connection mid-generation
and the local server aborted the task (#1307).
API_TIMEOUT_MS is a performance knob, not a credential: the plugin does
not set it, does not need to own it, and clearing it serves no isolation
purpose. Remove it from the managed set so a user-configured value
survives provider syncs; when unset, the Claude CLI keeps its own
default.
Closes#1307
* Fix handling of empty currentId in ProviderManager
Allow explicitly empty currentId to be a valid disabled state without triggering a config rewrite.
* Create ProviderManagerNormalizationTest.java
* Add files via upload
* Delete unused file
Cover the 19 commits merged by PR #1963:
- feat: persist the chat font-size level in ~/.codemoss/config.json so the
chosen scale survives IDE restarts, acknowledged by request ID
- perf: fetch the compressed entry and stylesheets in parallel and reuse
verified (SHA-256) startup assets across chat tabs
- fix: Token Tracker dashboard crashed with removeChild because the entry
was evaluated as a blob while lazy chunks resolved the canonical URL;
an import map keeps a single entry/React instance (#1951)
- fix: keep #L..-L.. visible on long file references in input file tags
- fix: scan plugin skills declared as individual directories (#1961)
- fix: keep the $ Codex skill list on partial skills/list errors
- fix: let Claude subagents keep their own permission mode
- fix: keep pasted image drafts in sessionStorage so tabs cannot share them
- fix: keep dropdown fly-outs anchored at any font zoom
- build: Aliyun Maven mirror first and foojay JDK toolchain resolution
webview/src/version/changelog.ts is regenerated by
webview/scripts/extract-changelog.mjs, not edited by hand.
Resolve semantic conflict with e0c356a5 (preserve entry module identity):
keep the verified startup-asset cache and add an import map that remaps the
canonical entry URL to the evaluated blob, so lazy chunks importing the
entry keep a single module identity while cached bytes are still reused.
When a directory both parses as a skill (root SKILL.md) and contains
child skill directories, scanning only the root silently dropped the
child skills. Scan both: asset folders contain no SKILL.md and are
skipped by parse, so single-skill and parent-directory manifests keep
working unchanged while mixed layouts now yield every skill.
Adds a regression test for the mixed shape.
Direct connections to repo.maven.apache.org are frequently TLS-reset
behind the GFW, so the Aliyun public mirror is consulted before
mavenCentral. The foojay toolchain resolver plugin lets Gradle download
the required JDK toolchain instead of failing when the local JDK does
not match the target version.
Submenu geometry mixed zoomed getBoundingClientRect values with
layout-pixel scroll sizes, so above 100% font zoom the effort fly-out
drifted off its row and overflowed small viewports. Compute the whole
submenu layout in layout pixels and measure the natural content size
even when maxHeight clips the box.
The main dropdown now clamps its width to the viewport and matches
the CSS min-width floor, and open menus follow window resizes and
content changes through a ResizeObserver instead of measuring once
at open time.
localStorage drafts are shared by every same-origin chat tab, so one
webview's screenshots could be restored by another. Persist the draft
in sessionStorage instead: it still survives input remounts and page
reloads but never crosses webviews, and the legacy shared draft is
deliberately left behind because its owner is unknown.
When a save fails or exceeds the size cap, drop the previous draft so
removed images stay removed after a reload, and skip rewriting a
just-restored draft so full storage cannot wipe what was restored.
JCEF pages loaded via loadHTML cannot rely on localStorage surviving a
restart, so the chosen scale vanished every time the IDE reopened. The
level now lives in ~/.codemoss/config.json next to the font-family
preference and rides the existing resolved UI-font payload.
Explicit selections send set_font_size_level with a request ID and are
acknowledged through the resolved font config; a shared external store
keeps Settings, the global --font-scale and the OSR repaint nudge in
sync, browser storage stays an immediate cache only, an unreadable
config file is rejected instead of replaced, and a legacy browser choice
is migrated without pinning the default.
The PreToolUse hook read the parent's cached mode for every call, so a
worker running under the native auto reviewer got forced to "ask" as soon
as the parent switched modes, and a worker's EnterPlanMode/ExitPlanMode
mutated the parent Query or opened a parent plan-approval dialog.
Recognized permission_mode values from the hook input now win over the
parent state for worker calls; worker plan transitions, plan-file
ownership and the SDK-only dontAsk/bubble modes yield to the native
permission flow, and missing or unknown modes still fall back to the
parent policy.
Addresses review feedback on #1961:
- reuse the already-parsed metadata when the scanned directory is itself a
skill, instead of parsing SKILL.md a second time
- use JUnit's TemporaryFolder rule so test directories are deleted after each
test instead of accumulating in the system temp dir
`scanSkillsAsCommands` treats the path it receives as a parent directory and
scans its subdirectories for SKILL.md. But `plugin.json` may declare `skills`
as an array of individual skill directories:
"skills": ["./skills/engineering/ask-matt", "./skills/engineering/tdd"]
The plugin reference allows both forms - "each a directory of <name>/SKILL.md
folders or one folder holding SKILL.md directly". With the per-skill form,
every listed directory is passed through as-is, nothing parses as a skill, and
all of the plugin's skills silently yield zero slash commands - no log line,
no warning.
Repro: enable a plugin that declares individual skill directories (for
example mattpocock-skills, 21 skills). The log reports
"Slash commands resolved locally: 15 commands" and none of that plugin's
skills appear in the "/" menu.
Fix: if the given directory itself contains SKILL.md, scan it as a single
skill instead of looking for subdirectories.
Tests: SkillDirScanTest covers the per-skill manifest shape, the parent
directory shape (regression guard) and a directory with no skill.
`declaredSkillDirectoryItselfIsScanned` fails without the change.
The tool-window entry point now checks that JBCefBrowser and
JBCefOSRHandlerFactory can load through the plugin class loader without
running their static initializers. Missing classes or linkage errors
show a Swing panel with existing JCEF installation and restart guidance,
then return before bridge startup, tab restoration, or listener
registration. Added tests cover availability, missing classes, linkage
failures, static initialization, and factory loading with JCEF hidden.
Fixes#1912
A partial `skills/list` failure (one unreadable SKILL.md, an invalid
frontmatter entry, a stale skill under another cwd) sets the catalog error
while still returning the valid skills. The `$` completion coordinator
treated any error as fatal and published an empty list, which erased every
Codex skill the user had (custom skills included) and marked the native
catalog authoritative, so the backend-scanned `$` list was ignored for the
rest of the session. The picker then reported "no results" with no way back.
Publish whatever the runtime did return on partial errors, and skip the
publish only when the read failed without any rows: a failed read is not an
authoritative empty inventory, so the backend-scanned fallback stays usable
until the runtime answers with real data. `nativeSkills.error` still reaches
the Skills settings warning.
Covered by two coordinator tests: partial error keeps native skills, and an
outright failure no longer swallows the backend-scanned `$` payload.
Full Auto could run sandboxed while the selector still read Full Auto. The
permission mode is app-level state that survives a reinstall, but the access
scope only lived in webview localStorage, so a wiped snapshot or a
pre-native-scope migration left the app-server on workspace-write and denied
writes outside the project — with no way to see why. The permissions panel made
it worse: it reported the legacy .idea record as both "desired" and "effective",
so it claimed full access while the runtime was sandboxed, and changing it there
was overwritten by the next message.
- Derive the access scope from the authoritative mode whenever no explicit
choice is recorded ('default' source), so a reinstall self-heals; an explicit
'user'/'native'/'migrated' scope is never overridden.
- Honour a persisted sandbox in the snapshot migration and stop treating the
never-written `codexFullAccessExplicit` flag as the only Full Auto evidence;
pair a persisted full-access preset with danger-full-access.
- Drive the permissions panel from the chat state (the value every turn sends)
and write panel changes back into it; show "effective" only from the
app-server's thread/settings/updated echo, never the requested value.
- Stop reporting fabricated sandboxDesired/sandboxEffective/sandboxConflict
from ProjectConfigHandler, and drop the matching webview branches.
- Export buildTurnSettings and pin the mode -> approval/sandbox contract with
tests, since the webview mirrors that mapping.
- Delete codex-sandbox-resolution.js and utils/permission-mapper.js: both were
unreferenced by production code, and the latter encoded a platform-dependent
Full Auto mapping that no longer exists.
Tests: webview 2440 passed (tsc clean), ProjectConfigHandler/CodexInteraction
handler tests, 32 persistent-codex-service tests.
The API rejects requests from a Claude Code older than a model's minimum
version ("Claude Code X does not support this model; version Y or newer is
required", error_code claude_code_version_too_old). The plugin runs the
Claude Code bundled in its own SDK, which updating the plugin or the
terminal CLI does not change, so the hint in the message does not help.
- Match the error in the diagnostic card and add an "Update SDK" button
that sends the existing update_dependency request and shows the result
in place.
- Restart the Claude daemon after a successful Claude SDK install or
update. The daemon caches the SDK module it imported, so the next
message would otherwise keep running the old SDK until the IDE restarts.