jeo-code 0.7.0 → 0.7.2
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 +34 -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/seed.ts +24 -0
- package/src/ai/model-catalog.ts +18 -0
- package/src/ai/model-discovery.ts +27 -9
- package/src/ai/providers/openai-compatible-catalog.ts +2 -2
- package/src/commands/launch/input.ts +137 -0
- package/src/commands/launch.ts +46 -71
- package/src/commands/ultragoal.ts +68 -18
- package/src/tui/app.ts +7 -4
- package/src/tui/components/ascii-art.ts +6 -2
- package/src/tui/components/themes.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ 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.7.2] - 2026-06-22
|
|
10
|
+
_Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6)._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Per-criterion `{verify: <command>}` directive for `ultragoal`.** An acceptance criterion authored with a trailing `{verify: <shell command>}` is now INDIVIDUALLY verifiable — `parseCriterion` (in `src/agent/seed.ts`) splits the directive off the criterion text (round-tripping unmangled through the seed's JSON-encoded list), and `ultragoal` runs that command and records a real PASS/FAIL in the ledger. Criteria without a directive stay UNVERIFIED on a green suite (honest by default, strengthenable on demand). The overall report now uses a status taxonomy: **FAILED** (suite red OR any checked criterion failed), **SUCCESS** (every criterion individually verified and passed), **PARTIAL** (some verified, others still unverified), and **SUITE_GREEN** (green suite, nothing individually proven). The verdict (`ok`) now reflects `status !== "FAILED"` rather than the raw suite result.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The boxed prompt's stdin→readline byte rewriter is extracted into a pure function.** `filterPromptInputChunk` (in `src/commands/launch/input.ts`) now holds the FULL keystroke wiring — bracketed-paste folding across chunks, mouse/terminal-report swallowing, Shift+Enter → hard-break SENTINEL, combo-key normalization, and the boundary-aware Up/Down box navigation — so it is testable WITHOUT a live readline/PTY. `launch.ts`'s live handler is now a thin adapter over it, eliminating the duplicated decision logic that previously diverged from its `boxVerticalNavAction` helper.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **The "↓ cuts the lower text" multi-line draft regression is now covered by direct byte-stream tests.** A new `test/prompt-key-filter.test.ts` (16 cases) drives raw escape sequences through `filterPromptInputChunk` exactly as the live `kfDataHandler` does — asserting which bytes reach readline and how the caret/paste state mutate — the closest automated stand-in for live-terminal verification of the box-navigation fix shipped in 0.7.1.
|
|
20
|
+
|
|
21
|
+
### Verified
|
|
22
|
+
- `bun run typecheck` clean; full suite **1826 pass / 0 fail** (222 files).
|
|
23
|
+
- `scripts/mem-probe.ts` (2000 turns × 40 tools): no long-term leak (exit 0; settled heap floor flat at ~4.3 MB, net +0.49 MB, exit-listeners flat at 1).
|
|
24
|
+
- `scripts/tmux-verify.sh battery`: **6/6 PASSED** on a fresh `jeo --tmux` boot (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
|
|
25
|
+
|
|
26
|
+
## [0.7.1] - 2026-06-22
|
|
27
|
+
_TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6)._
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Theme-derived forge flow palette.** `themeFlowPalette(theme)` builds a 3-stop neon palette (`accentShadow → accent → brightest stage-gradient end`) from the active TUI theme; the animated forge-card border and the forge mark now sweep the theme's own glow instead of the fixed brand `FORGE_FLOW_PALETTE`. Colorless/`mono` themes collapse to a single accent stop (the animation is already gated on color/TrueColor).
|
|
31
|
+
- **Expanded Tencent Cloud MaaS catalog.** Added live-verified model ids across DeepSeek (`deepseek-v4-pro`/`-202606`, `deepseek-v4-flash`/`-202605`, `deepseek-v3.2`), MiniMax (`minimax-m3`/`-m2.7`/`-m2.5`), and Zhipu GLM (`glm-5.2`/`5.1`/`5`). Ids were confirmed against live `/v1/messages` probes (the host exposes no `/v1/models` route).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **The boxed prompt's Up/Down keys no longer wipe a multi-line draft at its edges.** A new `boxVerticalNavAction` classifier returns `move` (a visual row exists → reposition the caret), `swallow` (no row AND a genuine multi-line draft → keep the keystroke inside the box, since falling through to readline would recall input history and erase the draft — the "↓ cuts the lower text" bug), or `history` (no row on a soft-wrapped single line → fall through so ↑/↓ recall history at the edges).
|
|
35
|
+
- **Model-picker no longer pins API-key providers to a bare default when the models-list endpoint is absent.** `catalogOr` now backfills the static capability catalog for OAuth sources *and* for API-key providers whose models endpoint returns HTTP 404 (e.g. Tencent MaaS), so the picker shows the full known model set instead of a single default.
|
|
36
|
+
- **`scripts/tmux-verify.sh smoke` no longer false-fails on the benign non-TTY attach.** The launcher-log grep was pure false-positive surface (it re-flagged the harmless `not a terminal` attach message); a real pre-handoff crash already fails via the session-start timeout and a post-handoff crash shows in the captured frame, so the rendered frame is the authoritative signal.
|
|
37
|
+
|
|
38
|
+
### Verified
|
|
39
|
+
- `bun run typecheck` clean; full suite **1806 pass / 0 fail** (221 files).
|
|
40
|
+
- `scripts/mem-probe.ts` (2000 turns × 40 tools): no long-term leak (exit 0; settled heap floor flat at ~4.3 MB, exit-listeners flat at 1).
|
|
41
|
+
- `scripts/tmux-verify.sh battery`: **6/6 PASSED** on a fresh `jeo --tmux` boot (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
|
|
42
|
+
|
|
9
43
|
## [0.7.0] - 2026-06-22
|
|
10
44
|
_Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6)._
|
|
11
45
|
|
package/README.ja.md
CHANGED
|
@@ -193,11 +193,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
193
193
|
## 変更履歴 (Changelog)
|
|
194
194
|
|
|
195
195
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
196
|
+
- **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
|
+
- **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
196
198
|
- **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
199
|
- **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
198
200
|
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
199
|
-
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
200
|
-
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
201
201
|
|
|
202
202
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
203
203
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -193,11 +193,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
193
193
|
## 변경 이력 (Changelog)
|
|
194
194
|
|
|
195
195
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
196
|
+
- **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
|
+
- **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
196
198
|
- **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
199
|
- **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
198
200
|
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
199
|
-
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
200
|
-
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
201
201
|
|
|
202
202
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
203
203
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -195,11 +195,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
195
195
|
## Changelog
|
|
196
196
|
|
|
197
197
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
198
|
+
- **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
199
|
+
- **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
198
200
|
- **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
199
201
|
- **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
200
202
|
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
201
|
-
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
202
|
-
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
203
203
|
|
|
204
204
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
205
205
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -193,11 +193,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
193
193
|
## 更新日志 (Changelog)
|
|
194
194
|
|
|
195
195
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
196
|
+
- **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
|
+
- **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
|
|
196
198
|
- **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
197
199
|
- **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
198
200
|
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
199
|
-
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
200
|
-
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
201
201
|
|
|
202
202
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
203
203
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/seed.ts
CHANGED
|
@@ -50,3 +50,27 @@ export function parseSeedList(content: string, name: string): string[] {
|
|
|
50
50
|
export function parseSeedAcceptanceCriteria(content: string): string[] {
|
|
51
51
|
return parseSeedList(content, "acceptance_criteria");
|
|
52
52
|
}
|
|
53
|
+
/** A single acceptance criterion, optionally carrying an executable check command.
|
|
54
|
+
* Authors make a criterion INDIVIDUALLY verifiable by appending a trailing
|
|
55
|
+
* `{verify: <shell command>}` directive; ultragoal runs that command and records a
|
|
56
|
+
* real PASS/FAIL instead of leaving the criterion UNVERIFIED. Criteria without a
|
|
57
|
+
* directive stay honest-by-default (UNVERIFIED on a green suite). */
|
|
58
|
+
export interface ParsedCriterion {
|
|
59
|
+
/** Human-readable criterion text with any {verify:...} directive stripped. */
|
|
60
|
+
text: string;
|
|
61
|
+
/** Optional shell command whose exit status proves this specific criterion. */
|
|
62
|
+
verify?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const VERIFY_DIRECTIVE = /\s*\{verify:\s*([^}]+?)\s*\}\s*$/i;
|
|
66
|
+
|
|
67
|
+
/** Split an optional trailing `{verify: <cmd>}` directive off a criterion string.
|
|
68
|
+
* The directive lives INSIDE the JSON-encoded seed value, so it round-trips
|
|
69
|
+
* unmangled through `yamlList`/`parseSeedList` like any other criterion text. */
|
|
70
|
+
export function parseCriterion(raw: string): ParsedCriterion {
|
|
71
|
+
const match = raw.match(VERIFY_DIRECTIVE);
|
|
72
|
+
if (!match) return { text: raw.trim() };
|
|
73
|
+
const verify = match[1].trim();
|
|
74
|
+
const text = raw.replace(VERIFY_DIRECTIVE, "").trim();
|
|
75
|
+
return verify ? { text, verify } : { text };
|
|
76
|
+
}
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -109,9 +109,27 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
109
109
|
company: id.includes("claude") ? "Anthropic via Antigravity" : id.includes("gpt") ? "OpenAI via Antigravity" : "Google Antigravity",
|
|
110
110
|
})),
|
|
111
111
|
// Tencent
|
|
112
|
+
// Tencent — DeepSeek
|
|
112
113
|
{ canonical: "deepseek-v4-pro", provider: "tencent", providerModel: "deepseek-v4-pro", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
114
|
+
{ canonical: "deepseek-v4-pro-202606", provider: "tencent", providerModel: "deepseek-v4-pro-202606", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
113
115
|
{ canonical: "deepseek-v4-flash", provider: "tencent", providerModel: "deepseek-v4-flash", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
116
|
+
{ canonical: "deepseek-v4-flash-202605", provider: "tencent", providerModel: "deepseek-v4-flash-202605", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
117
|
+
{ canonical: "deepseek-v3.2", provider: "tencent", providerModel: "deepseek-v3.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
118
|
+
// Tencent — MiniMax
|
|
114
119
|
{ canonical: "minimax-m3", provider: "tencent", providerModel: "minimax-m3", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
120
|
+
{ canonical: "minimax-m2.7", provider: "tencent", providerModel: "minimax-m2.7", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
121
|
+
{ canonical: "minimax-m2.5", provider: "tencent", providerModel: "minimax-m2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
122
|
+
// Tencent — Zhipu GLM
|
|
123
|
+
{ canonical: "glm-5.2", provider: "tencent", providerModel: "glm-5.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
124
|
+
{ canonical: "glm-5.1", provider: "tencent", providerModel: "glm-5.1", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
125
|
+
{ canonical: "glm-5", provider: "tencent", providerModel: "glm-5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
126
|
+
{ canonical: "glm-5-turbo", provider: "tencent", providerModel: "glm-5-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
127
|
+
{ canonical: "glm-5v-turbo", provider: "tencent", providerModel: "glm-5v-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: true, company: "Tencent" },
|
|
128
|
+
// Tencent — Moonshot Kimi
|
|
129
|
+
{ canonical: "kimi-k2.6", provider: "tencent", providerModel: "kimi-k2.6", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
130
|
+
{ canonical: "kimi-k2.5", provider: "tencent", providerModel: "kimi-k2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
|
|
131
|
+
// Tencent — Hunyuan MT
|
|
132
|
+
{ canonical: "hy-mt2-plus", provider: "tencent", providerModel: "hy-mt2-plus", contextTokens: 128_000, maxOutputTokens: 8192, thinking: STD, images: false, company: "Tencent" },
|
|
115
133
|
// Ollama (local)
|
|
116
134
|
{ canonical: "qwen2.5", provider: "ollama", providerModel: "ollama/qwen2.5:0.5b", contextTokens: 32_768, maxOutputTokens: 8_192, thinking: [], images: false },
|
|
117
135
|
];
|
|
@@ -270,23 +270,41 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
272
|
* When a provider is authenticated (oauth/api_key) but the live `models` endpoint
|
|
273
|
-
* is unusable — e.g. ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/models
|
|
274
|
-
*
|
|
275
|
-
*
|
|
273
|
+
* is unusable — e.g. ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/models`,
|
|
274
|
+
* or a provider (Tencent MaaS) simply has no `/v1/models` route — surface the static
|
|
275
|
+
* catalog ids so the provider's models still appear in pickers.
|
|
276
|
+
* Keyless/not-logged-in results, and api_key *auth rejections* (a bad key), are
|
|
277
|
+
* returned unchanged so they stay honest failures.
|
|
276
278
|
*/
|
|
277
279
|
export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
|
|
278
280
|
if (result.ok && result.models.length > 0) return result;
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// back to their static catalog too — EXCEPT Antigravity, whose available
|
|
282
|
-
// models depend on the Cloud Code Assist agent backend and must not be faked.
|
|
283
|
-
if (result.source !== "oauth") return result;
|
|
281
|
+
// Antigravity's available models depend on the Cloud Code Assist agent backend
|
|
282
|
+
// and must never be faked from a hard-coded catalog.
|
|
284
283
|
if (result.provider === "antigravity") return result;
|
|
285
|
-
|
|
284
|
+
// OAuth: OpenAI/Codex legitimately rejects the standard /models endpoint while
|
|
285
|
+
// the fixed Codex ids still work; other OAuth providers fall back to their full
|
|
286
|
+
// static catalog too. API-key providers only fall back when the provider has NO
|
|
287
|
+
// models-list endpoint at all (HTTP 404 / not found) — never for an auth
|
|
288
|
+
// rejection, so a wrong key stays a failure instead of fabricating catalog rows.
|
|
289
|
+
const eligible = result.source === "oauth"
|
|
290
|
+
|| (result.source === "api_key" && isMissingModelsEndpoint(result.error));
|
|
291
|
+
if (!eligible) return result;
|
|
292
|
+
const ids = result.provider === "openai" && result.source === "oauth"
|
|
293
|
+
? [...CODEX_MODELS]
|
|
294
|
+
: catalogByProvider(result.provider).map(m => m.providerModel);
|
|
286
295
|
if (ids.length === 0) return result;
|
|
287
296
|
return { ...result, models: ids, ok: true, fallback: true };
|
|
288
297
|
}
|
|
289
298
|
|
|
299
|
+
/**
|
|
300
|
+
* True when a discovery error means the models-list endpoint itself is absent
|
|
301
|
+
* (HTTP 404 / "not found") rather than an auth/key or transient network problem.
|
|
302
|
+
*/
|
|
303
|
+
function isMissingModelsEndpoint(error: string | undefined): boolean {
|
|
304
|
+
if (!error) return false;
|
|
305
|
+
return /(^|\D)404(\D|$)/.test(error) || /not found/i.test(error);
|
|
306
|
+
}
|
|
307
|
+
|
|
290
308
|
/** Discover the live model list for one provider. Never throws. */
|
|
291
309
|
export async function listProviderModels(
|
|
292
310
|
provider: ProviderName,
|
|
@@ -67,8 +67,8 @@ export const OPENAI_COMPAT_PROVIDERS: readonly OpenAICompatProviderDef[] = [
|
|
|
67
67
|
// Anthropic-Messages-protocol providers (served via makeAnthropicCompatibleAdapter).
|
|
68
68
|
{ name: "zai", label: "z.ai", baseUrl: "https://api.z.ai/api/anthropic", apiKeyEnv: "ZAI_API_KEY", defaultModel: "zai/glm-5.2", protocol: "anthropic" },
|
|
69
69
|
{ name: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/anthropic", apiKeyEnv: "MINIMAX_API_KEY", defaultModel: "minimax/minimax-m3", protocol: "anthropic" },
|
|
70
|
-
// Tencent Cloud MaaS (international) serves deepseek-v4 / minimax-m3 over the Anthropic Messages wire format at tokenhub-intl; `${base}/v1/messages` is pinned by the adapter.
|
|
71
|
-
{ name: "tencent", label: "Tencent", baseUrl: "https://tokenhub-intl.tencentcloudmaas.com", apiKeyEnv: "TENCENT_API_KEY", defaultModel: "tencent/deepseek-v4-pro", knownModels: ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m3"], protocol: "anthropic" },
|
|
70
|
+
// Tencent Cloud MaaS (international) serves deepseek-v4 / deepseek-v3.2 / minimax-m3 over the Anthropic Messages wire format at tokenhub-intl; `${base}/v1/messages` is pinned by the adapter. Model ids verified live (2026-06) via /v1/messages probes — the host has no /v1/models route.
|
|
71
|
+
{ name: "tencent", label: "Tencent", baseUrl: "https://tokenhub-intl.tencentcloudmaas.com", apiKeyEnv: "TENCENT_API_KEY", defaultModel: "tencent/deepseek-v4-pro", knownModels: ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m3", "deepseek-v3.2"], protocol: "anthropic" },
|
|
72
72
|
];
|
|
73
73
|
|
|
74
74
|
const BY_NAME = new Map<string, OpenAICompatProviderDef>(OPENAI_COMPAT_PROVIDERS.map(p => [p.name, p]));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { verticalCursorOffset } from "../../tui/components/input-box";
|
|
2
|
+
|
|
1
3
|
export interface InFlightAbortHarness {
|
|
2
4
|
controller: AbortController;
|
|
3
5
|
handleSigint(): void;
|
|
@@ -86,6 +88,34 @@ export function shouldBoxVerticalNav(
|
|
|
86
88
|
return line.length > 0 && opts.slashMatchCount === 0 && !opts.historyPanelOpen;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/** The action an Up/Down keystroke should take on the boxed prompt, given the draft and
|
|
92
|
+
* caret. `expandedLine` has explicit breaks expanded to "\n" (the wrapped row model the
|
|
93
|
+
* box renders); `rawLine` keeps the sentinels so a genuine multi-line draft is detectable.
|
|
94
|
+
*
|
|
95
|
+
* - "move": a visual row exists in `dir` → reposition the caret to `cursor` (textarea feel).
|
|
96
|
+
* - "swallow": no row to move to AND the draft is a GENUINE multi-line message → keep the
|
|
97
|
+
* keystroke INSIDE the box. Falling through to readline at the top/bottom edge
|
|
98
|
+
* would recall input history and WIPE the multi-line draft the user is composing
|
|
99
|
+
* (the "↓ cuts the lower text" bug) — so the boundary key is a deliberate no-op.
|
|
100
|
+
* - "history": no row to move to on a SOFT-WRAPPED single line → fall through to readline so
|
|
101
|
+
* ↑/↓ recall input history at the edges (the dominant one-liner REPL expectation). */
|
|
102
|
+
export type BoxVerticalNavAction =
|
|
103
|
+
| { kind: "move"; cursor: number }
|
|
104
|
+
| { kind: "swallow" }
|
|
105
|
+
| { kind: "history" };
|
|
106
|
+
|
|
107
|
+
export function boxVerticalNavAction(
|
|
108
|
+
expandedLine: string,
|
|
109
|
+
rawLine: string,
|
|
110
|
+
cursor: number,
|
|
111
|
+
width: number,
|
|
112
|
+
dir: "up" | "down",
|
|
113
|
+
): BoxVerticalNavAction {
|
|
114
|
+
const next = verticalCursorOffset(expandedLine, cursor, width, dir);
|
|
115
|
+
if (next != null) return { kind: "move", cursor: next };
|
|
116
|
+
return isGenuineMultilineDraft(rawLine) ? { kind: "swallow" } : { kind: "history" };
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
|
|
90
120
|
/**
|
|
91
121
|
* macOS / fixterms combo-key normalization for the boxed prompt's line editor.
|
|
@@ -474,4 +504,111 @@ export function decideCtrlC(
|
|
|
474
504
|
): CtrlCAction {
|
|
475
505
|
if (msSinceLastCtrlC < collapseMs) return "ignore";
|
|
476
506
|
return hasInput ? "clear" : "exit";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Shift+Enter encodings the box rewrites to a hard-break SENTINEL: the xterm
|
|
510
|
+
* `modifyOtherKeys` form (`CSI 27;2;13~`) and the kitty keyboard-protocol form
|
|
511
|
+
* (`CSI 13;2u`). Shared by the live input filter and its tests so they can't drift. */
|
|
512
|
+
export const SHIFT_ENTER_SEQS: readonly string[] = ["\u001b[27;2;13~", "\u001b[13;2u"];
|
|
513
|
+
|
|
514
|
+
/** Minimal readline view the prompt key-filter reads (and mutates `cursor` on a
|
|
515
|
+
* vertical-nav "move"). The live filter passes the real readline interface; tests
|
|
516
|
+
* pass a plain object. */
|
|
517
|
+
export interface PromptKeyFilterRl {
|
|
518
|
+
line: string;
|
|
519
|
+
cursor: number;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Per-keystroke context the filter needs that is NOT derivable from the bytes:
|
|
523
|
+
* the opt-in lone-LF Shift+Enter toggle, whether a slash dropdown / Ctrl+O history
|
|
524
|
+
* panel currently owns the arrow keys, and the terminal width (for the box's
|
|
525
|
+
* soft-wrap row model). */
|
|
526
|
+
export interface PromptKeyFilterEnv {
|
|
527
|
+
loneLfShiftEnter: boolean;
|
|
528
|
+
slashMatchCount: number;
|
|
529
|
+
historyPanelOpen: boolean;
|
|
530
|
+
/** `process.stdout.columns ?? 80` — the raw column count BEFORE the box's own insets. */
|
|
531
|
+
columns: number;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Carried across chunks: a bracketed paste can span several stdin chunks, so the
|
|
535
|
+
* in-paste flag must survive between filter calls. */
|
|
536
|
+
export interface PromptKeyFilterState {
|
|
537
|
+
inPaste: boolean;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface PromptKeyFilterResult {
|
|
541
|
+
/** Bytes to forward to readline. */
|
|
542
|
+
out: string;
|
|
543
|
+
/** True when the whole chunk was consumed and NOTHING should be written (the
|
|
544
|
+
* empty-line standalone-Backspace guard) — distinct from an empty `out`. */
|
|
545
|
+
drop: boolean;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** The stdin → readline byte rewriter for the boxed multi-line prompt, extracted as a
|
|
549
|
+
* pure function so the full keystroke wiring — paste folding, mouse/terminal-report
|
|
550
|
+
* swallowing, Shift+Enter → SENTINEL, combo-key normalization, and the boundary-aware
|
|
551
|
+
* Up/Down box navigation — is testable WITHOUT a live readline/PTY. The live filter in
|
|
552
|
+
* `launch.ts` is a thin adapter over this: feed it each chunk, forward `out` unless
|
|
553
|
+
* `drop`, and let it mutate `rl.cursor` (textarea caret moves) and `state.inPaste`.
|
|
554
|
+
*
|
|
555
|
+
* The "swallow" branch is the fix for the "↓ cuts the lower text" bug: at the top/bottom
|
|
556
|
+
* visual row of a GENUINE multi-line draft the key is consumed (no bytes emitted) so it
|
|
557
|
+
* can't reach readline's input-history recall and wipe the message being composed. */
|
|
558
|
+
export function filterPromptInputChunk(
|
|
559
|
+
data: string,
|
|
560
|
+
rl: PromptKeyFilterRl | null,
|
|
561
|
+
env: PromptKeyFilterEnv,
|
|
562
|
+
state: PromptKeyFilterState,
|
|
563
|
+
): PromptKeyFilterResult {
|
|
564
|
+
// Empty-line Backspace guard: a standalone Backspace with an empty buffer is a no-op
|
|
565
|
+
// that some Bun readline builds turn into a spurious `close` (hard exit) — drop it
|
|
566
|
+
// before it reaches readline. Forwarded normally inside a paste or with text present.
|
|
567
|
+
if (!state.inPaste && isStandaloneBackspace(data) && !(rl?.line ?? "")) {
|
|
568
|
+
return { out: "", drop: true };
|
|
569
|
+
}
|
|
570
|
+
let out = "";
|
|
571
|
+
let i = 0;
|
|
572
|
+
while (i < data.length) {
|
|
573
|
+
if (!state.inPaste && data.startsWith(PASTE_START, i)) { state.inPaste = true; out += PASTE_START; i += PASTE_START.length; continue; }
|
|
574
|
+
if (state.inPaste && data.startsWith(PASTE_END, i)) { state.inPaste = false; out += PASTE_END; i += PASTE_END.length; continue; }
|
|
575
|
+
if (state.inPaste) {
|
|
576
|
+
if (data.startsWith("\r\n", i)) { out += MULTILINE_SENTINEL; i += 2; continue; }
|
|
577
|
+
if (data[i] === "\n" || data[i] === "\r") { out += MULTILINE_SENTINEL; i += 1; continue; }
|
|
578
|
+
out += data[i]; i += 1; continue;
|
|
579
|
+
}
|
|
580
|
+
const mouse = matchMouseReport(data, i);
|
|
581
|
+
if (mouse > 0) { i += mouse; continue; }
|
|
582
|
+
const report = matchTerminalReport(data, i);
|
|
583
|
+
if (report > 0) { i += report; continue; }
|
|
584
|
+
let matched = false;
|
|
585
|
+
for (const seq of SHIFT_ENTER_SEQS) {
|
|
586
|
+
if (data.startsWith(seq, i)) { out += MULTILINE_SENTINEL; i += seq.length; matched = true; break; }
|
|
587
|
+
}
|
|
588
|
+
if (matched) continue;
|
|
589
|
+
const combo = matchCursorCombo(data, i);
|
|
590
|
+
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
591
|
+
// Up/Down between the box's visual rows (textarea feel) for any MULTI-ROW draft;
|
|
592
|
+
// at the top/bottom edge a soft-wrapped one-liner falls through to history recall,
|
|
593
|
+
// but a genuine multi-line draft swallows the key (the "↓ cuts text" fix).
|
|
594
|
+
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
595
|
+
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
596
|
+
const line = rl?.line ?? "";
|
|
597
|
+
if (shouldBoxVerticalNav(line, { slashMatchCount: env.slashMatchCount, historyPanelOpen: env.historyPanelOpen }) && rl) {
|
|
598
|
+
const winCols = Math.max(24, env.columns - 1);
|
|
599
|
+
const textWidth = Math.max(1, Math.max(24, winCols) - 6);
|
|
600
|
+
const cur = typeof rl.cursor === "number" ? rl.cursor : line.length;
|
|
601
|
+
const action = boxVerticalNavAction(line.split(MULTILINE_SENTINEL).join("\n"), line, cur, textWidth, dir);
|
|
602
|
+
if (action.kind === "move") { rl.cursor = action.cursor; i += 3; continue; }
|
|
603
|
+
if (action.kind === "swallow") { i += 3; continue; } // keep the multi-line draft intact
|
|
604
|
+
}
|
|
605
|
+
out += data.slice(i, i + 3); i += 3; continue;
|
|
606
|
+
}
|
|
607
|
+
if (env.loneLfShiftEnter && data[i] === "\n") { out += MULTILINE_SENTINEL; i += 1; continue; } // lone LF = Shift+Enter (opt-in)
|
|
608
|
+
// Ctrl+L (form feed): the prompt redraw hotkey, handled on the process.stdin keypress
|
|
609
|
+
// listener — never forward it to readline as a literal char.
|
|
610
|
+
if (data[i] === "\u000c") { i += 1; continue; }
|
|
611
|
+
out += data[i]; i += 1;
|
|
612
|
+
}
|
|
613
|
+
return { out, drop: false };
|
|
477
614
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -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,
|
|
65
|
+
import { renderInputFrame, 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";
|
|
@@ -139,12 +139,14 @@ import {
|
|
|
139
139
|
MULTILINE_SENTINEL,
|
|
140
140
|
isGenuineMultilineDraft,
|
|
141
141
|
shouldBoxVerticalNav,
|
|
142
|
+
boxVerticalNavAction,
|
|
142
143
|
queuePromptInputChunk,
|
|
143
144
|
captureLivePromptInputChunk,
|
|
144
145
|
restoreQueuedLinesToPrefill,
|
|
145
146
|
createInFlightAbortHarness,
|
|
146
147
|
classifyMidTurnLine,
|
|
147
148
|
decideCtrlC,
|
|
149
|
+
filterPromptInputChunk,
|
|
148
150
|
} from "./launch/input";
|
|
149
151
|
import {
|
|
150
152
|
gatedStdout,
|
|
@@ -209,6 +211,7 @@ export {
|
|
|
209
211
|
MULTILINE_SENTINEL,
|
|
210
212
|
isGenuineMultilineDraft,
|
|
211
213
|
shouldBoxVerticalNav,
|
|
214
|
+
boxVerticalNavAction,
|
|
212
215
|
queuePromptInputChunk,
|
|
213
216
|
captureLivePromptInputChunk,
|
|
214
217
|
restoreQueuedLinesToPrefill,
|
|
@@ -250,11 +253,11 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
250
253
|
* empty.
|
|
251
254
|
*
|
|
252
255
|
* Live discovery yields ids only for a logged-in, reachable provider, and
|
|
253
|
-
* `catalogOr` backfills the static catalog
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* capability catalog; else its single known default model (all
|
|
257
|
-
* providers carry one) so the user always sees at least one
|
|
256
|
+
* `catalogOr` backfills the static catalog for OAuth sources and for API-key
|
|
257
|
+
* providers whose models-list endpoint is absent (HTTP 404, e.g. Tencent MaaS).
|
|
258
|
+
* A not-yet-configured provider still has an empty live list, so prefer live ids;
|
|
259
|
+
* else the provider's capability catalog; else its single known default model (all
|
|
260
|
+
* 24 OpenAI-compat providers carry one) so the user always sees at least one id.
|
|
258
261
|
*/
|
|
259
262
|
export function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
260
263
|
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
@@ -1392,7 +1395,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1392
1395
|
// submit/race), the box renders it as a real line break, and it is expanded back to
|
|
1393
1396
|
// "\n" on submit. On for any interactive TTY; JEO_NO_MULTILINE=1 reads stdin directly.
|
|
1394
1397
|
const SENTINEL = MULTILINE_SENTINEL;
|
|
1395
|
-
const SHIFT_ENTER_SEQS = ["\u001b[27;2;13~", "\u001b[13;2u"];
|
|
1396
1398
|
// Multi-line input filter is ON for any interactive TTY: reliable multi-line paste
|
|
1397
1399
|
// (fills the box, submits intact into the user card) is the default. The lone-"\n"
|
|
1398
1400
|
// Shift+Enter rule stays opt-in (JEO_MULTILINE=1) — it needs ghostty's
|
|
@@ -1433,70 +1435,25 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1433
1435
|
// `keybind = shift+enter=text:\n`, passes tmux unchanged even with extended-keys
|
|
1434
1436
|
// off) and the xterm "\x1b[27;2;13~" / kitty "\x1b[13;2u" sequences. Enter ("\r")
|
|
1435
1437
|
// passes through and submits.
|
|
1436
|
-
|
|
1438
|
+
// Stateful across chunks (a bracketed paste can span several). The byte-rewrite logic
|
|
1439
|
+
// lives in `filterPromptInputChunk` (pure + exported) so the full keystroke wiring is
|
|
1440
|
+
// testable without a live readline/PTY; this handler is the thin live adapter.
|
|
1441
|
+
const kfState = { inPaste: false };
|
|
1437
1442
|
const kfDataHandler = (chunk: Buffer) => {
|
|
1438
1443
|
const data = chunk.toString("utf8");
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
out += data[i]; i += 1; continue;
|
|
1453
|
-
}
|
|
1454
|
-
// Swallow MOUSE-REPORT sequences (tmux `mouse on` from --tmux, or a stale pane):
|
|
1455
|
-
// their payload bytes would otherwise be typed into the prompt. Never in a paste.
|
|
1456
|
-
const mouse = matchMouseReport(data, i);
|
|
1457
|
-
if (mouse > 0) { i += mouse; continue; }
|
|
1458
|
-
// Swallow TERMINAL CAPABILITY-RESPONSE sequences (DA1/DA2/XTVERSION/OSC replies):
|
|
1459
|
-
// tmux probes the outer terminal on attach and its answers (`62;4;9;22c…>|xterm.js…`)
|
|
1460
|
-
// can land on stdin; without this they spray into the prompt as typed text.
|
|
1461
|
-
const report = matchTerminalReport(data, i);
|
|
1462
|
-
if (report > 0) { i += report; continue; }
|
|
1463
|
-
let matched = false;
|
|
1464
|
-
for (const seq of SHIFT_ENTER_SEQS) {
|
|
1465
|
-
if (data.startsWith(seq, i)) { out += SENTINEL; i += seq.length; matched = true; break; }
|
|
1466
|
-
}
|
|
1467
|
-
if (matched) continue;
|
|
1468
|
-
// Normalize macOS/fixterms combo cursor keys readline ignores (Option/Cmd+arrow,
|
|
1469
|
-
// Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
|
|
1470
|
-
const combo = matchCursorCombo(data, i);
|
|
1471
|
-
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
1472
|
-
// Up/Down move the caret BETWEEN the box's visual rows (textarea feel) for any
|
|
1473
|
-
// MULTI-ROW draft — explicit Shift+Enter breaks (→ SENTINEL) OR a long line the
|
|
1474
|
-
// box soft-wraps. The decision is boundary-aware: verticalCursorOffset returns a
|
|
1475
|
-
// target only when a visual row exists to move to, so ↑ on the TOP row, ↓ on the
|
|
1476
|
-
// BOTTOM row, and any single-visual-row draft fall through to readline and recall
|
|
1477
|
-
// input history — the dominant REPL expectation once the caret can climb no
|
|
1478
|
-
// higher. Skipped when a slash list or Ctrl+O history panel owns ↑/↓.
|
|
1479
|
-
|
|
1480
|
-
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
1481
|
-
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
1482
|
-
const line = activeRl?.line ?? "";
|
|
1483
|
-
if (shouldBoxVerticalNav(line, { slashMatchCount: navMatches.length, historyPanelOpen: promptHistoryLines != null }) && activeRl) {
|
|
1484
|
-
const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
1485
|
-
const textWidth = Math.max(1, Math.max(24, winCols) - 6);
|
|
1486
|
-
const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
|
|
1487
|
-
const next = verticalCursorOffset(expandSentinel(line), cur, textWidth, dir);
|
|
1488
|
-
if (next != null) { activeRl.cursor = next; i += 3; continue; }
|
|
1489
|
-
}
|
|
1490
|
-
out += data.slice(i, i + 3); i += 3; continue;
|
|
1491
|
-
}
|
|
1492
|
-
if (loneLfShiftEnter && data[i] === "\n") { out += SENTINEL; i += 1; continue; } // lone LF = Shift+Enter (opt-in)
|
|
1493
|
-
// Ctrl+L (form feed): consumed as the prompt "redraw / re-anchor" hotkey (handled on
|
|
1494
|
-
// the process.stdin 'keypress' listener), so it must never reach readline as a literal
|
|
1495
|
-
// char that would otherwise insert garbage / desync the box from rl.line.
|
|
1496
|
-
if (data[i] === "\u000c") { i += 1; continue; }
|
|
1497
|
-
out += data[i]; i += 1;
|
|
1498
|
-
}
|
|
1499
|
-
kf.write(out);
|
|
1444
|
+
const result = filterPromptInputChunk(
|
|
1445
|
+
data,
|
|
1446
|
+
activeRl ? (activeRl as unknown as { line: string; cursor: number }) : null,
|
|
1447
|
+
{
|
|
1448
|
+
loneLfShiftEnter,
|
|
1449
|
+
slashMatchCount: navMatches.length,
|
|
1450
|
+
historyPanelOpen: promptHistoryLines != null,
|
|
1451
|
+
columns: process.stdout.columns ?? 80,
|
|
1452
|
+
},
|
|
1453
|
+
kfState,
|
|
1454
|
+
);
|
|
1455
|
+
if (result.drop) return;
|
|
1456
|
+
kf.write(result.out);
|
|
1500
1457
|
};
|
|
1501
1458
|
process.stdin.on("data", kfDataHandler);
|
|
1502
1459
|
promptListenerCleanups.push(() => process.stdin.off("data", kfDataHandler));
|
|
@@ -2167,10 +2124,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2167
2124
|
process.stdin.setRawMode(true);
|
|
2168
2125
|
}
|
|
2169
2126
|
process.stdin.resume();
|
|
2170
|
-
const cols = Math.max(40, terminalSize().cols - 2);
|
|
2171
|
-
const rows = Math.max(6, terminalSize().rows - 6);
|
|
2172
2127
|
let rendered = 0;
|
|
2173
2128
|
const repaint = () => {
|
|
2129
|
+
// Re-measure on EVERY paint so the picker tracks live terminal resizes. The old code
|
|
2130
|
+
// captured cols/rows ONCE at open, so a picker left open across a resize never reflowed
|
|
2131
|
+
// and stayed pinned to its opening geometry — it never recovered after a shrink→grow.
|
|
2132
|
+
const cols = Math.max(40, terminalSize().cols - 2);
|
|
2133
|
+
const rows = Math.max(6, terminalSize().rows - 6);
|
|
2174
2134
|
const lines = render(cols, rows).map(line => truncateAnsi(line, cols));
|
|
2175
2135
|
const total = Math.max(rendered, lines.length);
|
|
2176
2136
|
// First paint: drop down ONCE to start on a fresh row; subsequent paints:
|
|
@@ -2205,6 +2165,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2205
2165
|
rendered = 0;
|
|
2206
2166
|
};
|
|
2207
2167
|
repaint();
|
|
2168
|
+
// Reflow the picker on terminal resize. The terminal has already reflowed the old
|
|
2169
|
+
// block's rows, so `rendered` no longer maps 1:1 to physical rows: drop the stale
|
|
2170
|
+
// accounting, wipe the visible screen (scrollback kept), and repaint at the freshly
|
|
2171
|
+
// measured geometry. Debounced so a drag-resize coalesces into a single clean repaint.
|
|
2172
|
+
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2173
|
+
const onPickerResize = () => {
|
|
2174
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
2175
|
+
resizeTimer = setTimeout(() => {
|
|
2176
|
+
resizeTimer = undefined;
|
|
2177
|
+
try { rendered = 0; out.write(clearVisible()); repaint(); } catch { /* resize render race */ }
|
|
2178
|
+
}, 16);
|
|
2179
|
+
};
|
|
2180
|
+
process.stdout.on("resize", onPickerResize);
|
|
2208
2181
|
try {
|
|
2209
2182
|
await new Promise<void>(resolve => {
|
|
2210
2183
|
const handler = (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
|
|
@@ -2220,6 +2193,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2220
2193
|
process.stdin.on("keypress", handler);
|
|
2221
2194
|
});
|
|
2222
2195
|
} finally {
|
|
2196
|
+
process.stdout.off("resize", onPickerResize);
|
|
2197
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
2223
2198
|
if (process.stdin.setRawMode && !wasRaw) {
|
|
2224
2199
|
process.stdin.setRawMode(false);
|
|
2225
2200
|
}
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readWorkflowState, writeWorkflowState, getLocalJeoDir, type WorkflowState } from "../agent/state";
|
|
4
4
|
import { bashTool } from "../agent/tools";
|
|
5
|
-
import { parseSeedAcceptanceCriteria } from "../agent/seed";
|
|
5
|
+
import { parseSeedAcceptanceCriteria, parseCriterion } from "../agent/seed";
|
|
6
6
|
|
|
7
7
|
export interface UltragoalEngineOptions {
|
|
8
8
|
cwd?: string;
|
|
@@ -80,13 +80,16 @@ export async function runUltragoalEngine(opts: UltragoalEngineOptions = {}): Pro
|
|
|
80
80
|
|
|
81
81
|
log(`Loaded ${criteria.length} acceptance criteria for verification.\n`);
|
|
82
82
|
|
|
83
|
-
// Round-7 #2 (architect ref 7-Round7Workflow)
|
|
84
|
-
//
|
|
85
|
-
// (
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
83
|
+
// Round-7 #2 (architect ref 7-Round7Workflow) + per-criterion verification:
|
|
84
|
+
// The suite still runs ONCE as a global signal — a green suite NEVER fabricates a
|
|
85
|
+
// per-criterion PASS (the old "run/cli → guaranteed-green --help" theater is gone).
|
|
86
|
+
// What's new: a criterion authored with a trailing `{verify: <command>}` directive
|
|
87
|
+
// is INDIVIDUALLY verifiable — ultragoal runs that command and records a real
|
|
88
|
+
// PASS/FAIL. Criteria without a directive stay UNVERIFIED on green (we never
|
|
89
|
+
// specifically proved them): honest by default, strengthenable on demand.
|
|
90
|
+
const parsed = criteria.map(parseCriterion);
|
|
91
|
+
const verifiableCount = parsed.filter(c => c.verify).length;
|
|
92
|
+
|
|
90
93
|
log(`[CHECK] Running the verification suite once ('bun test') — a global signal, not per-criterion proof.`);
|
|
91
94
|
if (opts.onProgress) {
|
|
92
95
|
opts.onProgress({ skill: "ultragoal", phase: "verifying", detail: "Running verification suite" });
|
|
@@ -94,11 +97,33 @@ export async function runUltragoalEngine(opts: UltragoalEngineOptions = {}): Pro
|
|
|
94
97
|
const suite = await bashTool("bun test", cwd);
|
|
95
98
|
log(` └─ Suite: ${suite.success ? "GREEN" : "FAILED"}`);
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
if (opts.signal?.aborted) {
|
|
101
|
+
return { ok: false, reason: "aborted" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Per-criterion checks: only a criterion carrying a {verify:...} directive runs an
|
|
105
|
+
// individual command; everything else inherits the global suite signal.
|
|
106
|
+
const results: { criterion: string; status: "passed" | "unverified" | "failed"; note: string }[] = [];
|
|
107
|
+
for (const c of parsed) {
|
|
108
|
+
if (c.verify) {
|
|
109
|
+
if (opts.signal?.aborted) return { ok: false, reason: "aborted" };
|
|
110
|
+
if (opts.onProgress) {
|
|
111
|
+
opts.onProgress({ skill: "ultragoal", phase: "verifying", detail: `Verifying: ${c.text}` });
|
|
112
|
+
}
|
|
113
|
+
log(`[CHECK] ${c.text}\n └─ $ ${c.verify}`);
|
|
114
|
+
const check = await bashTool(c.verify, cwd);
|
|
115
|
+
log(` ${check.success ? "PASS" : "FAIL"}`);
|
|
116
|
+
results.push(
|
|
117
|
+
check.success
|
|
118
|
+
? { criterion: c.text, status: "passed", note: `verified by \`${c.verify}\`` }
|
|
119
|
+
: { criterion: c.text, status: "failed", note: `\`${c.verify}\` exited non-zero` },
|
|
120
|
+
);
|
|
121
|
+
} else if (suite.success) {
|
|
122
|
+
results.push({ criterion: c.text, status: "unverified", note: "suite green — criterion not individually verified (add {verify: <cmd>})" });
|
|
123
|
+
} else {
|
|
124
|
+
results.push({ criterion: c.text, status: "failed", note: "verification suite failed" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
if (opts.signal?.aborted) {
|
|
104
129
|
return { ok: false, reason: "aborted" };
|
|
@@ -110,18 +135,37 @@ export async function runUltragoalEngine(opts: UltragoalEngineOptions = {}): Pro
|
|
|
110
135
|
const reportPath = path.join(reportDir, "ultragoal-report.md");
|
|
111
136
|
|
|
112
137
|
const totalCount = results.length;
|
|
113
|
-
const
|
|
138
|
+
const passedCount = results.filter(r => r.status === "passed").length;
|
|
139
|
+
const failedCount = results.filter(r => r.status === "failed").length;
|
|
140
|
+
const unverifiedCount = results.filter(r => r.status === "unverified").length;
|
|
141
|
+
|
|
142
|
+
// Overall status taxonomy:
|
|
143
|
+
// FAILED — the global suite is red, OR any individually-checked criterion failed.
|
|
144
|
+
// SUCCESS — every criterion was individually verified and passed (no UNVERIFIED left).
|
|
145
|
+
// PARTIAL — some criteria individually passed, but others remain UNVERIFIED.
|
|
146
|
+
// SUITE_GREEN — suite green but NO criterion was individually verified (legacy honest default).
|
|
147
|
+
let status: string;
|
|
148
|
+
if (!suite.success || failedCount > 0) {
|
|
149
|
+
status = "FAILED";
|
|
150
|
+
} else if (passedCount > 0 && unverifiedCount === 0) {
|
|
151
|
+
status = "SUCCESS";
|
|
152
|
+
} else if (passedCount > 0) {
|
|
153
|
+
status = "PARTIAL";
|
|
154
|
+
} else {
|
|
155
|
+
status = "SUITE_GREEN";
|
|
156
|
+
}
|
|
114
157
|
|
|
115
158
|
const reportContent =
|
|
116
159
|
`# Ultragoal Verification Report: ${interviewState.slug}\n` +
|
|
117
160
|
`Date: ${new Date().toISOString()}\n` +
|
|
118
|
-
`Status: ${status} — suite ${suite.success ? "green" : "FAILED"};
|
|
161
|
+
`Status: ${status} — suite ${suite.success ? "green" : "FAILED"}; ` +
|
|
162
|
+
`${totalCount} criteria (${passedCount} verified, ${unverifiedCount} unverified, ${failedCount} failed)\n` +
|
|
119
163
|
`Plan: ${teamState?.plan_path ?? "(team not run)"}\n` +
|
|
120
164
|
`Execution: ${teamState?.current_phase === "complete" ? "team complete" : "team NOT complete — verified current disk state"}\n\n` +
|
|
121
165
|
`## Criteria Record\n` +
|
|
122
166
|
`| Criterion | Status | Note |\n` +
|
|
123
167
|
`|---|---|---|\n` +
|
|
124
|
-
results.map(r => `| ${r.criterion} | ${r.status === "failed" ? "❌ FAILED" : "⚠️ UNVERIFIED"} | ${r.note} |`).join("\n") +
|
|
168
|
+
results.map(r => `| ${r.criterion} | ${r.status === "passed" ? "✅ PASSED" : r.status === "failed" ? "❌ FAILED" : "⚠️ UNVERIFIED"} | ${r.note} |`).join("\n") +
|
|
125
169
|
`\n`;
|
|
126
170
|
|
|
127
171
|
// Atomic temp+rename (zeroclaw): a torn report must not disagree with the state JSON.
|
|
@@ -144,18 +188,24 @@ export async function runUltragoalEngine(opts: UltragoalEngineOptions = {}): Pro
|
|
|
144
188
|
plan_path: teamState?.plan_path,
|
|
145
189
|
status,
|
|
146
190
|
suite_green: suite.success,
|
|
191
|
+
passed: passedCount,
|
|
147
192
|
total: totalCount,
|
|
148
193
|
};
|
|
149
194
|
await writeWorkflowState("ultragoal", ultragoalState, cwd);
|
|
150
195
|
|
|
151
196
|
log(`\n[VERIFICATION COMPLETE] Report saved to: ${reportPath}`);
|
|
152
|
-
log(
|
|
197
|
+
log(
|
|
198
|
+
verifiableCount > 0
|
|
199
|
+
? `Overall status: ${status} — ${passedCount}/${totalCount} criteria individually verified, ${unverifiedCount} unverified, ${failedCount} failed.`
|
|
200
|
+
: `Overall status: ${status} — ${totalCount} criteria recorded; none individually verified (add {verify: <cmd>} to a criterion for a real per-criterion check).`,
|
|
201
|
+
);
|
|
202
|
+
|
|
153
203
|
|
|
154
204
|
if (opts.onProgress) {
|
|
155
205
|
opts.onProgress({ skill: "ultragoal", phase: "complete" });
|
|
156
206
|
}
|
|
157
207
|
|
|
158
|
-
return { ok:
|
|
208
|
+
return { ok: status !== "FAILED" };
|
|
159
209
|
}
|
|
160
210
|
|
|
161
211
|
export async function runUltragoalCommand(): Promise<void> {
|
package/src/tui/app.ts
CHANGED
|
@@ -16,13 +16,13 @@ import { Spinner } from "./components/spinner";
|
|
|
16
16
|
import { ToolList } from "./components/tool-list";
|
|
17
17
|
import { StreamRegion } from "./components/stream";
|
|
18
18
|
import { renderFooter, type FooterData } from "./components/footer";
|
|
19
|
-
import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat
|
|
19
|
+
import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat } from "./components/ascii-art";
|
|
20
20
|
import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
|
|
21
21
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
22
22
|
import { supportsUnicode } from "./components/capability";
|
|
23
23
|
import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
|
|
24
24
|
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
25
|
-
import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint } from "./components/themes";
|
|
25
|
+
import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint, themeFlowPalette } from "./components/themes";
|
|
26
26
|
import { detectColorLevel, animatedGradientText, ColorLevel } from "./components/color";
|
|
27
27
|
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, scaleForgeWidth, type ForgeSummary } from "./components/forge";
|
|
28
28
|
import { renderStatusBar, renderStatusBox, type StatusBoxData } from "./components/status";
|
|
@@ -314,6 +314,9 @@ export class LaunchTui {
|
|
|
314
314
|
private readonly unicode: boolean = supportsUnicode();
|
|
315
315
|
// Active color theme (JEO_TUI_THEME), default cosmic; `mono` disables color.
|
|
316
316
|
private readonly theme = resolveTheme(process.env);
|
|
317
|
+
// Neon flow palette derived from the active theme (forge-card border + forge mark
|
|
318
|
+
// animation), so the live glow tracks the theme instead of a fixed brand palette.
|
|
319
|
+
private readonly themeFlow = themeFlowPalette(this.theme);
|
|
317
320
|
// Whether the live turn may use the alternate screen buffer (real TTY only).
|
|
318
321
|
private readonly tty: boolean;
|
|
319
322
|
// gjc-style inline rendering (default on a TTY): the live frame repaints in place in
|
|
@@ -968,7 +971,6 @@ export class LaunchTui {
|
|
|
968
971
|
this.subagentLive = null; // fresh turn: no nested subagent in flight
|
|
969
972
|
this.activityLog.length = 0; // per-turn ring: timestamps are turn-relative
|
|
970
973
|
this.spinner.updateStep(0, this.footer.maxSteps);
|
|
971
|
-
// On a real TTY the live turn renders gjc-style in the MAIN buffer by default:
|
|
972
974
|
// completed ledger lines are flushed into normal scrollback as they happen, so a
|
|
973
975
|
// tmux / terminal mouse-wheel scroll can review earlier progress mid-turn. The
|
|
974
976
|
// differential renderer reserves frame rows with real newlines, keeping the
|
|
@@ -1278,7 +1280,7 @@ export class LaunchTui {
|
|
|
1278
1280
|
// the card border and the prompt beat marks the title. Flushed/final cards
|
|
1279
1281
|
// stay static. Suppressed while `dim` (in-flight shading takes precedence).
|
|
1280
1282
|
...(anim && !dim
|
|
1281
|
-
? { flow: { palette:
|
|
1283
|
+
? { flow: { palette: this.themeFlow, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
|
|
1282
1284
|
: {}),
|
|
1283
1285
|
}));
|
|
1284
1286
|
}
|
|
@@ -1570,6 +1572,7 @@ export class LaunchTui {
|
|
|
1570
1572
|
unicode: this.unicode,
|
|
1571
1573
|
color: this.theme.color,
|
|
1572
1574
|
colorLevel: detectColorLevel(process.env, isTTY()),
|
|
1575
|
+
palette: this.themeFlow,
|
|
1573
1576
|
});
|
|
1574
1577
|
this.cachedArt = fit ? centerBlock(art, innerWidth) : art;
|
|
1575
1578
|
const track = evolutionTrack(idx, { unicode: this.unicode, color: this.theme.color });
|
|
@@ -528,8 +528,12 @@ export function renderForgeMark(opts: {
|
|
|
528
528
|
* flowing gradient `phase` this animates the forge identity without any
|
|
529
529
|
* frame-count growth. */
|
|
530
530
|
frame?: number;
|
|
531
|
+
/** Gradient palette for the flowing glow. Defaults to the brand `FORGE_FLOW_PALETTE`;
|
|
532
|
+
* pass a theme-derived palette (see `themeFlowPalette`) so the mark tracks the theme. */
|
|
533
|
+
palette?: readonly string[];
|
|
531
534
|
}): string[] {
|
|
532
|
-
const
|
|
535
|
+
const palette = opts.palette ?? FORGE_FLOW_PALETTE;
|
|
536
|
+
const memoKey = `${opts.grand ? "g" : "c"}|${opts.unicode !== false ? 1 : 0}|${opts.cols ?? -1}|${opts.color !== false ? 1 : 0}|${opts.colorLevel ?? ColorLevel.TrueColor}|${opts.phase ?? 0}|${opts.frame ?? 0}|${palette.join(",")}`;
|
|
533
537
|
const memoHit = forgeMarkMemo.get(memoKey);
|
|
534
538
|
if (memoHit) return memoHit;
|
|
535
539
|
const useUnicode = opts.unicode !== false;
|
|
@@ -551,7 +555,7 @@ export function renderForgeMark(opts: {
|
|
|
551
555
|
const phase = opts.phase ?? 0;
|
|
552
556
|
const useColor = opts.color !== false;
|
|
553
557
|
const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
|
|
554
|
-
|
|
558
|
+
|
|
555
559
|
|
|
556
560
|
const result = source.map((line, idx) => {
|
|
557
561
|
const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
|
|
@@ -285,6 +285,20 @@ export function accentShadowPaint(theme: EvolutionTheme): (s: string) => string
|
|
|
285
285
|
return (s: string) => chalk.dim(chalk.hex(hex)(s));
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
|
|
289
|
+
/** A 3-stop neon "flow" palette derived from the active theme, for the animated
|
|
290
|
+
* forge-card border and the forge mark so the live flow matches the THEME instead
|
|
291
|
+
* of a fixed brand palette. Sweeps `accentShadow` → `accent` → the brightest stage
|
|
292
|
+
* gradient end (a dark → accent → light shimmer that reads as the theme's own glow).
|
|
293
|
+
* Colorless themes collapse to a single accent stop — callers gate the animation on
|
|
294
|
+
* color/TrueColor, so the value is only a harmless fallback there. */
|
|
295
|
+
export function themeFlowPalette(theme: EvolutionTheme): readonly string[] {
|
|
296
|
+
if (!theme.color) return [theme.accent];
|
|
297
|
+
const bright = theme.gradients[theme.gradients.length - 1]?.to ?? theme.accent;
|
|
298
|
+
const dark = theme.accentShadow ?? theme.accent;
|
|
299
|
+
return [dark, theme.accent, bright];
|
|
300
|
+
}
|
|
301
|
+
|
|
288
302
|
/** Default muted foreground when a theme defines no `muted` — a readable mid-gray
|
|
289
303
|
* that holds up on dark terminals (unlike ANSI `dim`, which collapses toward the
|
|
290
304
|
* background). */
|