jeo-code 0.6.31 → 0.6.33
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 +26 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/engine.ts +9 -1
- package/src/ai/model-catalog.ts +4 -5
- package/src/ai/providers/anthropic.ts +91 -16
- package/src/commands/launch.ts +26 -15
- package/src/prompts/agents/executor.md +1 -0
- package/src/tui/app.ts +22 -15
- package/src/tui/components/ascii-art.ts +63 -37
- package/src/tui/components/forge.ts +9 -0
- package/src/tui/components/input-box.ts +29 -9
- package/src/tui/components/slash.ts +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ 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.33] - 2026-06-19
|
|
10
|
+
_A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification._
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The forge mark is a hollow line-board emblem now.** `FORGE_MARK_ART`, its grand `FORGE_MARK_ART_GRAND` hero variant, the claw-snap blink frames (`FORGE_MARK_FRAMES`), and every ASCII fallback (`*_ASCII`) were redrawn as the `>-<` silhouette of two pincer CLAWS (집게) whose top arms bend inward toward a narrow central eyeglass-frame (안경태) BRIDGE — each stroke a thick rounded-corner tube (`╭╮╰╯` + `─│`, ASCII `.-'|`) so the shape reads as a heavy neon outline instead of a filled block. The old wordmark glyphs (`J E O`) and the `╳` DNA double-helix nodes are gone; every line stays width-1 and equal-width so the blue→violet→pink flow gradient and the padding/centering math are untouched.
|
|
14
|
+
- **Forge cards render at a compact reduced width.** New `FORGE_SCALE` (1.2) + `scaleForgeWidth(available)` in `forge.ts` divide the caller's available column run down to a compact panel width (floored at 24). `app.ts` routes both the inline `flushForgeCard` and the static forge summary through it, so a card reads as a contained panel instead of stretching edge-to-edge with a dead right margin.
|
|
15
|
+
- **One source for the done-time verification directive.** New `VERIFICATION_DIRECTIVE` constant in `engine.ts` replaces the string that was duplicated verbatim in `executorSystemPrompt`'s default and `launch.ts`'s interactive prompt. It folds in gjc's `<verification>` test-quality contract — written tests must exercise observable behavior, edge values, branch conditions, invariants, and error handling, never asserting defaults or tautologies — and `prompts/agents/executor.md` gains a matching constraint line.
|
|
16
|
+
|
|
17
|
+
### Verified
|
|
18
|
+
- **No bun memory leak / slowdown.** `scripts/mem-probe.ts` drove 2000 realistic LaunchTui turns: post-GC heap returns to baseline (per-turn slope **−480 bytes/turn**, net +3.28 MB held flat), with `exit`/`resize`/`SIGINT` process-listener counts stable (no accumulation).
|
|
19
|
+
- **`jeo --tmux` live.** `scripts/tmux-verify.sh smoke` OK and `battery` **6/6 PASSED** (boot, `/help`, unknown `$skill` feedback, `/agents` roster, `$ultragoal` dispatch, unresolved `/command` report).
|
|
20
|
+
- **Green gates.** `bun run typecheck` clean; `bun test` **1710 pass / 0 fail** across 211 files.
|
|
21
|
+
|
|
22
|
+
## [0.6.32] - 2026-06-19
|
|
23
|
+
_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._
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
31
|
+
### Verified
|
|
32
|
+
- **`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.
|
|
33
|
+
- **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).
|
|
34
|
+
|
|
9
35
|
## [0.6.31] - 2026-06-19
|
|
10
36
|
_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
37
|
|
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.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
|
+
- **[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
205
|
- **[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
206
|
- **[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
207
|
- **[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
|
-
- **[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.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
|
+
- **[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
205
|
- **[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
206
|
- **[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
207
|
- **[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
|
-
- **[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.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
|
+
- **[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
205
|
- **[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
206
|
- **[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
207
|
- **[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
|
-
- **[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.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
|
+
- **[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
205
|
- **[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
206
|
- **[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
207
|
- **[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
|
-
- **[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
package/src/agent/engine.ts
CHANGED
|
@@ -164,10 +164,18 @@ export const OUTPUT_DISCIPLINE = [
|
|
|
164
164
|
"- Match reply length to the task: a one-line change gets a one-line report.",
|
|
165
165
|
].join("\n");
|
|
166
166
|
|
|
167
|
+
/** gjc-inherited verification directive (plan/gjc-inheritance.md, round 16): the
|
|
168
|
+
* done self-check PLUS gjc's `<verification>` test-quality contract — what makes a
|
|
169
|
+
* test worth writing. Single source consumed by both executorSystemPrompt's default
|
|
170
|
+
* and launch.ts's interactive prompt (was duplicated verbatim in both). */
|
|
171
|
+
export const VERIFICATION_DIRECTIVE =
|
|
172
|
+
"Before calling done, self-check: did I run the test or command that exercises this change, are directly-affected callsites/tests/docs updated, and does my claim match real output? If any answer is no, keep working — do not call done. " +
|
|
173
|
+
"When you write tests, exercise observable behavior, edge values, branch conditions, invariants, and error handling — never assert defaults or tautologies.";
|
|
174
|
+
|
|
167
175
|
export function executorSystemPrompt(
|
|
168
176
|
role = "Executor Agent, a senior software developer",
|
|
169
177
|
protocol: string = TOOL_PROTOCOL,
|
|
170
|
-
verificationDirective =
|
|
178
|
+
verificationDirective = VERIFICATION_DIRECTIVE,
|
|
171
179
|
): string {
|
|
172
180
|
return (
|
|
173
181
|
`You are the ${role}.\n` +
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -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
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
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
|
-
]
|
|
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
|
-
]
|
|
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
|
|
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,
|
|
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 (
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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");
|
package/src/commands/launch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
|
-
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, type AgentLoopEvents } from "../agent/engine";
|
|
4
|
+
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, VERIFICATION_DIRECTIVE, type AgentLoopEvents } from "../agent/engine";
|
|
5
5
|
import { createOpikTracer, wrapEvents } from "../agent/opik-tracer";
|
|
6
6
|
import { initialDynamicStepLimit } from "../agent/step-budget";
|
|
7
7
|
import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
|
|
@@ -15,11 +15,11 @@ import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
|
15
15
|
import { runTeamEngine, type TeamEngineOptions } from "./team";
|
|
16
16
|
import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
17
17
|
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
18
|
-
import { formatForgeBox } from "../tui/components/forge";
|
|
18
|
+
import { formatForgeBox, scaleForgeWidth } 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";
|
|
@@ -470,7 +470,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
470
470
|
preamble + "\n\n" + protocol + "\n\n" +
|
|
471
471
|
WORKING_DISCIPLINE + "\n\n" +
|
|
472
472
|
OUTPUT_DISCIPLINE + "\n\n" +
|
|
473
|
-
|
|
473
|
+
VERIFICATION_DIRECTIVE +
|
|
474
474
|
"\nWhen you have finished the user's request, or need to reply to or ask the user something, call done with {\"reason\": <your natural-language reply to the user>}. The reason text is shown to the user as your message." +
|
|
475
475
|
(allowedTools.has("task") ? "\n\nDelegation: " + taskToolProtocolLine(cfg) +
|
|
476
476
|
" Call task with {\"role\": <one of the advertised roles>, \"task\": <assignment>, \"context\": <optional>} to hand a focused slice to a subagent." : "") +
|
|
@@ -1194,7 +1194,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1194
1194
|
{
|
|
1195
1195
|
const card = formatForgeBox(
|
|
1196
1196
|
{ title: "[skill]", lines: skillInvocationCard(skill, intent) },
|
|
1197
|
-
{ width: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
1197
|
+
{ width: scaleForgeWidth(Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2))), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
1198
1198
|
);
|
|
1199
1199
|
logLines(card);
|
|
1200
1200
|
}
|
|
@@ -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
|
-
):
|
|
1807
|
-
if (!uiTheme.color) return
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
const
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
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);
|
|
@@ -10,6 +10,7 @@ Turn a bounded task into a working, verified outcome with the smallest correct c
|
|
|
10
10
|
- Keep diffs small and aligned to existing patterns.
|
|
11
11
|
- Do not broaden scope or invent abstractions unless the task requires them.
|
|
12
12
|
- Verify the task before calling done.
|
|
13
|
+
- When you add tests, exercise observable behavior, edge values, branch conditions, invariants, and error handling — never assert defaults or tautologies.
|
|
13
14
|
- Communicate the result through `done.reason` using the required output contract.
|
|
14
15
|
</constraints>
|
|
15
16
|
|
package/src/tui/app.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./comp
|
|
|
24
24
|
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
25
25
|
import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint } from "./components/themes";
|
|
26
26
|
import { detectColorLevel, animatedGradientText, ColorLevel } from "./components/color";
|
|
27
|
-
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, type ForgeSummary } from "./components/forge";
|
|
27
|
+
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, scaleForgeWidth, type ForgeSummary } from "./components/forge";
|
|
28
28
|
import { renderStatusBar, renderStatusBox, type StatusBoxData } from "./components/status";
|
|
29
29
|
import { costForUsage } from "../ai/pricing";
|
|
30
30
|
import { renderMarkdownTables } from "./components/markdown-table";
|
|
@@ -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?:
|
|
672
|
-
/** Recolor
|
|
673
|
-
* input box (idle-prompt parity). Caller supplies code-point
|
|
674
|
-
* draft text + a painter; undefined clears
|
|
675
|
-
setLivePromptHighlight(hl?:
|
|
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
|
|
678
|
-
if (
|
|
679
|
-
this.livePromptHighlight =
|
|
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
|
|
|
@@ -1198,7 +1206,7 @@ export class LaunchTui {
|
|
|
1198
1206
|
* Non-inline modes keep the card in `forgeSummaries` for the final static summary. */
|
|
1199
1207
|
private flushForgeCard(summary: ForgeSummary, success?: boolean): void {
|
|
1200
1208
|
if (!this.inline || this.finished) return;
|
|
1201
|
-
const width =
|
|
1209
|
+
const width = scaleForgeWidth(size().cols - 1);
|
|
1202
1210
|
// gjc D2 (state-encoded border): a FAILED card gets a red border so it pops
|
|
1203
1211
|
// out of scrollback at a glance; OK/neutral cards keep the theme accent
|
|
1204
1212
|
// identity. The ✓/✗ title mark already encodes state, but the border tone
|
|
@@ -1227,10 +1235,9 @@ export class LaunchTui {
|
|
|
1227
1235
|
anim?: { phase: number; colorLevel: ColorLevel; beat: string },
|
|
1228
1236
|
dim = false,
|
|
1229
1237
|
): string[] {
|
|
1230
|
-
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
const boxWidth = Math.max(floor, width);
|
|
1238
|
+
// Forge cards render at a reduced (÷FORGE_SCALE) compact width rather than
|
|
1239
|
+
// spanning the full available column run.
|
|
1240
|
+
const boxWidth = scaleForgeWidth(width);
|
|
1234
1241
|
const paint = this.theme.color ? accentPaint(this.theme) : (s: string) => s;
|
|
1235
1242
|
const lines: string[] = [];
|
|
1236
1243
|
for (const [i, summary] of this.forgeSummaries.slice(-maxEntries).entries()) {
|
|
@@ -412,46 +412,61 @@ export async function animateFrames(stage: AsciiStage, opts: AnimateFramesOption
|
|
|
412
412
|
}
|
|
413
413
|
return total;
|
|
414
414
|
}
|
|
415
|
-
/** The compact jeo forge mark: a symmetrical crayfish (가재) brand emblem
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
415
|
+
/** The compact jeo forge mark: a symmetrical crayfish (가재) brand emblem drawn as a
|
|
416
|
+
* 2.5×-thick LINE-BOARD (hollow outline, no fill) — the `>-<` silhouette of two pincer
|
|
417
|
+
* CLAWS (집게) whose top arms bend (꺾임) inward toward a narrow central BRIDGE (the
|
|
418
|
+
* eyeglass-frame / 안경태 bar). Every stroke is rendered as a thick rounded-corner tube
|
|
419
|
+
* (╭╮╰╯ + ─│) so the shape reads as a heavy neon outline, not a filled block. No letters.
|
|
420
|
+
* Width-1 glyphs only (box drawing) so padding/centering math stays exact, and the
|
|
421
|
+
* blue→violet→pink flow gradient from renderForgeMark supplies the neon glow. Frame 0 is
|
|
422
|
+
* the static symbol. */
|
|
423
423
|
export const FORGE_MARK_ART: string[] = [
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
424
|
+
"╭─────╮ ╭─────╮",
|
|
425
|
+
"│ │ │ │",
|
|
426
|
+
"╰─╮ │ ╭─────────╮ │ ╭─╯",
|
|
427
|
+
" │ │ │ │ │ │ ",
|
|
428
|
+
"╭─╯ ╭─╯ ╰─────────╯ ╰─╮ ╰─╮",
|
|
429
|
+
"│ │ │ │",
|
|
430
|
+
"╰───╯ ╰───╯"
|
|
427
431
|
];
|
|
428
432
|
|
|
429
433
|
export const FORGE_MARK_ART_ASCII: string[] = [
|
|
430
|
-
"
|
|
431
|
-
"
|
|
432
|
-
"
|
|
434
|
+
".-----. .-----.",
|
|
435
|
+
"| | | |",
|
|
436
|
+
"'-. | .---------. | .-'",
|
|
437
|
+
" | | | | | | ",
|
|
438
|
+
".-' .-' '---------' '-. '-.",
|
|
439
|
+
"| | | |",
|
|
440
|
+
"'---' '---'"
|
|
433
441
|
];
|
|
434
442
|
|
|
435
|
-
/** Claw-snap blink frames for the compact lobster forge mark: the
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the static
|
|
443
|
+
/** Claw-snap blink frames for the compact lobster forge mark: the central bridge stays
|
|
444
|
+
* fixed while the four pincer arms snap (open → clenched), so the lobster "clicks" its
|
|
445
|
+
* claws. Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the static
|
|
439
446
|
* symbol. All lines share the same width and width-1 glyphs. */
|
|
440
447
|
export const FORGE_MARK_FRAMES: string[][] = [
|
|
441
448
|
FORGE_MARK_ART,
|
|
442
449
|
[
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
450
|
+
"╭───╮ ╭───╮",
|
|
451
|
+
"│ │ │ │",
|
|
452
|
+
"╰─╮ ╰─╮ ╭─────────╮ ╭─╯ ╭─╯",
|
|
453
|
+
" │ │ │ │ │ │ ",
|
|
454
|
+
"╭─╯ │ ╰─────────╯ │ ╰─╮",
|
|
455
|
+
"│ │ │ │",
|
|
456
|
+
"╰─────╯ ╰─────╯"
|
|
446
457
|
]
|
|
447
458
|
];
|
|
448
459
|
|
|
449
460
|
export const FORGE_MARK_FRAMES_ASCII: string[][] = [
|
|
450
461
|
FORGE_MARK_ART_ASCII,
|
|
451
462
|
[
|
|
452
|
-
"
|
|
453
|
-
"
|
|
454
|
-
"
|
|
463
|
+
".---. .---.",
|
|
464
|
+
"| | | |",
|
|
465
|
+
"'-. '-. .---------. .-' .-'",
|
|
466
|
+
" | | | | | | ",
|
|
467
|
+
".-' | '---------' | '-.",
|
|
468
|
+
"| | | |",
|
|
469
|
+
"'-----' '-----'"
|
|
455
470
|
]
|
|
456
471
|
];
|
|
457
472
|
|
|
@@ -461,23 +476,34 @@ export function forgeMarkFrameCount(): number {
|
|
|
461
476
|
}
|
|
462
477
|
|
|
463
478
|
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the same
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* symmetry, with renderForgeMark's blue→violet→pink flow gradient supplying the neon glow.
|
|
469
|
-
* Width 29 (matches the welcome compact↔grand threshold) and width-1 glyphs only so
|
|
470
|
-
* padding/centering math stays exact. */
|
|
479
|
+
* crayfish line-board emblem rendered large — the pincer claws and central eyeglass
|
|
480
|
+
* bridge drawn as wider 3×-thick rounded outline tubes (╭╮╰╯ + ─│), no fill, no letters.
|
|
481
|
+
* Width-1 glyphs only so padding/centering math stays exact, with renderForgeMark's
|
|
482
|
+
* blue→violet→pink flow gradient supplying the neon glow. */
|
|
471
483
|
export const FORGE_MARK_ART_GRAND: string[] = [
|
|
472
|
-
"
|
|
473
|
-
"
|
|
474
|
-
"
|
|
484
|
+
"╭────────╮ ╭────────╮",
|
|
485
|
+
"│ │ │ │",
|
|
486
|
+
"│ │ │ │",
|
|
487
|
+
"╰──╮ │ ╭──────────────╮ │ ╭──╯",
|
|
488
|
+
" │ │ │ │ │ │ ",
|
|
489
|
+
" │ │ │ │ │ │ ",
|
|
490
|
+
"╭──╯ ╭──╯ ╰──────────────╯ ╰──╮ ╰──╮",
|
|
491
|
+
"│ │ │ │",
|
|
492
|
+
"│ │ │ │",
|
|
493
|
+
"╰─────╯ ╰─────╯"
|
|
475
494
|
];
|
|
476
495
|
|
|
477
496
|
export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
"
|
|
497
|
+
".--------. .--------.",
|
|
498
|
+
"| | | |",
|
|
499
|
+
"| | | |",
|
|
500
|
+
"'--. | .--------------. | .--'",
|
|
501
|
+
" | | | | | | ",
|
|
502
|
+
" | | | | | | ",
|
|
503
|
+
".--' .--' '--------------' '--. '--.",
|
|
504
|
+
"| | | |",
|
|
505
|
+
"| | | |",
|
|
506
|
+
"'-----' '-----'"
|
|
481
507
|
];
|
|
482
508
|
|
|
483
509
|
// Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
|
|
@@ -479,6 +479,15 @@ export function fitForgeBoxes(lines: string[], budget: number): string[] {
|
|
|
479
479
|
return out;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/** Forge cards render at a reduced scale: the available width is divided by this
|
|
483
|
+
* factor so a box reads as a compact panel instead of spanning the full terminal. */
|
|
484
|
+
export const FORGE_SCALE = 1.2;
|
|
485
|
+
|
|
486
|
+
/** Scale a caller's available width down to the forge card's compact render width. */
|
|
487
|
+
export function scaleForgeWidth(available: number): number {
|
|
488
|
+
return Math.max(24, Math.trunc(available / FORGE_SCALE));
|
|
489
|
+
}
|
|
490
|
+
|
|
482
491
|
export function formatForgeBox(summary: ForgeSummary, opts: ForgeBoxOptions = {}): string[] {
|
|
483
492
|
const innerWidth = opts.width ?? 80;
|
|
484
493
|
const floor = Math.min(24, innerWidth);
|
|
@@ -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
|
|
22
|
-
* `/command
|
|
23
|
-
* recognized as it is typed
|
|
24
|
-
*
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
cur +=
|
|
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
|
|
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,
|