jeo-code 0.6.31 → 0.6.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.6.32] - 2026-06-19
10
+ _Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification._
11
+
12
+ ### Fixed
13
+ - **Anthropic extended thinking was never turned on — opus-4-8/4-7 reasoning is now actually requested.** The provider parsed and replayed thinking blocks on the *response* side and sent the `interleaved-thinking` beta, but `anthropicPayload` never put a `thinking` parameter in the *request* body, so the API treated every call as non-thinking and (for the internally-reasoning opus-4-7/4-8) returned signature-only/empty thought — reasoning effectively never activated. The request builder now selects a thinking transport per model (`anthropicThinkingMode` via `parseAnthropicVersion` on `claude-<family>-<major>-<minor>`): Anthropic **≥ 4.6 → adaptive** (`thinking: { type: "adaptive" }` with `display: "summarized"` gated to Opus **≥ 4.7** via `supportsAdaptiveThinkingDisplay`, depth riding `output_config.effort`, no `budget_tokens`); **4.5 → budget-effort** (`{ type: "enabled", budget_tokens, display: "summarized" }` + `output_config.effort`); **older → budget** (budget only). jeo's reasoning effort maps to the adaptive/effort literal via `anthropicAdaptiveEffort` (minimal/low/medium/high; xhigh folded to high upstream), `temperature` stays dropped on the thinking path, and the legacy `interleaved-thinking-2025-05-14` beta is filtered out for Opus ≥ 4.7 (`anthropicBetaHeader`) so it can't shadow the adaptive transport. Mirrors gjc's `inferThinkingControlMode` / `supportsAdaptiveThinkingDisplay` behavior.
14
+
15
+ ### Changed
16
+ - **The trigger highlight now paints *every* `/command`·`$skill` token on the line and keeps it lit after the space.** New pure helpers in `slash.ts` — `allTriggerTokens(line)` (every whitespace-delimited `/`·`$` word, left-to-right, with code-point `start` offsets; paths like `src/cli` and `FOO$BAR` still excluded) and `committedTriggerToken(line)` (the leading invocation once a space follows, so the highlight no longer vanishes the instant you type a trailing space). `InputBoxOptions.highlight` accepts a multi-range `HighlightRange[]`, so a prompt mentioning several invocations lights each one (valid → neon green, no-match → pink) at once, independent of caret position.
17
+
18
+ ### Verified
19
+ - **`jeo --tmux` has no bun memory leak and stays responsive.** A real `--tmux` session flooded with 200 `/command` keystrokes plus 80 SGR mouse-report sequences via `tmux send-keys` holds RSS bounded (159.8 → 161.5 MB peak → 161.4 MB settled, +1.5 MB and *decreasing* after the flood — no per-event linear growth) and the `tmux-verify.sh smoke` + `battery` (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`) all pass.
20
+ - **Full suite green:** `bun run typecheck` clean and `bun test` 1708 pass / 0 fail across 211 files (includes the extended `test/anthropic-stream.test.ts` adaptive/budget request-body coverage and the `test/slash.test.ts` / `test/input-box.test.ts` multi-token highlight tests).
21
+
9
22
  ## [0.6.31] - 2026-06-19
10
23
  _Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification._
11
24
 
package/README.ja.md CHANGED
@@ -200,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
200
200
  ## 変更履歴 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
203
204
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
204
205
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
205
206
  - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
206
207
  - **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
207
- - **[0.6.27]** (2026-06-19) — Ponytail pass on the reasoning-tier mapper, plus a real-tmux verification of `jeo --tmux`.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -200,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
200
200
  ## 변경 이력 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
203
204
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
204
205
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
205
206
  - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
206
207
  - **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
207
- - **[0.6.27]** (2026-06-19) — Ponytail pass on the reasoning-tier mapper, plus a real-tmux verification of `jeo --tmux`.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -200,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
200
200
  ## Changelog
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
203
204
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
204
205
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
205
206
  - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
206
207
  - **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
207
- - **[0.6.27]** (2026-06-19) — Ponytail pass on the reasoning-tier mapper, plus a real-tmux verification of `jeo --tmux`.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -200,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
200
200
  ## 更新日志 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
203
204
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
204
205
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
205
206
  - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
206
207
  - **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
207
- - **[0.6.27]** (2026-06-19) — Ponytail pass on the reasoning-tier mapper, plus a real-tmux verification of `jeo --tmux`.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.31",
3
+ "version": "0.6.32",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -65,11 +65,10 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
65
65
  { canonical: "claude-sonnet-4-5", provider: "anthropic", providerModel: "claude-sonnet-4-5-20250929", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
66
66
  { canonical: "claude-opus-4-1", provider: "anthropic", providerModel: "claude-opus-4-1-20250805", contextTokens: 200_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
67
67
  { canonical: "claude-opus-4-5", provider: "anthropic", providerModel: "claude-opus-4-5-20251101", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
68
- // NOTE: opus-4-7 accepts extended thinking but currently returns 0 thinking tokens
69
- // (model-internal, no visible thought). opus-4-8 thinks internally (tokens billed,
70
- // signature present) but returns empty thinking text. Both are FULL-capable in the
71
- // catalog so the budget is always sent the nativizable path handles signature-only
72
- // artifacts for cross-turn continuity.
68
+ // NOTE: opus 4.6+ use Anthropic ADAPTIVE thinking (type:"adaptive" + output_config.effort).
69
+ // opus 4.7/4.8 OMIT visible thought unless the request opts into `display: "summarized"` —
70
+ // anthropic.ts sets that on the adaptive transport so reasoning streams again (gjc parity).
71
+ // The nativizable path still replays signature-only thinking blocks for cross-turn continuity.
73
72
  { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
74
73
  { canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
75
74
  { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
@@ -16,14 +16,25 @@ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
16
16
  const ANTHROPIC_API_KEY_BETA = [
17
17
  "interleaved-thinking-2025-05-14",
18
18
  "prompt-caching-scope-2026-01-05",
19
- ].join(",");
19
+ ];
20
20
  const ANTHROPIC_OAUTH_BETA = [
21
21
  "claude-code-20250219",
22
22
  "oauth-2025-04-20",
23
23
  "interleaved-thinking-2025-05-14",
24
24
  "context-management-2025-06-27",
25
25
  "prompt-caching-scope-2026-01-05",
26
- ].join(",");
26
+ ];
27
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
28
+
29
+ /** The interleaved-thinking beta drives BUDGET-based thinking+tools. Adaptive-display models
30
+ * (Opus 4.7+) use adaptive thinking and DON'T need it — gjc drops it for these so the legacy
31
+ * beta doesn't shadow the adaptive transport. */
32
+ function anthropicBetaHeader(betas: string[], model: string): string {
33
+ const filtered = supportsAdaptiveThinkingDisplay(model)
34
+ ? betas.filter(b => b !== INTERLEAVED_THINKING_BETA)
35
+ : betas;
36
+ return filtered.join(",");
37
+ }
27
38
 
28
39
  interface AnthropicSystemBlock {
29
40
  type: "text";
@@ -94,6 +105,51 @@ function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"], maxToke
94
105
  return Math.min(budget, Math.max(1024, maxTokens - 1024));
95
106
  }
96
107
 
108
+ /** Parse an Anthropic model id's family + version for thinking-transport selection.
109
+ * Matches the modern `claude-<family>-<major>-<minor>[...]` naming (opus/sonnet/haiku 4.x+);
110
+ * legacy ids (claude-3-5-sonnet) and non-Anthropic-compatible names return undefined. */
111
+ function parseAnthropicVersion(model: string): { kind: "opus" | "sonnet" | "haiku"; major: number; minor: number } | undefined {
112
+ const m = /claude-(opus|sonnet|haiku)-(\d+)-(\d+)/.exec(model);
113
+ if (!m) return undefined;
114
+ return { kind: m[1] as "opus" | "sonnet" | "haiku", major: Number(m[2]), minor: Number(m[3]) };
115
+ }
116
+
117
+ /** Adaptive thinking `display` is supported starting with Opus 4.7. Without it, Opus 4.7/4.8
118
+ * OMIT thinking content entirely (tokens billed, signature present, but zero visible thought —
119
+ * the "reasoning doesn't show" bug). Older adaptive models (Opus 4.6, Sonnet 4.6+) reject the
120
+ * field, so it is gated to Opus ≥ 4.7. (gjc: supportsAdaptiveThinkingDisplay) */
121
+ function supportsAdaptiveThinkingDisplay(model: string): boolean {
122
+ const v = parseAnthropicVersion(model);
123
+ if (!v || v.kind !== "opus") return false;
124
+ return v.major > 4 || (v.major === 4 && v.minor >= 7);
125
+ }
126
+
127
+ /** Thinking transport for a model (gjc parity — inferThinkingControlMode):
128
+ * - Anthropic ≥ 4.6 → "adaptive" (model decides depth; effort rides output_config, NO budget)
129
+ * - Anthropic 4.5 → "budget-effort" (budget_tokens + output_config effort)
130
+ * - otherwise → "budget" (budget_tokens only).
131
+ * The adaptive shift is the core opus-4.7/4.8 reasoning fix: those models reject the legacy
132
+ * budget transport's visible-thought contract and require type:"adaptive" + display:summarized. */
133
+ type AnthropicThinkingMode = "adaptive" | "budget-effort" | "budget";
134
+ function anthropicThinkingMode(model: string): AnthropicThinkingMode {
135
+ const v = parseAnthropicVersion(model);
136
+ if (!v) return "budget";
137
+ if (v.major > 4 || (v.major === 4 && v.minor >= 6)) return "adaptive";
138
+ if (v.major === 4 && v.minor === 5) return "budget-effort";
139
+ return "budget";
140
+ }
141
+
142
+ /** Map jeo's reasoning effort to Anthropic's adaptive/output_config effort literal. jeo folds
143
+ * xhigh→high upstream, so only minimal/low/medium/high arrive here. (gjc: mapEffortToAnthropicAdaptiveEffort) */
144
+ function anthropicAdaptiveEffort(effort: NonNullable<CallOptions["reasoningEffort"]>): "low" | "medium" | "high" {
145
+ switch (effort) {
146
+ case "minimal":
147
+ case "low": return "low";
148
+ case "medium": return "medium";
149
+ case "high": return "high";
150
+ }
151
+ }
152
+
97
153
  type AnthropicContentBlock = Record<string, unknown>;
98
154
  type AnthropicMessage = { role: string; content: string | AnthropicContentBlock[] };
99
155
 
@@ -160,10 +216,17 @@ export function anthropicPayload(
160
216
  const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
161
217
  // Image attachments + native tool/thinking-block reconstruction live in buildAnthropicMessages.
162
218
  const maxTokens = options.maxTokens ?? 4000;
163
- const thinkingBudget = anthropicThinkingBudget(options.reasoningEffort, maxTokens);
219
+ const effort = options.reasoningEffort;
220
+ const thinkingEnabled = effort !== undefined;
221
+ // gjc parity: pick the thinking transport per model. Adaptive (Opus/Sonnet 4.6+) carries NO
222
+ // budget_tokens — depth rides output_config.effort. budget/budget-effort still use a budget.
223
+ const thinkingMode = thinkingEnabled ? anthropicThinkingMode(model) : "budget";
224
+ const thinkingBudget = thinkingEnabled && thinkingMode !== "adaptive"
225
+ ? anthropicThinkingBudget(effort, maxTokens)
226
+ : undefined;
164
227
  // Reconstruct native tool_use / tool_result / thinking blocks for same-model turns when
165
228
  // thinking is enabled (and not stripped by a fail-safe retry); else plain string/image.
166
- const anthropicMessages = buildAnthropicMessages(messages, options.model, thinkingBudget !== undefined && !stripArtifacts);
229
+ const anthropicMessages = buildAnthropicMessages(messages, options.model, thinkingEnabled && !stripArtifacts);
167
230
  // Conversation prompt caching (gjc parity — the main same-model latency gap):
168
231
  // one breakpoint on the LAST message caches the entire conversation prefix, so
169
232
  // each agent-loop step only pays input processing for the new tail instead of
@@ -187,12 +250,24 @@ export function anthropicPayload(
187
250
  max_tokens: thinkingBudget !== undefined ? Math.max(maxTokens, thinkingBudget + 1024) : maxTokens,
188
251
  };
189
252
  if (credential.kind === "oauth") payload.metadata = { user_id: createClaudeCloakingUserId() };
190
- if (thinkingBudget !== undefined) {
191
- // Apply the thinking level: enable Claude extended thinking (the interleaved-thinking
192
- // beta is already in the headers). Extended thinking forbids a custom temperature, so
193
- // temperature is only set on the non-thinking path. Previously the thinking level only
194
- // changed max_tokens and never reached Claude as actual reasoning depth.
195
- payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
253
+ if (effort !== undefined) {
254
+ // Enable Claude extended thinking. Extended thinking forbids a custom temperature, so
255
+ // temperature is only set on the non-thinking path.
256
+ if (thinkingMode === "adaptive") {
257
+ // Opus/Sonnet 4.6+: the model decides how much to think. `display: "summarized"` is
258
+ // REQUIRED on Opus 4.7+ or thinking content is omitted from the response (the empty-thought
259
+ // bug); older adaptive models (4.6) reject the field, so it is gated. Effort rides
260
+ // output_config — there is no budget_tokens on this transport.
261
+ payload.thinking = supportsAdaptiveThinkingDisplay(model)
262
+ ? { type: "adaptive", display: "summarized" }
263
+ : { type: "adaptive" };
264
+ payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
265
+ } else {
266
+ // Budget-based extended thinking. `display: "summarized"` keeps human-readable thought
267
+ // streaming. The 4.5 (budget-effort) transport also carries an output_config effort.
268
+ payload.thinking = { type: "enabled", budget_tokens: thinkingBudget, display: "summarized" };
269
+ if (thinkingMode === "budget-effort") payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
270
+ }
196
271
  } else if (includeTemperature && options.temperature !== undefined) {
197
272
  payload.temperature = options.temperature;
198
273
  }
@@ -221,7 +296,7 @@ export function anthropicRequest(
221
296
  // Anthropic-compatible providers (z.ai, MiniMax, …) accept the Messages wire
222
297
  // format at their own host; an explicit baseUrl pins `${base}/v1/messages`.
223
298
  url: options.baseUrl ? `${options.baseUrl.replace(/\/$/, "")}/v1/messages` : ANTHROPIC_URL,
224
- headers: headersFor(credential, stream),
299
+ headers: headersFor(credential, stream, stripAnthropicPrefix(options.model)),
225
300
  body: anthropicPayload(messages, options, stream, includeTemperature, credential, stripArtifacts),
226
301
  };
227
302
  }
@@ -431,10 +506,10 @@ function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${str
431
506
  }
432
507
  }
433
508
 
434
- function claudeCodeOAuthHeaders(stream: boolean): Record<string, string> {
509
+ function claudeCodeOAuthHeaders(stream: boolean, model: string): Record<string, string> {
435
510
  return {
436
511
  accept: stream ? "text/event-stream" : "application/json",
437
- "anthropic-beta": ANTHROPIC_OAUTH_BETA,
512
+ "anthropic-beta": anthropicBetaHeader(ANTHROPIC_OAUTH_BETA, model),
438
513
  "anthropic-dangerous-direct-browser-access": "true",
439
514
  "user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
440
515
  "x-app": "cli",
@@ -449,13 +524,13 @@ function claudeCodeOAuthHeaders(stream: boolean): Record<string, string> {
449
524
  };
450
525
  }
451
526
 
452
- function headersFor(credential: Credential, stream: boolean): Record<string, string> {
527
+ function headersFor(credential: Credential, stream: boolean, model: string): Record<string, string> {
453
528
  if (credential.kind === "oauth") {
454
529
  return {
455
530
  "content-type": "application/json",
456
531
  authorization: `Bearer ${credential.token}`,
457
532
  "anthropic-version": "2023-06-01",
458
- ...claudeCodeOAuthHeaders(stream),
533
+ ...claudeCodeOAuthHeaders(stream, model),
459
534
  };
460
535
  }
461
536
  if (credential.kind === "api_key") {
@@ -464,7 +539,7 @@ function headersFor(credential: Credential, stream: boolean): Record<string, str
464
539
  "content-type": "application/json",
465
540
  "x-api-key": credential.token,
466
541
  "anthropic-version": "2023-06-01",
467
- "anthropic-beta": ANTHROPIC_API_KEY_BETA,
542
+ "anthropic-beta": anthropicBetaHeader(ANTHROPIC_API_KEY_BETA, model),
468
543
  };
469
544
  }
470
545
  throw new Error("anthropic adapter requires a credential");
@@ -19,7 +19,7 @@ import { formatForgeBox } from "../tui/components/forge";
19
19
  import { interactiveOAuthLogin } from "./auth";
20
20
  import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
21
21
  import type { AuthProvider } from "../auth";
22
- import { matchSlash, isSlashAttempt, suggestSlashCommands, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
22
+ import { matchSlash, isSlashAttempt, suggestSlashCommands, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, allTriggerTokens, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
23
23
  import { staticCompletionContext, readlineCompleter, formatCompletionPreview, formatMidTurnHint, tokenize, type CompletionContext } from "../tui/components/autocomplete";
24
24
  import { normalizeBaseUrl } from "./setup-helpers";
25
25
  import { EVOLUTION_STAGES, animateAsciiArt } from "../tui/components/ascii-art";
@@ -62,7 +62,7 @@ import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } fro
62
62
  import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
63
63
  import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
64
64
  import { categoryBadge } from "../tui/components/category-index";
65
- import { renderInputFrame, verticalCursorOffset } from "../tui/components/input-box";
65
+ import { renderInputFrame, verticalCursorOffset, type HighlightRange } from "../tui/components/input-box";
66
66
 
67
67
  import { renderStatusBar } from "../tui/components/status";
68
68
  import { detectColorLevel, ColorLevel, visibleWidth } from "../tui/components/color";
@@ -1803,15 +1803,26 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1803
1803
  const TRIGGER_HL_UNKNOWN = "#ff6b81";
1804
1804
  const triggerHighlight = (
1805
1805
  rendered: string,
1806
- ): { start: number; end: number; paint: (s: string) => string } | undefined => {
1807
- if (!uiTheme.color) return undefined;
1808
- const trigger = activeTriggerToken(rendered);
1809
- if (!trigger) return undefined;
1810
- const start = Array.from(rendered.slice(0, trigger.start)).length;
1811
- const end = start + Array.from(trigger.token).length;
1812
- const valid = slashPreviewMatches(rendered, skillSlashDetails, resolvedSkills).length > 0;
1813
- const hex = valid ? TRIGGER_HL_VALID : TRIGGER_HL_UNKNOWN;
1814
- return { start, end, paint: (s: string) => chalk.hex(hex)(s) };
1806
+ ): HighlightRange[] => {
1807
+ if (!uiTheme.color) return [];
1808
+ // Highlight EVERY `/command`·`$skill` invocation in the line at once and
1809
+ // independent of caret position, so multiple triggers all stay lit and a
1810
+ // token keeps its color even when the caret jumps elsewhere to edit.
1811
+ const out: HighlightRange[] = [];
1812
+ for (const trigger of allTriggerTokens(rendered)) {
1813
+ const start = Array.from(rendered.slice(0, trigger.start)).length;
1814
+ const end = start + Array.from(trigger.token).length;
1815
+ // "Open" = the word still being typed: it reaches the end of the line with
1816
+ // no space after it. An open token counts as valid-so-far on any match
1817
+ // (incl. fuzzy prefix); every committed token must be an EXACT known
1818
+ // command/skill to stay green, else it shows caution pink (likely typo).
1819
+ const isOpen = trigger.start + trigger.token.length === rendered.length;
1820
+ const matches = slashPreviewMatches(trigger.token, skillSlashDetails, resolvedSkills);
1821
+ const valid = isOpen ? matches.length > 0 : matches.includes(trigger.token);
1822
+ const hex = valid ? TRIGGER_HL_VALID : TRIGGER_HL_UNKNOWN;
1823
+ out.push({ start, end, paint: (s: string) => chalk.hex(hex)(s) });
1824
+ }
1825
+ return out;
1815
1826
  };
1816
1827
  const refreshUiTheme = (): void => {
1817
1828
  uiTheme = resolveTheme(process.env);
package/src/tui/app.ts CHANGED
@@ -37,10 +37,18 @@ import { formatHintBar } from "./components/hints";
37
37
  import { formatDuration, formatUsage } from "./components/duration";
38
38
  import { renderHud, type JeoPhase } from "./components/hud";
39
39
  import { formatTodoWriteCard } from "./components/todo-card";
40
- import { renderInputBox } from "./components/input-box";
40
+ import { renderInputBox, type HighlightRange } from "./components/input-box";
41
41
  import { jeoEnv } from "../util/env";
42
42
  import chalk from "chalk";
43
43
 
44
+ /** Stable signature of a highlight range list — offsets plus the painted color
45
+ * (probed with a sentinel char) — so equal-length but differently-colored
46
+ * re-highlights (valid↔unknown at the same span) still trigger a redraw. */
47
+ function highlightSignature(hl?: readonly HighlightRange[]): string {
48
+ if (!hl || hl.length === 0) return "";
49
+ return hl.map(r => `${r.start}:${r.end}:${r.paint("\u0000")}`).join("|");
50
+ }
51
+
44
52
  export interface LaunchTuiOptions {
45
53
  model: string;
46
54
  /** Resolved provider name for the footer (anthropic / openai / gemini / ollama). */
@@ -668,15 +676,15 @@ export class LaunchTui {
668
676
  this.draw();
669
677
  }
670
678
 
671
- private livePromptHighlight?: { start: number; end: number; paint: (s: string) => string };
672
- /** Recolor the active `/command`·`$skill` trigger token inside the mid-turn live
673
- * input box (idle-prompt parity). Caller supplies code-point offsets into the
674
- * draft text + a painter; undefined clears it. */
675
- setLivePromptHighlight(hl?: { start: number; end: number; paint: (s: string) => string }): void {
679
+ private livePromptHighlight?: readonly HighlightRange[];
680
+ /** Recolor every active/committed `/command`·`$skill` trigger token inside the
681
+ * mid-turn live input box (idle-prompt parity). Caller supplies code-point
682
+ * offsets into the draft text + a painter per token; undefined/empty clears. */
683
+ setLivePromptHighlight(hl?: readonly HighlightRange[]): void {
676
684
  if (this.finished) return;
677
- const a = this.livePromptHighlight, b = hl;
678
- if (a?.start === b?.start && a?.end === b?.end && (!a) === (!b)) return;
679
- this.livePromptHighlight = hl;
685
+ const next = hl && hl.length ? hl : undefined;
686
+ if (highlightSignature(this.livePromptHighlight) === highlightSignature(next)) return;
687
+ this.livePromptHighlight = next;
680
688
  this.draw();
681
689
  }
682
690
 
@@ -18,11 +18,20 @@ export interface InputBoxOptions {
18
18
  /** Shadow painter for the bottom/right "shaded" edges; defaults to a dim accent.
19
19
  * The lit-vs-shaded two-tone contrast gives the box visible depth. */
20
20
  accentShadow?: (s: string) => string;
21
- /** Paint a contiguous CHARACTER range of the typed text (e.g. the active
22
- * `/command` or `$skill` trigger token) so the user sees the invocation is
23
- * recognized as it is typed. Offsets index `Array.from(line)` code points
24
- * ([start, end)). Ignored for the placeholder and when `color` is false. */
25
- highlight?: { start: number; end: number; paint: (s: string) => string };
21
+ /** Paint contiguous CHARACTER ranges of the typed text (e.g. each active or
22
+ * committed `/command`/`$skill` trigger token) so the user sees every
23
+ * invocation recognized as it is typed regardless of caret position or how
24
+ * many appear. Offsets index `Array.from(line)` code points ([start, end)).
25
+ * Accepts a single range or an array; ranges should not overlap. Ignored for
26
+ * the placeholder and when `color` is false. */
27
+ highlight?: HighlightRange | readonly HighlightRange[];
28
+ }
29
+
30
+ /** A painted span of the input text: [start, end) code-point offsets + a painter. */
31
+ export interface HighlightRange {
32
+ start: number;
33
+ end: number;
34
+ paint: (s: string) => string;
26
35
  }
27
36
 
28
37
  export interface InputFrame {
@@ -43,7 +52,7 @@ function wrapWithCursor(
43
52
  text: string,
44
53
  cursor: number,
45
54
  width: number,
46
- highlight?: { start: number; end: number; paint: (s: string) => string },
55
+ highlights?: readonly HighlightRange[],
47
56
  ): { rows: string[]; row: number; col: number } {
48
57
  const rows: string[] = [];
49
58
  let cur = "";
@@ -73,8 +82,8 @@ function wrapWithCursor(
73
82
  continue;
74
83
  }
75
84
  if (ch !== "") {
76
- const lit = highlight && i >= highlight.start && i < highlight.end;
77
- cur += lit ? highlight.paint(rendered) : rendered;
85
+ const hl = highlights?.find(r => i >= r.start && i < r.end);
86
+ cur += hl ? hl.paint(rendered) : rendered;
78
87
  curW += w;
79
88
  }
80
89
  }
@@ -82,6 +91,16 @@ function wrapWithCursor(
82
91
  return { rows, row, col };
83
92
  }
84
93
 
94
+ /** Normalize the `highlight` option (single range, array, or absent) into a
95
+ * non-empty range array, or undefined when there is nothing to paint. */
96
+ function normalizeHighlights(
97
+ h?: HighlightRange | readonly HighlightRange[],
98
+ ): readonly HighlightRange[] | undefined {
99
+ if (!h) return undefined;
100
+ const arr = Array.isArray(h) ? h : [h as HighlightRange];
101
+ return arr.length ? arr : undefined;
102
+ }
103
+
85
104
  /**
86
105
  * Boxed input prompt (gjc-style): a `>` marker leads the first body row, the typed
87
106
  * text (or a dim placeholder) follows, and the caret cell is reported so the caller
@@ -102,7 +121,8 @@ export function renderInputFrame(line: string, opts: InputBoxOptions = {}): Inpu
102
121
  rows = [placeholder];
103
122
  placeholderRow = true;
104
123
  } else {
105
- const wrapped = wrapWithCursor(line, opts.cursor ?? line.length, textWidth, useColor ? opts.highlight : undefined);
124
+ const hl = useColor ? normalizeHighlights(opts.highlight) : undefined;
125
+ const wrapped = wrapWithCursor(line, opts.cursor ?? line.length, textWidth, hl);
106
126
  rows = wrapped.rows;
107
127
  crow = wrapped.row;
108
128
  ccol = wrapped.col;
@@ -216,6 +216,42 @@ export function activeTriggerToken(line: string): ActiveTrigger | undefined {
216
216
  return { kind: token[0] as "/" | "$", token, start: (m.index ?? 0) + m[1]!.length };
217
217
  }
218
218
 
219
+ /**
220
+ * The LEADING `/command` or `$skill` keyword once it has been committed with a
221
+ * trailing space — `"/model gpt-4"` → `/model`, `"$test the bug"` → `$test`.
222
+ * Unlike {@link activeTriggerToken} (which only matches the word the caret still
223
+ * sits on) this keeps the invoked keyword recognizable while arguments are typed,
224
+ * so the trigger highlight persists after the space instead of vanishing. Only
225
+ * the leading word counts — a command is invoked at the start of the line — and a
226
+ * still-being-typed keyword (no space yet) returns undefined so the active-token
227
+ * path owns it. Returns the same shape as {@link activeTriggerToken}.
228
+ */
229
+ export function committedTriggerToken(line: string): ActiveTrigger | undefined {
230
+ const m = /^(\s*)([/$]\S+)\s/.exec(line);
231
+ if (!m) return undefined;
232
+ const token = m[2]!;
233
+ return { kind: token[0] as "/" | "$", token, start: Array.from(m[1]!).length };
234
+ }
235
+
236
+ /**
237
+ * EVERY `/command` or `$skill` trigger token in the line (mention-style), in
238
+ * left-to-right order — `"/model x then $test y"` → [`/model`, `$test`]. Each
239
+ * is a whitespace-delimited word whose first char is `/`·`$` (paths like
240
+ * `src/cli` and vars like `FOO$BAR` stay excluded, just like the single-token
241
+ * helpers). `start` is the token's first-character index in `line`. Used to
242
+ * highlight all invocations at once, independent of caret position. Pure.
243
+ */
244
+ export function allTriggerTokens(line: string): ActiveTrigger[] {
245
+ const out: ActiveTrigger[] = [];
246
+ const re = /(^|\s)([/$]\S*)/g;
247
+ let m: RegExpExecArray | null;
248
+ while ((m = re.exec(line))) {
249
+ const token = m[2]!;
250
+ out.push({ kind: token[0] as "/" | "$", token, start: m.index + m[1]!.length });
251
+ }
252
+ return out;
253
+ }
254
+
219
255
  /**
220
256
  * Compact live preview shown beneath the input box while a `/command` or
221
257
  * `$skill` keyword is being typed — at any position in the line (mention-style,