Skip to content

fix: new-chat model selection, JCEF module dependency, headless turn-test hang - #172

Merged
spopescu merged 41 commits into
mainfrom
fix/chat-model-selection-jcef-hang
Jul 22, 2026
Merged

fix: new-chat model selection, JCEF module dependency, headless turn-test hang#172
spopescu merged 41 commits into
mainfrom
fix/chat-model-selection-jcef-hang

Conversation

@spopescu

@spopescu spopescu commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Four fixes, plus the previously-unpushed main commit 33a7a90 (rescued here since main is protected).

1. New-chat model selection regression

A fresh chat showed the pinned model (e.g. Opus) in the dropdown but launched the CLI on its own default (Haiku) until the user manually re-picked.

Root cause: the dropdown reads the tab's AgentSelection.modelId, while the Claude/Codex backends re-derived --model from the selectedModels settings map — empty when the default was set via the Roles tab (which writes roleSelections, never selectedModels). So no --model was passed and the CLI fell back to its own default.

Fix: CliProcess honors the tab's pinned selection.modelId (threaded through AgentBackendFactory), falling back to the settings-map read only when blank. Codex branch mirrors this.

2. JCEF NoClassDefFoundError: JBCefJSQuery$Response

Crashed on some machines (JCEF disabled / JBR without CEF) while working on others — same IDE version.

Root cause: JBCefJSQuery$Response lives in the separate com.intellij.modules.jcef platform module, which ClawDEA never declared. Without it the jcef module is absent from the plugin classloader and ChatPanel.<init> throws the moment the tool window opens.

Fix: add <depends>com.intellij.modules.jcef</depends> to plugin.xml.

3. Headless turn-test hang

OpenAiCompatibleAgentBackend restart/retry/steering tests hung when run in a fork without an IntelliJ Application.

Root cause: a running turn read ClawDEASettings.getInstance(); headless, that threw inside the turn coroutine, which then never emitted a terminal Result, so the tests' unbounded readEvent() loop blocked forever.

Fix: add a settingsProvider test seam (matching the existing DI style) so turns are self-sufficient headless, plus a bounded @Test(timeout=…) backstop.

4. Settings apply migrated an open chat to the global provider

Setting Roles → Chat default to Opus (or any settings apply) rebuilt the currently-open chat onto the global provider (e.g. Qwen).

Root cause: ChatPanel.onSettingsChanged fired on every apply and realigned the open tab to the global effectiveProviderId(), ignoring the tab's own per-tab bridge.selection. Backend kinds differed → rebuildSessionForBackendChange(null) reseeded the tab from the global provider. Predates the per-tab selection model.

Fix: a settings apply never changes an open tab's provider/backend — it only restarts the same bridge in place to refresh non-provider settings (tool-approval mode, toggles, CLI args, wiki model); the tab keeps its provider/model, and only NEW chats adopt a changed global default. The global-provider rebuild coordinator is replaced by a pure settingsApplyAction() decision.

Test plan

  • CliProcessModelSelectionTest — pinned-model precedence + blank fallback
  • SettingsApplyDecisionTest — settings apply restarts in place, never rebuilds
  • The three backend turn tests pass in isolation (previously hung)
  • compileKotlin, buildPlugin, verifyPluginProjectConfiguration green; no IDE diagnostics
  • runIde launches with the JCEF dependency; JCEF helper processes run
  • Full JetBrains verifier verdict against 2026.2 — not obtained (verifier crashed on an unrelated offline dependency download); worth confirming in CI

Notes

  • <depends>com.intellij.modules.jcef> makes JCEF a hard requirement — correct, since the whole chat UI is JCEF-based, but ClawDEA won't load on an IDE with JCEF disabled (rather than loading and crashing).
  • Also sweeps in some pre-existing in-flight work that was already in the working tree (completions/settings-tab tweaks, a wiki-librarian error-summary enhancement).
  • The 16 NoSuchMethodError: runBlockingWithParallelismCompensation failures in the full test suite are a pre-existing coroutines/platform-test-framework binary clash (present on a clean tree), unrelated to these changes.

🤖 Generated with Claude Code

spopescu and others added 2 commits July 20, 2026 13:26
OpenAI-compatible providers (Qwen, GPT, etc.) were truncating responses at 9-25s
because the openai-tooling-prompt.md resource was missing, causing a silent
fallback to incomplete tooling guidance. This incomplete guidance lacked critical
OpenAI-specific instructions for:
- Function-calling format specification
- Streaming behavior and completion semantics
- Tool approval workflows
- Best practices for safe tool execution

Without this guidance, non-Claude models emit malformed tool calls or misinterpret
stream boundaries, causing the CLI to exit prematurely mid-response.

Created src/main/resources/prompts/openai-tooling-prompt.md with comprehensive
OpenAI-specific tooling guidance mirroring the structure used for other backends.

Root cause: CliProcess.loadStaticPrompt() errors loudly when resources are missing,
but OpenAiInstructions.build() silently falls back to buildFallbackToolingGuidance().
The fallback was intended as a temporary measure but lacked the model-specific
instructions non-Claude backends require.

Fixes: Qwen 3.6-35B-A3B and other non-Claude OpenAI-compatible providers now
receive proper tooling instructions and no longer truncate mid-response.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…turn-test hang

Model selection regression: a fresh chat showed the pinned model (e.g. Opus)
in the dropdown but launched the CLI on its own default (Haiku) until the user
manually re-picked. The dropdown reads the tab's AgentSelection.modelId while
the Claude/Codex backends re-derived --model from the selectedModels settings
map, which is empty when the default was set via the Roles tab. CliProcess now
honors the tab's pinned model id (threaded through AgentBackendFactory), falling
back to the settings-map read only when blank; the Codex branch mirrors this.

JCEF NoClassDefFoundError: JBCefJSQuery$Response lives in the separate
com.intellij.modules.jcef module, which ClawDEA never declared. Without it the
jcef module is absent from the plugin classloader and ChatPanel.<init> throws on
installs where JCEF is not already on the shared classpath. Add the dependency.

Headless turn-test hang: the three OpenAiCompatibleAgentBackend turn tests hung
when run in a fork without an IntelliJ Application, because a running turn read
ClawDEASettings.getInstance() and the dying coroutine never emitted a terminal
Result, blocking the queue reader forever. Add a settingsProvider test seam so
turns are self-sufficient headless, plus a bounded @test timeout as a backstop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@spopescu

spopescu commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Token usage on this PR

Model Input Cache read Cache create Output
0 0 0 0
claude-haiku-4-5-20251001 1k 9582k 1164k 48k
claude-opus-4-7 28 1422k 127k 20k
claude-opus-4-8 133k 499185k 33171k 4026k
claude-sonnet-4-5-20250929 2k 19312k 2552k 122k
claude-sonnet-5 8k 161140k 3337k 586k

Totals — Input: 145k · Cache read: 690642k · Cache create: 40353k · Output: 4804k

spopescu and others added 27 commits July 20, 2026 20:29
…vider

Applying settings (e.g. setting Roles → Chat default to Opus) rebuilt any open
chat tab onto the GLOBAL effective provider. ChatPanel.onSettingsChanged read
AuthManager.effectiveProviderId() (global = Qwen), compared its backend kind to
the tab's own backend, saw they differed, and called rebuildSessionForBackendChange(),
reseeding the tab from the global provider. So an open Opus chat flipped to Qwen
on any settings apply — the Roles change was incidental.

A settings apply must never change an already-open tab's provider/backend: the
tab keeps its own per-tab AgentSelection and only NEW chats adopt a changed
global default. onSettingsChanged now only restarts the SAME bridge in place to
pick up non-provider settings (tool-approval mode, toggles, CLI path/args, wiki
model); CliProcess re-reads the tab's pinned model on restart, so provider and
model are preserved. The global-provider-based rebuild coordinator is replaced
by a pure settingsApplyAction() decision (RESTART_IN_PLACE / NONE), and the
now-unused deferred-rebuild path (onTurnBecameIdle flush, no-arg
rebuildSessionForBackendChange) is removed.

Also includes an in-flight enhancement: a failed wiki-librarian subagent now
prepends its WIKI-role model to the error summary for at-a-glance diagnosis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the WIP begun with the EventStreamHandler librarian-error change: a
stale WIKI-role model (e.g. Bedrock Claude 3 Haiku, which rejects prompt caching
with an HTTP 400) silently broke the in-chat wiki librarian, and the surfaced
error named neither the model nor the cause.

Root-cause fix — smart per-role defaults (RoleDefaults/RoleDefaultsResolver):
compute Chat / Wiki / Completions from whichever provider is authenticated —
latest Opus for chat + latest Haiku for wiki/completions on Claude, Terra/Luna
on Codex, first agentic model on openai-compatible — so each role stays on a
caching-capable, task-appropriate model. RoleSelectionStore seeds these on fresh
install (falling back to the legacy clone-across-roles when nothing is
authenticated), and the Roles settings tab gains a "Reset defaults" button.

Diagnostics: AgenticLibrarian now names the failing model in its error
(mirroring the in-chat subagent path in EventStreamHandler), so an
unsupported-model 400 is legible at a glance instead of an opaque failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The skill-invocation probe was hardcoded true, so every backend forwarded
'/skill-name' as literal text. OpenAI-compatible and Codex backends have no
CLI-native slash resolver, so skills silently did nothing. Gate the native
branch on BackendKind.CLAUDE_CLI; other backends take the SKILL.md-injection
fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture session skills in start(); advertise the Skill tool (gated on
preloadSkillCatalog + non-empty skills) and route Skill calls to SkillTool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ture)

Regression guard: currentSkills must be captured on every start(), incl.
resume, so the Skill tool is advertised after a resumed session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the false 'IDE will intercept slash text' claim; instruct the model to
invoke skills via the Skill tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrong-typed or null "name"/"args" now return a soft tool-error instead of
throwing out of the executor and ending the turn (matching the Bash branch).
Restore the dropped "proceed without skills if none listed" prompt caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
propose_write / apply_patch could report a file as written when it never
landed on disk: EditDiffReviewer.applyContent returned Unit and silently
bailed when parent.mkdirs() failed, and reviewAndRespond / HostPatchTool
reported ACCEPTED regardless. Non-Claude backends (Qwen) also emit relative
file_path args, which File(path) resolved against the IDE JVM CWD (~ "/"),
so writes mislanded or apply_patch wrongly rejected them as "outside project".

- applyContent now returns Boolean and resolves relative paths against the
  project base (mirrors PsiUtils); callers surface a truthful error on false.
- HostPatchTool normalizes relative file_path against projectBasePath before
  the path-inside-project check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On non-Claude backends a user-typed /skill fell back to injecting the full
SKILL.md as the bridge payload, which ChatPanel rendered verbatim as a user
bubble. Now the fallback renders a compact "Using skill /name" chip and
dispatches the markdown via CommandContext.dispatchToBridge (renderInChat=false,
mirroring BridgeExpandingHandler) so the model still receives the full skill
text. Falls back to the plain (rendering) send only when no hidden-dispatch
channel is available (headless/tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ackend

Agentic OpenAI-compatible models had no way to dispatch a sub-agent, so skills
like subagent-driven-development narrated "dispatching..." and stalled. Add an
Agent tool that runs a depth-1 nested tool loop and streams its steps into the
existing sub-agent card (SubAgentController already recognizes the "Agent" tool
and routes children by parentToolUseId).

- AgentLoopController gains an optional SubAgentRunner; a tool call named "Agent"
  is routed to it (suspend + emit) instead of the synchronous executor, so the
  executor interface and all implementors are untouched.
- SubAgentDispatcher runs a fresh nested turn, re-tags child events with the
  dispatching tool_use id, swallows the nested terminal Result, and returns the
  sub-agent's final report as the tool result. Fail-soft on missing prompt /
  malformed args. Sub-agents are NOT given the Agent tool (no recursion).
- OpenAiToolCatalog.agentToolDefinition() advertises it (subagent_type/description
  match the card fields; prompt is the task). Gated on a new enableOpenAiSubagents
  setting (default on).
- openai-tooling-prompt: document the Agent tool + require absolute file paths and
  actually writing spec/plan files instead of pasting them into chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the completions model dropdown from ProvidersTab to AdvancedTab
since the Roles tab already handles per-role model selection and the
behavioral settings (enabled/debounce/manual-only) are global, not
provider-scoped.
Since the Roles tab now handles per-role model selection and completions
settings are global (not provider-specific), move the model dropdown from
ProvidersTab to AdvancedTab alongside the existing behavioral toggles.
…ompatible

Weak/limited-context models (e.g. 128K Qwen) degraded into repetitive tool-call
loops mid-turn. Root cause: the model's context window was unknown, so the agent
loop used a ~1M-char compaction proxy that exceeded the real window — compaction
never fired and history overflowed. There was also no UI to set a window: the
models table had no such column, and ContextWindows read only profile JSON.

- Add a "Context window" column to the OpenAI-compatible models table
  (ModelEntry.contextWindow + OpenAiModelTableModel); preserved across /models
  refresh by both catalog mergers.
- ContextWindows.resolve(): catalog column → profile map → conservative 128K
  default (never null). The backend passes the resolved window so the loop always
  uses the token budget. When a provider reports no usage, the loop estimates
  tokens from chars against the same window so compaction still fires.
- ToolCallLoopGuard: detects a run of identical FAILING tool calls and escalates
  nudge → clean stop, breaking degeneration loops without erroring the turn.
  Sub-agent dispatches are exempt; a success resets the streak; distinct args are
  distinct signatures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ModifiedFrom

Complete the wiring for the new completionsModelCombo so it persists
the selected model to ClawDEASettings.State.completionsModel on apply
and reads it back on load.
…Behavior panel, fix CLI path visibility for all providers
…hardcoded completions model, extract CLI settings, fix CLI path visibility
Makes ask_wiki_librarian work for every WIKI-role provider, fixing the
"OpenAI-compatible profile '' not configured" crash when a Codex or Claude
WIKI role invoked the tool, and adds a genuine Codex execution path.

chooseLibrarianExecution tiers the handler by WIKI provider:
- Claude   -> ClaudeSubprocessLibrarian (claude -p, allowlisted MCP tools)
- openai   -> AgenticLibrarian in-process loop
- Codex    -> CodexExecLibrarian (NEW): codex exec --json

CodexExecLibrarian runs read-only by construction: -s read-only with
approval_policy=never and NO MCP server. The exec spike proved MCP tools
only run under danger-full-access (any sandbox blocks the loopback socket
on macOS), which would leave codex's shell ungated -- so the librarian
reads the on-disk wiki and greps the tree with codex's built-in shell
instead. Read commands need no escalation; a write escalation fails under
never (no hang, no write). Tradeoff: no record_wiki_suggestion, no index
tools -- acceptable for read-only Q&A.

Chat routing keys on the CHAT backend: Codex/openai chats (which cannot
spawn the --agents subagent) advertise the ask_wiki_librarian MCP tool;
the shared primer anchor is mechanism-neutral.

Verified: compileKotlin + compileTestKotlin green; 18/18 unit tests pass
(CodexExecLibrarianTest 7, LibrarianExecutionTest 6,
ClaudeSubprocessLibrarianTest 5); read-only sandbox confirmed live. Full
end-to-end Codex answer not yet proven live (OpenAI workspace spend cap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chat model selector was noticeably wider than its content. Two
independent Swing sizing quirks each added slack:

- The collapsed combo sized itself via prototypeDisplayValue, which
  routes through the cell renderer and picks up ~50px of internal
  padding; and its widest row was a signed-out third-party profile
  (long name + "(sign in)" suffix) the user never sees selected. Now
  the combo width is pinned to the widest *enabled* label + arrow +
  insets + a small margin.
- BasicComboPopup.show() hardcodes the popup width to the combo's own
  width (arrow button included). The popup is now resized to its list
  content width when shown.

Also refresh the selector on subscription/Codex auth status changes so
newly-signed-in providers become selectable without reopening the tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NotesPaths/SessionScanner/TranscriptCostReader encoded the project base
path with `"-" + trimStart('/').replace("/", "-")`, which only stripped
forward slashes. On Windows the drive colon survived (e.g. `-C:-Users-…`)
and Path.resolve threw InvalidPathException, silently dropping the notes
primer section. It also failed to locate sessions for any POSIX path
containing `.` or `_` (e.g. `.claude/worktrees` sessions).

Centralize the scheme in ClaudeProjectDir.encode(), which replaces every
non-alphanumeric char with `-` — verified against real CLI-created dirs
(`.claude` → `--claude`, `enc.test_dir` → `enc-test-dir`). Route all three
consumers through it so the plugin points at the exact directory the CLI
creates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…md.exe cap)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
spopescu and others added 12 commits July 22, 2026 00:32
The interactive chat CLI no longer injects wiki --agents; the librarian is
reached via the ask_wiki_librarian MCP tool and the author via the
out-of-band WikiAuthorInvoker. Update both pages to drop the deleted
chooseLibrarianMode / CLAUDE_SUBAGENT* framing and describe the single
MCP-tool dispatch path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete dead prompts/wiki-librarian-prompt.md (the interactive librarian
  now uses WIKI_LIBRARIAN_TOOL_PROMPT; the subprocess librarian loads
  /agents/wiki-librarian.md) and its now-orphaned resource test.
- Fix stale WIKI_LIBRARIAN_PROMPT comment references → WIKI_LIBRARIAN_TOOL_PROMPT.
- runAuthorNow KDoc: describe the invoker generically (not "--agents"),
  since the openai-compatible/codex invokers don't use --agents.
- Drop a stray double blank line in ChatPanel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…not merely unused

Reflects that the subagent-persona constant and its prompt resource are now
gone entirely (Task 4 + the minor-cleanup sweep), not "still exists but
unreferenced".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The one-shot claude/codex subprocesses redirected stdin from a hardcoded
File("/dev/null") to hand the process immediate EOF (it otherwise blocks on
an open stdin pipe). On Windows "/dev/null" is a non-existent relative path,
so Redirect.from throws at process start — the in-chat wiki-librarian tool,
the codex librarian, and the out-of-band wiki-author all failed on Windows.

Centralize the null-device path in NullDevice (NUL on Windows, /dev/null
elsewhere) and route all three ProcessBuilder stdin redirects through it,
preserving the immediate-EOF behavior cross-platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ab model

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provider/service/profile resolution before the runner call was unguarded; a
throw there would escape the /seed-wiki coroutine (no CoroutineExceptionHandler)
and leave a dangling "Seeding…" line with no resolution. Wrap setup+run so any
failure surfaces as a Result(ok=false) the caller renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…horing entry point

Adds the seed-wiki path (WikiPromptRunner, tiered by WIKI-role backend, used by
/seed-wiki to bootstrap under the WIKI model) alongside rescan auto-apply and
runAuthorNow. Produced by the drift auto-author; verified accurate against source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@spopescu
spopescu merged commit 8374385 into main Jul 22, 2026
4 checks passed
spopescu added a commit that referenced this pull request Jul 22, 2026
…0 failures) (#175)

* fix(deps): pin kotlinx-coroutines-test to the platform's coroutines-core fork

Two same-named kotlinx.coroutines.BuildersKt classes were on the test
classpath simultaneously: the IntelliJ Platform's own forked
coroutines-core (adds runBlockingWithParallelismCompensation, needed
by ComponentManagerImpl's service init) and the vanilla upstream jar
kotlinx-coroutines-test:1.11.0 pulls in transitively (needs
runBlockingK instead, which the platform's fork never had). Whichever
copy the flat PathClassLoader bound first "won" nondeterministically
across a full ./gradlew test run, breaking whichever caller needed
the other — surfacing as ~32 NoSuchMethodError failures.

Pin kotlinx-coroutines-test to 1.10.2, matching the platform fork's
base version (whose calls the fork satisfies), and exclude the
transitive vanilla kotlinx-coroutines-core/-core-jvm at the
configuration level so the platform's fork is the only BuildersKt on
the classpath.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: repair 4 pre-existing test-suite failures

- RoleSelectionStoreTest: the migration test depended on real machine
  auth state (macOS Keychain "Claude Code-credentials") instead of
  mocking AuthManager, so it failed on any machine with live Claude
  subscription credentials. Add a test seam
  (RoleSelectionStore.migrateFromLegacyIfNeeded/applyDefaults now
  accept an injectable AuthManager) and inject a fake reporting no
  authenticated providers.

- SeedWikiHandlerTest: a stale comment assumed no IntelliJ
  Application is registered so the handler's executeOnPooledThread
  fallback runs synchronously, but the Platform test harness always
  registers a real one, making the dispatch genuinely async and
  creating a race the test always lost. Wait on a CountDownLatch
  instead of asserting immediately.

- OpenAiCompatibleIntegrationTest: the expected advertised-tools set
  was never updated after PR #172 added the "Agent" sub-agent tool
  (advertised by default via enableOpenAiSubagents).

- ChatHtmlTemplateTest: asserted the pre-refactor finalizeReasoning()
  body (moving reasoning into a collapsed history block); commit
  c9d597a rewrote it to an intentional no-op and removed the related
  CSS, but the test wasn't updated to match.

Full suite: 2309 tests, 0 failures/errors/skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant