jeo-code 0.6.0 → 0.6.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 +26 -0
- package/README.ja.md +10 -2
- package/README.ko.md +10 -2
- package/README.md +10 -2
- package/README.zh.md +10 -2
- package/package.json +1 -1
- package/src/ai/providers/anthropic.ts +28 -3
- package/src/ai/providers/antigravity.ts +6 -0
- package/src/ai/providers/openai-responses.ts +14 -1
- package/src/commands/launch.ts +184 -44
- package/src/skills/catalog.ts +9 -2
- package/src/tui/app.ts +25 -7
- package/src/tui/components/config-panel.ts +29 -0
- package/src/tui/components/evolution.ts +82 -3
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/markdown-text.ts +58 -6
- package/src/tui/components/provider-picker.ts +5 -3
- package/src/tui/components/section.ts +18 -4
- package/src/tui/components/spinner.ts +1 -0
- package/src/tui/components/status.ts +28 -6
- package/src/tui/components/transcript.ts +6 -0
- package/src/util/retry.ts +9 -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.2] - 2026-06-16
|
|
10
|
+
_Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Interactive `/provider` picker** (gjc-style) with a clean screen after login (#26).
|
|
14
|
+
- **Clearer visual structure** — unified labeled boundaries for thinking / reasoning / output blocks and a dimensional animated status line (#29, building on the labeled-boundary work).
|
|
15
|
+
- `docs/minimo/` — a plan to apply MiMo Code's memory & goal-management ideas to jeo (#28).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Retry transient empty-200 responses** (a 200 with an empty body) for stability — gjc parity (#27).
|
|
19
|
+
|
|
20
|
+
## [0.6.1] - 2026-06-16
|
|
21
|
+
_Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes._
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Live reasoning progress.** Codex/OpenAI reasoning models now stream their thinking into the live frame (`reasoning.summary: "auto"` + `response.reasoning*.delta` events surfaced via `onReasoning`), and the status row reads `reasoning (model)…` / `thinking — reasoning, no token stream yet…` after a silent wait instead of a frozen `calling model (Ns)…`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Thinking level is now applied to the **Anthropic and Antigravity** providers (it was a silent no-op there).
|
|
28
|
+
- The **input box + caret stay in place after running a command** — no more vanishing box / caret parked at the reservation top.
|
|
29
|
+
- **Skill runs render a compact `[skill]` card** instead of dumping the injected `SKILL.md` into a user box.
|
|
30
|
+
- **Ctrl+O fold toggle** + incremental session durability across interruption.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Trimmed `fastThinkingLevelForModel` fallback to the real gap (ponytail pass); added a usage guide + demo video, linked from all READMEs.
|
|
34
|
+
|
|
9
35
|
## [0.6.0] - 2026-06-16
|
|
10
36
|
_TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel._
|
|
11
37
|
|
package/README.ja.md
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
リポジトリ内で `jeo` を実行すると、ファイルを読み・編集し・コマンドを実行してタスクを完了まで進めます — 全ステップがスクロールバック親和なインライン TUI でライブ配信されます。
|
|
30
30
|
|
|
31
|
+
## ドキュメント
|
|
32
|
+
|
|
33
|
+
📖 **[使い方ガイド](docs/usage-guide.md)** — インストール、TUI操作(↑履歴、Ctrl+O、`!`シェル)、スラッシュコマンド、`/resume`、スペックファーストワークフローをデモ動画付きで解説。
|
|
34
|
+
|
|
35
|
+
<video src="https://raw.githubusercontent.com/akillness/jeo-code/main/docs/jeo-code-promo.mp4" controls muted playsinline width="100%"></video>
|
|
36
|
+
|
|
37
|
+
> インライン再生されない場合は ▶ [デモ動画を再生/ダウンロード](docs/jeo-code-promo.mp4)。
|
|
38
|
+
|
|
31
39
|
## ハイライト
|
|
32
40
|
|
|
33
41
|
- **マルチプロバイダ・単一ループ** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama を均一な JSON ツールループで。入力欄から OAuth ログイン(`/provider login`)、モデル選択は即座にデフォルトとして永続化。
|
|
@@ -150,11 +158,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
150
158
|
## 変更履歴 (Changelog)
|
|
151
159
|
|
|
152
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
162
|
+
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
153
163
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
154
164
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
155
165
|
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
156
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
157
|
-
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
158
166
|
|
|
159
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
168
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
저장소 안에서 `jeo`를 실행하면 파일을 읽고, 수정하고, 명령을 실행하며 작업을 완료까지 끌고 갑니다 — 모든 스텝이 스크롤백 친화적인 인라인 TUI로 실시간 스트리밍됩니다.
|
|
30
30
|
|
|
31
|
+
## 문서
|
|
32
|
+
|
|
33
|
+
📖 **[사용 가이드](docs/usage-guide.md)** — 설치, TUI 조작(↑ 이전 쿼리, Ctrl+O, `!` 셸), 슬래시 명령, `/resume`, 스펙 우선 워크플로를 데모 영상과 함께 설명합니다.
|
|
34
|
+
|
|
35
|
+
<video src="https://raw.githubusercontent.com/akillness/jeo-code/main/docs/jeo-code-promo.mp4" controls muted playsinline width="100%"></video>
|
|
36
|
+
|
|
37
|
+
> 인라인 재생이 안 되면 ▶ [데모 영상 재생/다운로드](docs/jeo-code-promo.mp4).
|
|
38
|
+
|
|
31
39
|
## 하이라이트
|
|
32
40
|
|
|
33
41
|
- **멀티 프로바이더, 단일 루프** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama를 균일한 JSON 도구 루프로. 입력창에서 바로 OAuth 로그인(`/provider login`), 모델 선택은 즉시 기본값으로 영속.
|
|
@@ -150,11 +158,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
150
158
|
## 변경 이력 (Changelog)
|
|
151
159
|
|
|
152
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
162
|
+
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
153
163
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
154
164
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
155
165
|
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
156
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
157
|
-
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
158
166
|
|
|
159
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
168
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
Run `jeo` inside a repository and it reads files, edits them, runs commands, and drives the task to completion — streaming every step live in an inline, scrollback-friendly TUI.
|
|
30
30
|
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
📖 **[Usage guide](docs/usage-guide.md)** — install, TUI controls (↑ recall, Ctrl+O, `!` shell), slash commands, `/resume`, and the spec-first workflow, with a demo video.
|
|
34
|
+
|
|
35
|
+
<video src="https://raw.githubusercontent.com/akillness/jeo-code/main/docs/jeo-code-promo.mp4" controls muted playsinline width="100%"></video>
|
|
36
|
+
|
|
37
|
+
> Demo not playing inline? ▶ [Play / download the demo video](docs/jeo-code-promo.mp4).
|
|
38
|
+
|
|
31
39
|
## Highlights
|
|
32
40
|
|
|
33
41
|
- **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama behind a uniform JSON tool loop. OAuth login from the input box (`/provider login`), every model pick persists as the new default.
|
|
@@ -150,11 +158,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
150
158
|
## Changelog
|
|
151
159
|
|
|
152
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
162
|
+
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
153
163
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
154
164
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
155
165
|
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
156
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
157
|
-
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
158
166
|
|
|
159
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
168
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
在仓库内运行 `jeo`,它会读取文件、编辑代码、执行命令,并把任务推进到完成 — 每一步都通过滚动友好的内联 TUI 实时呈现。
|
|
30
30
|
|
|
31
|
+
## 文档
|
|
32
|
+
|
|
33
|
+
📖 **[使用指南](docs/usage-guide.md)** — 安装、TUI 操作(↑ 历史、Ctrl+O、`!` shell)、斜杠命令、`/resume`、规格优先工作流,附演示视频。
|
|
34
|
+
|
|
35
|
+
<video src="https://raw.githubusercontent.com/akillness/jeo-code/main/docs/jeo-code-promo.mp4" controls muted playsinline width="100%"></video>
|
|
36
|
+
|
|
37
|
+
> 无法内联播放?▶ [播放/下载演示视频](docs/jeo-code-promo.mp4)。
|
|
38
|
+
|
|
31
39
|
## 亮点
|
|
32
40
|
|
|
33
41
|
- **多提供商、单一循环** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama 统一在一个 JSON 工具循环中。输入框内直接 OAuth 登录(`/provider login`),模型选择即刻持久化为默认值。
|
|
@@ -150,11 +158,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
150
158
|
## 更新日志 (Changelog)
|
|
151
159
|
|
|
152
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
162
|
+
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
153
163
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
154
164
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
155
165
|
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
156
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
157
|
-
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
158
166
|
|
|
159
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
168
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -72,6 +72,18 @@ function anthropicSystemBlocks(
|
|
|
72
72
|
return blocks;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** Anthropic extended-thinking budget by reasoning effort (kept under max_tokens). Off for
|
|
76
|
+
* low/minimal/unset effort so /fast and minimal thinking stay non-thinking (cheaper/faster). */
|
|
77
|
+
function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"], maxTokens: number): number | undefined {
|
|
78
|
+
let budget: number;
|
|
79
|
+
switch (effort) {
|
|
80
|
+
case "medium": budget = 4096; break;
|
|
81
|
+
case "high": budget = 10000; break;
|
|
82
|
+
default: return undefined;
|
|
83
|
+
}
|
|
84
|
+
return Math.min(budget, Math.max(1024, maxTokens - 1024));
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
export function anthropicPayload(
|
|
76
88
|
messages: Message[],
|
|
77
89
|
options: CallOptions,
|
|
@@ -109,13 +121,24 @@ export function anthropicPayload(
|
|
|
109
121
|
last.content[last.content.length - 1] = { ...tail, cache_control: { type: "ephemeral" } };
|
|
110
122
|
}
|
|
111
123
|
}
|
|
124
|
+
const maxTokens = options.maxTokens ?? 4000;
|
|
125
|
+
const thinkingBudget = anthropicThinkingBudget(options.reasoningEffort, maxTokens);
|
|
112
126
|
const payload: Record<string, unknown> = {
|
|
113
127
|
model,
|
|
114
128
|
messages: anthropicMessages,
|
|
115
|
-
max_tokens
|
|
129
|
+
// Extended thinking requires max_tokens strictly above the thinking budget.
|
|
130
|
+
max_tokens: thinkingBudget !== undefined ? Math.max(maxTokens, thinkingBudget + 1024) : maxTokens,
|
|
116
131
|
};
|
|
117
132
|
if (credential.kind === "oauth") payload.metadata = { user_id: createClaudeCloakingUserId() };
|
|
118
|
-
if (
|
|
133
|
+
if (thinkingBudget !== undefined) {
|
|
134
|
+
// Apply the thinking level: enable Claude extended thinking (the interleaved-thinking
|
|
135
|
+
// beta is already in the headers). Extended thinking forbids a custom temperature, so
|
|
136
|
+
// temperature is only set on the non-thinking path. Previously the thinking level only
|
|
137
|
+
// changed max_tokens and never reached Claude as actual reasoning depth.
|
|
138
|
+
payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
139
|
+
} else if (includeTemperature && options.temperature !== undefined) {
|
|
140
|
+
payload.temperature = options.temperature;
|
|
141
|
+
}
|
|
119
142
|
if (options.tools?.length) {
|
|
120
143
|
// NATIVE tool-calling: declare jeo's tools as Anthropic functions. tool_choice
|
|
121
144
|
// "auto" keeps prose-salvage reachable and lets the model call `done` (declared as
|
|
@@ -232,7 +255,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
232
255
|
type?: string;
|
|
233
256
|
index?: number;
|
|
234
257
|
content_block?: { type?: string; name?: string };
|
|
235
|
-
delta?: { type?: string; text?: string; partial_json?: string; stop_reason?: string };
|
|
258
|
+
delta?: { type?: string; text?: string; partial_json?: string; thinking?: string; stop_reason?: string };
|
|
236
259
|
message?: { usage?: AnthropicUsage };
|
|
237
260
|
usage?: { output_tokens?: number };
|
|
238
261
|
};
|
|
@@ -249,6 +272,8 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
249
272
|
} else if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta" && evt.delta.text) {
|
|
250
273
|
yieldedAny = true;
|
|
251
274
|
yield evt.delta.text;
|
|
275
|
+
} else if (evt.type === "content_block_delta" && evt.delta?.type === "thinking_delta" && evt.delta.thinking) {
|
|
276
|
+
options.onReasoning?.(evt.delta.thinking);
|
|
252
277
|
} else if (evt.type === "message_start" && evt.message?.usage) {
|
|
253
278
|
// Cache only — usage is reported ONCE at message_delta so an accumulating
|
|
254
279
|
// sink can't double-count input (and a pre-first-chunk retry that replays
|
|
@@ -4,6 +4,7 @@ import type { CallOptions, Message, ProviderAdapter } from "../types";
|
|
|
4
4
|
import { readSse } from "../sse";
|
|
5
5
|
import { providerHttpError } from "./errors";
|
|
6
6
|
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
7
|
+
import { geminiThinkingBudget } from "./gemini";
|
|
7
8
|
|
|
8
9
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
9
10
|
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
@@ -130,6 +131,11 @@ export function antigravityRequest(messages: Message[], options: CallOptions, cr
|
|
|
130
131
|
if (options.temperature !== undefined) generationConfig.temperature = options.temperature;
|
|
131
132
|
// Upstream Antigravity strips maxOutputTokens for non-Claude models; do the same.
|
|
132
133
|
if (model.toLowerCase().includes("claude")) generationConfig.maxOutputTokens = options.maxTokens ?? 4000;
|
|
134
|
+
// Apply the thinking level: antigravity serves Gemini models through CCA, so reuse the
|
|
135
|
+
// Gemini thinkingConfig budget (off at minimal, scaling with reasoning effort). Without
|
|
136
|
+
// this the thinking level only changed token budget, never actual reasoning depth.
|
|
137
|
+
const agThinkingBudget = geminiThinkingBudget(model, options.reasoningEffort);
|
|
138
|
+
if (agThinkingBudget !== undefined) generationConfig.thinkingConfig = { thinkingBudget: agThinkingBudget };
|
|
133
139
|
|
|
134
140
|
const request: Record<string, unknown> = {
|
|
135
141
|
contents: antigravityContents(messages),
|
|
@@ -72,7 +72,9 @@ export function codexResponsesRequest(
|
|
|
72
72
|
// Map thinkingLevel → reasoning effort for Codex reasoning models (gjc parity).
|
|
73
73
|
// Drop out-of-enum values instead of forwarding them — the backend 400s on unknown efforts.
|
|
74
74
|
if (options.reasoningEffort && VALID_REASONING_EFFORTS.has(options.reasoningEffort)) {
|
|
75
|
-
|
|
75
|
+
// `summary: "auto"` makes the backend stream reasoning-summary deltas so the live
|
|
76
|
+
// frame can show the model's thinking instead of a frozen "calling model (Ns)…".
|
|
77
|
+
payload.reasoning = { effort: options.reasoningEffort, summary: "auto" };
|
|
76
78
|
}
|
|
77
79
|
const accountId = extractChatgptAccountId(token);
|
|
78
80
|
const headers: Record<string, string> = {
|
|
@@ -88,6 +90,9 @@ export function codexResponsesRequest(
|
|
|
88
90
|
|
|
89
91
|
export interface ResponsesEvent {
|
|
90
92
|
delta?: string;
|
|
93
|
+
/** Reasoning-summary text delta (`response.reasoning_summary_text.delta` and the
|
|
94
|
+
* Codex backend's `reasoning_text` variant) — streamed live as the model thinks. */
|
|
95
|
+
reasoningDelta?: string;
|
|
91
96
|
usage?: { inputTokens?: number; outputTokens?: number };
|
|
92
97
|
error?: string;
|
|
93
98
|
/** `response.incomplete` cause (e.g. max_output_tokens) — surfaced when the
|
|
@@ -125,6 +130,12 @@ export function parseResponsesEvent(data: string): ResponsesEvent {
|
|
|
125
130
|
return { toolCallArgsDelta: o.delta, toolCallIndex: o.output_index };
|
|
126
131
|
}
|
|
127
132
|
if (o.type === "response.output_text.delta" && typeof o.delta === "string") return { delta: o.delta };
|
|
133
|
+
// Reasoning-summary streaming: surface the model's thinking live. Accept the
|
|
134
|
+
// documented `response.reasoning_summary_text.delta` and the Codex backend's
|
|
135
|
+
// `response.reasoning_text.delta` (any reasoning*.delta variant) uniformly.
|
|
136
|
+
if (typeof o.delta === "string" && /^response\.reasoning[a-z_]*\.delta$/.test(o.type ?? "")) {
|
|
137
|
+
return { reasoningDelta: o.delta };
|
|
138
|
+
}
|
|
128
139
|
// `response.incomplete` (max_output_tokens / content filter) also carries usage — don't drop it.
|
|
129
140
|
if ((o.type === "response.completed" || o.type === "response.incomplete") && o.response?.usage) {
|
|
130
141
|
return {
|
|
@@ -175,6 +186,7 @@ export async function codexResponsesCall(messages: Message[], options: CallOptio
|
|
|
175
186
|
for await (const data of readSse(response.body)) {
|
|
176
187
|
const ev = parseResponsesEvent(data);
|
|
177
188
|
if (ev.delta) out += ev.delta;
|
|
189
|
+
if (ev.reasoningDelta) options.onReasoning?.(ev.reasoningDelta);
|
|
178
190
|
accumulateResponsesToolCall(toolAcc, ev);
|
|
179
191
|
if (ev.usage) options.onUsage?.(ev.usage);
|
|
180
192
|
if (ev.incompleteReason) incompleteReason = ev.incompleteReason;
|
|
@@ -202,6 +214,7 @@ export async function* codexResponsesStream(
|
|
|
202
214
|
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
203
215
|
for await (const data of readSse(response.body)) {
|
|
204
216
|
const ev = parseResponsesEvent(data);
|
|
217
|
+
if (ev.reasoningDelta) options.onReasoning?.(ev.reasoningDelta);
|
|
205
218
|
if (ev.delta) {
|
|
206
219
|
yieldedAny = true;
|
|
207
220
|
yield ev.delta;
|
package/src/commands/launch.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { SelectList, renderSelectList, type SelectItem } from "../tui/components
|
|
|
45
45
|
import {
|
|
46
46
|
formatModelLine,
|
|
47
47
|
formatProviderPanel,
|
|
48
|
+
emitLoginCleanup,
|
|
48
49
|
formatAgentsPanel,
|
|
49
50
|
formatAgentDetail,
|
|
50
51
|
formatConfigPanel,
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
} from "../tui/components/config-panel";
|
|
56
57
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
57
58
|
|
|
58
|
-
import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
|
|
59
|
+
import { providerPicker, renderProviderPicker, buildProviderChoices } from "../tui/components/provider-picker";
|
|
59
60
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
60
61
|
import { categoryBadge } from "../tui/components/category-index";
|
|
61
62
|
import { renderInputFrame } from "../tui/components/input-box";
|
|
@@ -142,6 +143,12 @@ function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefined {
|
|
|
142
143
|
const supported = catalogMetadata(modelId)?.thinking ?? [];
|
|
143
144
|
if (supported.includes("minimal")) return "minimal";
|
|
144
145
|
if (supported.includes("low")) return "low";
|
|
146
|
+
// Fallback for the one thinking-capable family that misses a catalog entry in practice:
|
|
147
|
+
// the dynamic antigravity `gemini-3.x` variants (gemini-3.5-flash-low/-extra-low, …) the
|
|
148
|
+
// CCA backend serves but the static catalog can't enumerate. They apply a thinking budget,
|
|
149
|
+
// so /fast offers `minimal` as the fast level instead of reporting "unsupported". (openai
|
|
150
|
+
// reasoning models and *-thinking/-high/-low antigravity ids are already catalogued.)
|
|
151
|
+
if (/gemini-(2\.5|[3-9])/.test(modelId.toLowerCase())) return "minimal";
|
|
145
152
|
return undefined;
|
|
146
153
|
}
|
|
147
154
|
|
|
@@ -1311,6 +1318,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1311
1318
|
base.onToolResult?.(tool, success, output);
|
|
1312
1319
|
},
|
|
1313
1320
|
});
|
|
1321
|
+
/** Compose a session-persistence flush into onStep so each completed step is
|
|
1322
|
+
* written as it lands (durability across mid-turn interruption) without
|
|
1323
|
+
* disturbing the original onStep sink. */
|
|
1324
|
+
const withStepPersistence = (
|
|
1325
|
+
base: AgentLoopEvents,
|
|
1326
|
+
persist: () => Promise<void>,
|
|
1327
|
+
): AgentLoopEvents => ({
|
|
1328
|
+
...base,
|
|
1329
|
+
onStep: async (step: number) => {
|
|
1330
|
+
await base.onStep?.(step);
|
|
1331
|
+
await persist();
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1314
1334
|
/** The Ctrl+O detail block (shared by the prompt-time keypress handler and the
|
|
1315
1335
|
* mid-turn TUI binding): full last reply + full last tool output. */
|
|
1316
1336
|
const composeDetailLines = (): string[] => {
|
|
@@ -1372,7 +1392,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1372
1392
|
const runTurn = async (
|
|
1373
1393
|
userInput: string,
|
|
1374
1394
|
useTui: boolean,
|
|
1375
|
-
images?: ImageAttachment[]
|
|
1395
|
+
images?: ImageAttachment[],
|
|
1396
|
+
// What to show as the user card in the live frame: undefined → the prompt
|
|
1397
|
+
// itself (normal turns); null → suppress it entirely (skill runs, where the
|
|
1398
|
+
// injected SKILL.md is NOT user-authored and the [skill] card already names it,
|
|
1399
|
+
// gjc-style); a string → show that compact label instead of the raw input.
|
|
1400
|
+
opts?: { userCard?: string | null },
|
|
1376
1401
|
): Promise<{ done: boolean; steps: number; reply: string; rendered: boolean; usage: string }> => {
|
|
1377
1402
|
const turnConfig = await readGlobalConfig();
|
|
1378
1403
|
const activeModel = sessionModel || turnConfig.defaultModel;
|
|
@@ -1389,6 +1414,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1389
1414
|
// AFTER compaction (which mutates history) and consumed by the post-turn
|
|
1390
1415
|
// persistence block below.
|
|
1391
1416
|
let beforeLen = history.length;
|
|
1417
|
+
// Incremental session persistence (durability across mid-turn interruption):
|
|
1418
|
+
// persistTurnTail() flushes history messages added since the last flush — called
|
|
1419
|
+
// right after the user prompt, on every onStep boundary, and once post-turn — so
|
|
1420
|
+
// Ctrl+C / crash / ESC can't lose the prompt + finished steps before /resume.
|
|
1421
|
+
let persistedLen = beforeLen;
|
|
1422
|
+
const persistTurnTail = async (): Promise<void> => {
|
|
1423
|
+
if (!sessionId || history.length <= persistedLen) return;
|
|
1424
|
+
const tail = history.slice(persistedLen);
|
|
1425
|
+
persistedLen = history.length;
|
|
1426
|
+
try { await appendMessages(sessionId, tail, cwd); } catch { /* best-effort */ }
|
|
1427
|
+
};
|
|
1392
1428
|
let result;
|
|
1393
1429
|
try {
|
|
1394
1430
|
// Paint the live frame + spinner the INSTANT the turn is accepted, BEFORE the
|
|
@@ -1413,17 +1449,24 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1413
1449
|
tui?.events().onNotice?.(`(compacted ${compRes.removed} older message${compRes.removed === 1 ? "" : "s"})`);
|
|
1414
1450
|
}
|
|
1415
1451
|
beforeLen = history.length;
|
|
1452
|
+
persistedLen = beforeLen; // re-baseline after compaction mutated history
|
|
1416
1453
|
if (images?.length && catalogMetadata(activeModel)?.images === false) {
|
|
1417
1454
|
const warn = `! ${activeModel} does not advertise image input — sending the attachment anyway.`;
|
|
1418
1455
|
if (tui) tui.events().onNotice?.(warn);
|
|
1419
1456
|
else console.log(warn);
|
|
1420
1457
|
}
|
|
1421
1458
|
history.push(images?.length ? { role: "user", content: userInput, images } : { role: "user", content: userInput });
|
|
1459
|
+
// Persist the user prompt immediately so an interrupted turn keeps it on disk.
|
|
1460
|
+
await persistTurnTail(); // persist the user prompt immediately
|
|
1422
1461
|
// Keep the submitted query in scrollback: the prompt that STARTS a turn shows
|
|
1423
1462
|
// only as the transient HUD turn-title otherwise, which vanishes when the live
|
|
1424
1463
|
// frame clears at turn-end — so the conversation transcript lost every user
|
|
1425
1464
|
// prompt. Flush a `user` card (same surface as a mid-turn steer) so it persists.
|
|
1426
|
-
|
|
1465
|
+
// Flush a `user` card (same surface as a mid-turn steer) so the prompt persists
|
|
1466
|
+
// in scrollback. opts.userCard overrides it: null suppresses the card entirely
|
|
1467
|
+
// (skill runs), a string shows a compact label instead of the raw input.
|
|
1468
|
+
const cardText = opts?.userCard === undefined ? userInput : opts.userCard;
|
|
1469
|
+
if (tui && cardText && cardText.trim()) tui.flushUserCard(cardText);
|
|
1427
1470
|
tui?.setContextUsage(historyTokens(history), contextTokens);
|
|
1428
1471
|
|
|
1429
1472
|
// Per-turn steering inbox (gjc parity): additional queries typed mid-turn land
|
|
@@ -1548,7 +1591,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1548
1591
|
maxTokens: sessionThinking ? thinkingMaxTokens(sessionThinking) : undefined,
|
|
1549
1592
|
signal: ac.signal,
|
|
1550
1593
|
steer: drainSteer,
|
|
1551
|
-
events: wrapEvents({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, opik),
|
|
1594
|
+
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
|
|
1552
1595
|
});
|
|
1553
1596
|
if (result.done && looksLikeSkillEcho(result.doneReason ?? "", resolvedSkills)) {
|
|
1554
1597
|
history.push({
|
|
@@ -1603,13 +1646,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1603
1646
|
|| (result.done
|
|
1604
1647
|
? `(done in ${result.steps} step${result.steps === 1 ? "" : "s"} — the model returned no summary)`
|
|
1605
1648
|
: `(reached the ${result.steps}-step limit without signaling done)`);
|
|
1606
|
-
//
|
|
1607
|
-
//
|
|
1649
|
+
// Persist any messages this turn produced that onStep hasn't flushed yet (the
|
|
1650
|
+
// final step's tool-call/result + any retry-loop messages), then the reply.
|
|
1651
|
+
// Incremental onStep flushes already wrote the prompt and completed steps, so
|
|
1652
|
+
// this only covers the tail — net content is the full turn either way.
|
|
1608
1653
|
try {
|
|
1609
|
-
|
|
1610
|
-
// One batched fs append for the whole turn (was: one awaited append per message).
|
|
1611
|
-
await appendMessages(sessionId, history.slice(beforeLen), cwd);
|
|
1612
|
-
}
|
|
1654
|
+
await persistTurnTail();
|
|
1613
1655
|
history.push({ role: "assistant", content: reply });
|
|
1614
1656
|
if (sessionId) await appendMessage(sessionId, { role: "assistant", content: reply }, cwd);
|
|
1615
1657
|
if (tui) tui.finish(reply);
|
|
@@ -1729,7 +1771,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1729
1771
|
console.log(`▶ Running skill: ${inv.skill.name}${inv.intent ? ` — ${inv.intent}` : ""}`);
|
|
1730
1772
|
}
|
|
1731
1773
|
const task = buildSkillTask(inv.skill, inv.intent, inv.invokedAs);
|
|
1732
|
-
const { reply, rendered, usage } = await runTurn(task, useOneShotTui);
|
|
1774
|
+
const { reply, rendered, usage } = await runTurn(task, useOneShotTui, undefined, { userCard: null });
|
|
1733
1775
|
if (!rendered) console.log(stripMarkdown(renderMarkdownTables(reply)) + usage);
|
|
1734
1776
|
else if (usage) console.log(usage.trim());
|
|
1735
1777
|
};
|
|
@@ -1837,7 +1879,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1837
1879
|
// starts — skill name, resolved SKILL.md path, and the prompt size.
|
|
1838
1880
|
{
|
|
1839
1881
|
const card = formatForgeBox(
|
|
1840
|
-
{ title: "[skill]", lines: skillInvocationCard(skill) },
|
|
1882
|
+
{ title: "[skill]", lines: skillInvocationCard(skill, intent) },
|
|
1841
1883
|
{ width: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
1842
1884
|
);
|
|
1843
1885
|
logLines(card);
|
|
@@ -1918,7 +1960,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1918
1960
|
// final reply is the skill's result.
|
|
1919
1961
|
if (!useTui) console.log(`▶ Running skill: ${skill.name}${intent ? ` — ${intent}` : ""}`);
|
|
1920
1962
|
const task = buildSkillTask(skill, intent, invokedAs);
|
|
1921
|
-
const { reply, rendered, usage } = await runTurn(task, useTui);
|
|
1963
|
+
const { reply, rendered, usage } = await runTurn(task, useTui, undefined, { userCard: null });
|
|
1922
1964
|
if (!rendered) console.log(`jeo> ${stripMarkdown(renderMarkdownTables(reply))}${usage}`);
|
|
1923
1965
|
else if (usage) console.log(usage.trim());
|
|
1924
1966
|
}
|
|
@@ -2299,6 +2341,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2299
2341
|
}
|
|
2300
2342
|
};
|
|
2301
2343
|
let previewPending = false;
|
|
2344
|
+
// Idle-prompt Ctrl+O detail panel: a REVERSIBLE, scrollable overlay drawn inside
|
|
2345
|
+
// the footer reservation. Toggle open/closed with Ctrl+O; ↑↓/PgUp/PgDn scroll so
|
|
2346
|
+
// long/CJK content is fully reachable (no "… N more" clip). null = closed.
|
|
2347
|
+
let promptHistoryLines: string[] | null = null;
|
|
2348
|
+
let promptHistoryScroll = 0;
|
|
2349
|
+
let promptHistoryMaxScroll = 0;
|
|
2350
|
+
let promptHistoryPage = 1;
|
|
2302
2351
|
|
|
2303
2352
|
// Inline boxed-footer rendering with a FIXED reservation (the "@-mention typing
|
|
2304
2353
|
// pushes the box down" fix). The footer reserves its full `footerRows` height
|
|
@@ -2450,6 +2499,44 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2450
2499
|
const preview = (slash.length ? slash : args).map(l => chalk.gray(truncateAnsi(l, cols)));
|
|
2451
2500
|
return [statusBarLine(cols), "", ...input, ...preview].slice(0, footerRows);
|
|
2452
2501
|
};
|
|
2502
|
+
// Render the reversible Ctrl+O detail panel into the footer reservation: a status
|
|
2503
|
+
// bar, a title (with scroll hint when needed), then a windowed slice of the detail
|
|
2504
|
+
// with ↑/↓ counters. Recomputes the scroll bound/page size each paint so scroll
|
|
2505
|
+
// keys can clamp correctly. Mirrors the mid-turn LaunchTui panel.
|
|
2506
|
+
const historyPreviewLines = (detail: string[]): string[] => {
|
|
2507
|
+
const cols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
2508
|
+
const physical = detail.flatMap(line => line.split("\n")).map(line => truncateAnsi(line, cols));
|
|
2509
|
+
const bodyLimit = Math.max(1, footerRows - 2); // status bar + title rows
|
|
2510
|
+
const scrollable = physical.length > bodyLimit;
|
|
2511
|
+
const cap = scrollable ? Math.max(1, bodyLimit - 2) : bodyLimit;
|
|
2512
|
+
promptHistoryPage = cap;
|
|
2513
|
+
promptHistoryMaxScroll = Math.max(0, physical.length - cap);
|
|
2514
|
+
if (promptHistoryScroll > promptHistoryMaxScroll) promptHistoryScroll = promptHistoryMaxScroll;
|
|
2515
|
+
const hint = scrollable ? "· ↑↓/PgUp/PgDn scroll · Ctrl+O closes" : "· Ctrl+O closes";
|
|
2516
|
+
const title = `${uiAccent("history")} ${chalk.dim(hint)}`;
|
|
2517
|
+
let body: string[];
|
|
2518
|
+
if (!scrollable) {
|
|
2519
|
+
body = physical;
|
|
2520
|
+
} else {
|
|
2521
|
+
const start = promptHistoryScroll;
|
|
2522
|
+
const above = start;
|
|
2523
|
+
const reserveTop = above > 0 ? 1 : 0;
|
|
2524
|
+
let innerLimit = Math.max(1, bodyLimit - reserveTop - 1);
|
|
2525
|
+
let win = physical.slice(start, start + innerLimit);
|
|
2526
|
+
let below = physical.length - (start + win.length);
|
|
2527
|
+
if (below === 0) {
|
|
2528
|
+
innerLimit = Math.max(1, bodyLimit - reserveTop);
|
|
2529
|
+
win = physical.slice(start, start + innerLimit);
|
|
2530
|
+
below = physical.length - (start + win.length);
|
|
2531
|
+
}
|
|
2532
|
+
body = [];
|
|
2533
|
+
if (above > 0) body.push(chalk.dim(`↑ ${above} more above`));
|
|
2534
|
+
body.push(...win);
|
|
2535
|
+
if (below > 0) body.push(chalk.dim(`↓ ${below} more below`));
|
|
2536
|
+
}
|
|
2537
|
+
footerCursor = { row: Math.min(1, footerRows - 1), col: 1 };
|
|
2538
|
+
return [statusBarLine(cols), title, ...body].slice(0, footerRows);
|
|
2539
|
+
};
|
|
2453
2540
|
const drawFooter = (lines: string[]) => {
|
|
2454
2541
|
if (!previewArmed || footerRendered === 0) return;
|
|
2455
2542
|
// ALWAYS paint exactly footerRendered rows so the reservation is fully covered
|
|
@@ -2896,24 +2983,41 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2896
2983
|
return;
|
|
2897
2984
|
}
|
|
2898
2985
|
if (!previewArmed || pickerActive) return;
|
|
2899
|
-
// Ctrl+O:
|
|
2900
|
-
//
|
|
2901
|
-
//
|
|
2902
|
-
// (Cmd+O is intercepted by
|
|
2986
|
+
// Ctrl+O: toggle a reversible, scrollable detail panel inside the footer
|
|
2987
|
+
// (expand on the first press, FOLD on the next). ↑↓/PgUp/PgDn scroll it while
|
|
2988
|
+
// open — long/CJK content is fully reachable, nothing is clipped. The live-turn
|
|
2989
|
+
// path uses LaunchTui.showDetail(). (Cmd+O is intercepted by the terminal.)
|
|
2903
2990
|
if (key?.ctrl && key.name === "o") {
|
|
2991
|
+
if (promptHistoryLines) {
|
|
2992
|
+
promptHistoryLines = null; // fold
|
|
2993
|
+
drawFooter(previewLines(typedLine, navIdx));
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2904
2996
|
const detail = composeDetailLines();
|
|
2905
2997
|
if (detail.length === 0) return;
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
// the same path the resize handler/main loop use, so the input box and the
|
|
2910
|
-
// typed draft restore without corruption.
|
|
2911
|
-
disarmPreview();
|
|
2912
|
-
logLines(detail);
|
|
2913
|
-
armPreview();
|
|
2914
|
-
drawFooter(previewLines(typedLine, navIdx));
|
|
2998
|
+
promptHistoryLines = detail;
|
|
2999
|
+
promptHistoryScroll = 0;
|
|
3000
|
+
drawFooter(historyPreviewLines(detail));
|
|
2915
3001
|
return;
|
|
2916
3002
|
}
|
|
3003
|
+
// While the detail panel is open, arrows / PgUp / PgDn scroll it instead of
|
|
3004
|
+
// navigating slash/history; every other key (below) closes it.
|
|
3005
|
+
if (promptHistoryLines && key && (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown")) {
|
|
3006
|
+
const page = key.name === "pageup" || key.name === "pagedown";
|
|
3007
|
+
const dir = key.name === "up" || key.name === "pageup" ? -1 : 1;
|
|
3008
|
+
const step = page ? Math.max(1, promptHistoryPage - 1) : 1;
|
|
3009
|
+
const next = Math.min(promptHistoryMaxScroll, Math.max(0, promptHistoryScroll + dir * step));
|
|
3010
|
+
if (next !== promptHistoryScroll) {
|
|
3011
|
+
promptHistoryScroll = next;
|
|
3012
|
+
drawFooter(historyPreviewLines(promptHistoryLines));
|
|
3013
|
+
}
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (promptHistoryLines) {
|
|
3017
|
+
// Any other key dismisses the panel, then falls through to normal handling.
|
|
3018
|
+
promptHistoryLines = null;
|
|
3019
|
+
drawFooter(previewLines(typedLine, navIdx));
|
|
3020
|
+
}
|
|
2917
3021
|
// Ctrl+V: attach a clipboard IMAGE to the next message. Terminal text paste
|
|
2918
3022
|
// never arrives as a ctrl+v keypress (it streams as plain stdin data), so this
|
|
2919
3023
|
// binding is image-only; when the clipboard holds no image it's a silent no-op.
|
|
@@ -2956,7 +3060,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2956
3060
|
if (!previewArmed) return;
|
|
2957
3061
|
try {
|
|
2958
3062
|
if (key && (key.name === "return" || key.name === "enter")) {
|
|
2959
|
-
drawFooter([])
|
|
3063
|
+
// Redraw the REAL box (not drawFooter([]), which erases it): this runs in a
|
|
3064
|
+
// setImmediate and can fire AFTER the loop already re-armed + repainted the box
|
|
3065
|
+
// for the next prompt (slash command / turn). An empty draw there wiped the fresh
|
|
3066
|
+
// box and parked the cursor at the reservation top — the "input box vanishes and
|
|
3067
|
+
// the caret leaves the prompt after a command" bug. Drawing previewLines keeps the
|
|
3068
|
+
// box present and the caret in it whether this fires before submit or after re-arm.
|
|
3069
|
+
drawFooter(previewLines(typedLine, navIdx));
|
|
2960
3070
|
return;
|
|
2961
3071
|
}
|
|
2962
3072
|
// Arrow up/down: move the highlight over the slash keyword preview list.
|
|
@@ -3016,7 +3126,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3016
3126
|
try {
|
|
3017
3127
|
disarmPreview();
|
|
3018
3128
|
armPreview();
|
|
3019
|
-
drawFooter(previewLines(typedLine, navIdx));
|
|
3129
|
+
drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
|
|
3020
3130
|
} catch { /* ignore resize render races */ }
|
|
3021
3131
|
};
|
|
3022
3132
|
process.stdout.on("resize", idleResizeHandler);
|
|
@@ -3055,6 +3165,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3055
3165
|
// window where the box is gone and keystrokes echo nowhere (the "no response
|
|
3056
3166
|
// after the result" gap). readline's own echo stays gated while armed.
|
|
3057
3167
|
armPreview();
|
|
3168
|
+
promptHistoryLines = null; // each fresh prompt starts with the Ctrl+O panel closed
|
|
3058
3169
|
if (prefilledLine) {
|
|
3059
3170
|
rli.line = prefilledLine;
|
|
3060
3171
|
rli.cursor = prefilledLine.length;
|
|
@@ -3521,7 +3632,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3521
3632
|
}
|
|
3522
3633
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
3523
3634
|
const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
|
|
3524
|
-
|
|
3635
|
+
let name = (tokens[0] ?? "").toLowerCase();
|
|
3525
3636
|
const explicitModel = tokens[1];
|
|
3526
3637
|
// `/provider login|auth [name]` → run OAuth login from the REPL.
|
|
3527
3638
|
if (name === "login" || name === "auth") {
|
|
@@ -3550,20 +3661,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3550
3661
|
console.log(`Starting OAuth login for ${target}…`);
|
|
3551
3662
|
try {
|
|
3552
3663
|
const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
|
|
3553
|
-
|
|
3664
|
+
// Tidy back to the initial query-input screen: the OAuth flow printed
|
|
3665
|
+
// browser prompts / "waiting…" lines that clutter scrollback. Clear the
|
|
3666
|
+
// screen + scrollback and re-render the welcome (same path as /clear),
|
|
3667
|
+
// then a single concise confirmation. lastPickIndex is still seeded so
|
|
3668
|
+
// `/model #N` works; the verbose live-model dump is dropped (it cluttered).
|
|
3554
3669
|
const live = await refreshLiveModelsCache();
|
|
3555
3670
|
const after = (await describeAllProviders()).find(s => s.name === target);
|
|
3556
|
-
if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
|
|
3557
3671
|
const forProvider = live.filter(r => r.provider === target);
|
|
3558
|
-
if (forProvider.some(r => r.ok && r.models.length > 0))
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3672
|
+
if (forProvider.some(r => r.ok && r.models.length > 0)) lastPickIndex = flattenModels(forProvider);
|
|
3673
|
+
// Tidy back to the initial query-input screen: the OAuth flow printed
|
|
3674
|
+
// browser prompts / "waiting…" lines that clutter scrollback. emitLoginCleanup
|
|
3675
|
+
// clears + re-renders the welcome (same path as /clear) then prints one
|
|
3676
|
+
// confirmation; the verbose live-model dump is dropped (lastPickIndex is
|
|
3677
|
+
// still seeded so /model #N works). Orchestration is unit-tested.
|
|
3678
|
+
emitLoginCleanup(
|
|
3679
|
+
{
|
|
3680
|
+
clear: () => { disarmPreview(); process.stdout.write("\x1b[2J\x1b[3J\x1b[H"); },
|
|
3681
|
+
write: line => console.log(line),
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
isTty: process.stdout.isTTY === true,
|
|
3685
|
+
provider: target,
|
|
3686
|
+
email,
|
|
3687
|
+
ready: after?.ready ?? false,
|
|
3688
|
+
label: after?.label,
|
|
3689
|
+
welcomeLines: renderWelcome(welcomeData),
|
|
3690
|
+
},
|
|
3691
|
+
);
|
|
3567
3692
|
} catch (err) {
|
|
3568
3693
|
console.log(`[FAILED] ${(err as Error).message} — or set ${target.toUpperCase()}_API_KEY.`);
|
|
3569
3694
|
}
|
|
@@ -3572,10 +3697,25 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3572
3697
|
const cfgNow = await readGlobalConfig();
|
|
3573
3698
|
const statuses = await describeAllProviders(cfgNow);
|
|
3574
3699
|
if (!name) {
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3700
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3701
|
+
// gjc-style interactive picker (grouped ready / needs-setup, current marked)
|
|
3702
|
+
// instead of a static text dump — arrows + Enter switch, Esc cancels.
|
|
3703
|
+
const current = (await describeModel(sessionModel || cfgNow.defaultModel, cfgNow)).provider;
|
|
3704
|
+
const items = buildProviderChoices(statuses, true, current).map(c => ({
|
|
3705
|
+
value: c.value as string,
|
|
3706
|
+
label: c.label,
|
|
3707
|
+
hint: c.hint,
|
|
3708
|
+
group: c.group,
|
|
3709
|
+
}));
|
|
3710
|
+
const picked = await pickFromOptions("Select a provider ↑↓ move · Enter switch · Esc cancel", items);
|
|
3711
|
+
if (!picked) { console.log("(switch cancelled)"); continue; }
|
|
3712
|
+
name = picked.toLowerCase(); // fall through to the switch logic below
|
|
3713
|
+
} else {
|
|
3714
|
+
console.log("Providers (credential · base URL):");
|
|
3715
|
+
logLines(formatProviderPanel(statuses));
|
|
3716
|
+
console.log("Switch with: /provider <name> [model] · choose models: /model");
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3579
3719
|
}
|
|
3580
3720
|
if (!isProviderName(name)) {
|
|
3581
3721
|
console.log(`Unknown provider '${name}'. Known: ${statuses.map(s => s.name).join(", ")}.`);
|
package/src/skills/catalog.ts
CHANGED
|
@@ -450,13 +450,20 @@ export async function loadSkills(cwd: string = process.cwd()): Promise<SkillDoc[
|
|
|
450
450
|
/** gjc-style skill-invocation card body (the `[skill]` block shown in the TUI
|
|
451
451
|
* when `$name`//skill runs): name, resolved SKILL.md path (or the bundled
|
|
452
452
|
* module path), and the prompt size actually injected. Pure — testable. */
|
|
453
|
-
export function skillInvocationCard(skill: SkillDoc): string[] {
|
|
453
|
+
export function skillInvocationCard(skill: SkillDoc, intent?: string): string[] {
|
|
454
454
|
const promptLines = (skill.raw ?? skill.details ?? "").split("\n").filter(l => l.trim().length > 0).length;
|
|
455
455
|
// jeo-ref tree-connector detail: the skill name leads, resolved metadata hangs
|
|
456
|
-
// off ├─/└─ connectors so the card scans like the reference's Skill panel.
|
|
456
|
+
// off ├─/└─ connectors so the card scans like the reference's Skill panel. The
|
|
457
|
+
// intent (when given) is shown here so it isn't lost — the injected SKILL.md is
|
|
458
|
+
// NOT echoed as a user box (gjc-style: compact card, not the raw doc).
|
|
459
|
+
const trimmedIntent = intent?.trim();
|
|
460
|
+
const intentLine = trimmedIntent
|
|
461
|
+
? [`├─ intent: ${trimmedIntent.length > 88 ? `${trimmedIntent.slice(0, 87)}…` : trimmedIntent}`]
|
|
462
|
+
: [];
|
|
457
463
|
return [
|
|
458
464
|
`Skill: ${skill.name}`,
|
|
459
465
|
`├─ path: ${skill.sourcePath ?? `(bundled) src/prompts/skills/${skill.name}/SKILL.md`}`,
|
|
466
|
+
...intentLine,
|
|
460
467
|
`└─ prompt: ${promptLines} lines`,
|
|
461
468
|
];
|
|
462
469
|
}
|
package/src/tui/app.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { evolutionTrack, createStageProgress, type StageProgress, transitionMess
|
|
|
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
|
-
import { SECTION_GAP, stackSections } from "./components/section";
|
|
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
27
|
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, type ForgeSummary } from "./components/forge";
|
|
@@ -414,12 +414,21 @@ export class LaunchTui {
|
|
|
414
414
|
this.thinking = false; // model replied; now dispatching the tool
|
|
415
415
|
this.retryNotice = null; // the call got through — clear any backoff notice
|
|
416
416
|
// Flush the streamed reasoning once into scrollback as a jeo-ref reasoning
|
|
417
|
-
// block —
|
|
418
|
-
//
|
|
417
|
+
// block — a muted "Reasoning" divider header, the prose below it (the durable
|
|
418
|
+
// record) — then stop showing the transient live reasoning row.
|
|
419
419
|
if (this.streamingReasoning && this.streamingReasoning !== this.flushedReasoning) {
|
|
420
420
|
this.flushedReasoning = this.streamingReasoning;
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
// A muted "Reasoning" card-header divider announces the reasoning block
|
|
422
|
+
// boundary (consistent with the section design tokens — Thinking/Reasoning/
|
|
423
|
+
// Output share one visual language); the prose below it is the durable record.
|
|
424
|
+
// A full-width divider ROW respects appendLedger's 1-line=1-row pre-wrap
|
|
425
|
+
// invariant (no per-line prefix), unlike a left-border enclosure which would
|
|
426
|
+
// push wrapped lines past `cols` and tear the frame.
|
|
427
|
+
const header = sectionLabel("Reasoning", Math.max(20, size().cols), {
|
|
428
|
+
color: this.theme.color,
|
|
429
|
+
unicode: this.unicode,
|
|
430
|
+
});
|
|
431
|
+
this.appendLedger(`${header}\n${this.streamingReasoning}\n`, "reasoning");
|
|
423
432
|
}
|
|
424
433
|
this.streamingReasoning = "";
|
|
425
434
|
this.streamingThought = "";
|
|
@@ -766,6 +775,15 @@ export class LaunchTui {
|
|
|
766
775
|
// (e.g. "rate limited (HTTP 429) — auto-retry #2 in 4s") instead of an opaque
|
|
767
776
|
// ever-growing "calling model (18.4s)…".
|
|
768
777
|
if (this.retryNotice) return `${this.retryNotice} (${elapsed}s)`;
|
|
778
|
+
// Reasoning is streaming → the live thought block already shows it; label the row.
|
|
779
|
+
if (this.streamingThought.trim() || this.streamingReasoning.trim()) {
|
|
780
|
+
return `reasoning (${this.footer.model}) (${elapsed}s)…`;
|
|
781
|
+
}
|
|
782
|
+
// No tokens after a few seconds: the model is almost certainly reasoning
|
|
783
|
+
// server-side (e.g. OpenAI hidden reasoning), NOT hung — say so instead of a
|
|
784
|
+
// frozen "calling model …" so a long silent wait still reads as progress.
|
|
785
|
+
const waited = this.currentStepStartedAt ? (Date.now() - this.currentStepStartedAt) / 1000 : 0;
|
|
786
|
+
if (waited >= 8) return `thinking (${this.footer.model}) — reasoning, no token stream yet (${elapsed}s)…`;
|
|
769
787
|
return `calling model (${this.footer.model}) (${elapsed}s)…`;
|
|
770
788
|
}
|
|
771
789
|
if (running) {
|
|
@@ -1227,7 +1245,7 @@ export class LaunchTui {
|
|
|
1227
1245
|
// (duplicate model bar) is gone; height now toggles only at lifecycle boundaries.
|
|
1228
1246
|
const ROWS = 6;
|
|
1229
1247
|
const shown = wrapped.slice(-ROWS);
|
|
1230
|
-
tail.push(
|
|
1248
|
+
tail.push(sectionLabel("Thinking", Math.max(8, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
|
|
1231
1249
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1232
1250
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1233
1251
|
tail.push("");
|
|
@@ -1246,7 +1264,7 @@ export class LaunchTui {
|
|
|
1246
1264
|
// so cumulative stdout growth does not thrash the frame height.
|
|
1247
1265
|
const ROWS = 8;
|
|
1248
1266
|
const shown = wrapped.slice(-ROWS);
|
|
1249
|
-
tail.push(
|
|
1267
|
+
tail.push(sectionLabel("Output", Math.max(8, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
|
|
1250
1268
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1251
1269
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1252
1270
|
tail.push("");
|
|
@@ -46,6 +46,35 @@ export function formatProviderPanel(statuses: ProviderStatus[]): string[] {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Side-effect sinks for {@link emitLoginCleanup} — injected so the orchestration
|
|
50
|
+
* (ordering + conditionals + the confirmation string) is unit-testable without a
|
|
51
|
+
* real terminal. The caller wires `clear` to the actual screen+scrollback escape. */
|
|
52
|
+
export interface LoginCleanupIO {
|
|
53
|
+
clear: () => void;
|
|
54
|
+
write: (line: string) => void;
|
|
55
|
+
}
|
|
56
|
+
export interface LoginCleanupOpts {
|
|
57
|
+
isTty: boolean;
|
|
58
|
+
provider: string;
|
|
59
|
+
email?: string;
|
|
60
|
+
ready: boolean;
|
|
61
|
+
label?: string;
|
|
62
|
+
welcomeLines: string[];
|
|
63
|
+
}
|
|
64
|
+
/** Tidy the screen after a successful `/provider login`: on a TTY clear the screen
|
|
65
|
+
* and re-render the welcome (drops the OAuth flow's browser/"waiting…" noise — the
|
|
66
|
+
* same path `/clear` uses), then print ONE confirmation line; off a TTY just the
|
|
67
|
+
* confirmation. Pure orchestration over the injected IO. */
|
|
68
|
+
export function emitLoginCleanup(io: LoginCleanupIO, opts: LoginCleanupOpts): void {
|
|
69
|
+
if (opts.isTty) {
|
|
70
|
+
io.clear();
|
|
71
|
+
io.write(opts.welcomeLines.join("\n"));
|
|
72
|
+
}
|
|
73
|
+
const who = opts.email ? ` (${opts.email})` : "";
|
|
74
|
+
const status = !opts.ready && opts.label ? ` — ${opts.label}` : "";
|
|
75
|
+
io.write(`✓ Logged in to ${opts.provider}${who}${status}. Pick a model with /model.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/** Subagent roster: ` id title — model · thinking ≤N steps (read-only)`. */
|
|
50
79
|
export function formatAgentsPanel(
|
|
51
80
|
roles: readonly SubagentRole[],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ColorLevel, hexToRgb, lerpColor, fgEscape, bgEscape, resetEscape, stripAnsi } from "./color";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -33,10 +34,10 @@ export const EVOLUTION_STAGE_COLORS: readonly ((s: string) => string)[] = [
|
|
|
33
34
|
/** Spinner frame sets, one per evolution stage. */
|
|
34
35
|
export const EVOLUTION_SPINNER_FRAMES: readonly string[][] = [
|
|
35
36
|
[". ", ".. ", "... ", "....", "... ", ".. "],
|
|
36
|
-
["\u2801", "\
|
|
37
|
-
["
|
|
37
|
+
["\u2801", "\u2803", "\u2807", "\u2827", "\u2837", "\u283f", "\u283e", "\u283d", "\u283b", "\u2819", "\u2818", "\u2810"],
|
|
38
|
+
["▲", "▶", "▼", "◀"],
|
|
38
39
|
["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834", "\u2826", "\u2827", "\u2807", "\u280f"],
|
|
39
|
-
["\
|
|
40
|
+
["\u25f0", "\u25f3", "\u25f2", "\u25f1"],
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -303,3 +304,81 @@ export const EVOLUTION_TRANSITION_MESSAGES: readonly string[] = [
|
|
|
303
304
|
export function transitionMessage(index: number): string {
|
|
304
305
|
return EVOLUTION_TRANSITION_MESSAGES[clampStageIndex(index)]!;
|
|
305
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Applies a dimensional, animated gradient (foreground or background) to a string of text.
|
|
309
|
+
* Driven by a phaseMs timestamp, it creates a slow, tasteful shimmer wave
|
|
310
|
+
* and adds depth cues (bright core, dimmer edges).
|
|
311
|
+
*/
|
|
312
|
+
export function applyDimensionalGradient(
|
|
313
|
+
text: string,
|
|
314
|
+
timeMs: number,
|
|
315
|
+
fromHex: string,
|
|
316
|
+
toHex: string,
|
|
317
|
+
level: ColorLevel,
|
|
318
|
+
isBg: boolean,
|
|
319
|
+
fgHex: string = "#ebebeb"
|
|
320
|
+
): string {
|
|
321
|
+
const plain = stripAnsi(text);
|
|
322
|
+
if (level === ColorLevel.None || plain.length === 0) return plain;
|
|
323
|
+
|
|
324
|
+
const from = hexToRgb(fromHex);
|
|
325
|
+
const to = hexToRgb(toHex);
|
|
326
|
+
const fg = hexToRgb(fgHex);
|
|
327
|
+
|
|
328
|
+
const L = plain.length;
|
|
329
|
+
let out = "";
|
|
330
|
+
if (isBg) {
|
|
331
|
+
out += fgEscape(fg, level);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Slow period: 4.5 seconds for a complete wave cycle
|
|
335
|
+
const p = (timeMs / 4500) % 1;
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < L; i++) {
|
|
338
|
+
const ch = plain[i]!;
|
|
339
|
+
if (!isBg && ch === " ") {
|
|
340
|
+
out += ch;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const x = L > 1 ? i / (L - 1) : 0;
|
|
345
|
+
const base = lerpColor(from, to, x);
|
|
346
|
+
|
|
347
|
+
// 3D depth cue: bright center (1.0), dimmer edges (0.75)
|
|
348
|
+
// Using a parabolic/sin curve
|
|
349
|
+
const depth = 0.75 + 0.25 * Math.sin(x * Math.PI);
|
|
350
|
+
|
|
351
|
+
// Shimmer highlight wave
|
|
352
|
+
// Shortest circular distance between position x and wave phase p
|
|
353
|
+
let dist = Math.abs(x - p);
|
|
354
|
+
if (dist > 0.5) dist = 1 - dist;
|
|
355
|
+
|
|
356
|
+
const waveWidth = 0.22;
|
|
357
|
+
const highlight = dist < waveWidth ? Math.cos((dist / waveWidth) * (Math.PI / 2)) : 0;
|
|
358
|
+
|
|
359
|
+
// Apply depth
|
|
360
|
+
let r = base.r * depth;
|
|
361
|
+
let g = base.g * depth;
|
|
362
|
+
let b = base.b * depth;
|
|
363
|
+
|
|
364
|
+
// Apply shimmer highlight (blending towards white)
|
|
365
|
+
const shimmerIntensity = 0.45;
|
|
366
|
+
r += (255 - r) * highlight * shimmerIntensity;
|
|
367
|
+
g += (255 - g) * highlight * shimmerIntensity;
|
|
368
|
+
b += (255 - b) * highlight * shimmerIntensity;
|
|
369
|
+
|
|
370
|
+
const rgb = {
|
|
371
|
+
r: Math.max(0, Math.min(255, Math.round(r))),
|
|
372
|
+
g: Math.max(0, Math.min(255, Math.round(g))),
|
|
373
|
+
b: Math.max(0, Math.min(255, Math.round(b))),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (isBg) {
|
|
377
|
+
out += bgEscape(rgb, level) + ch;
|
|
378
|
+
} else {
|
|
379
|
+
out += fgEscape(rgb, level) + ch;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return out + resetEscape(level);
|
|
384
|
+
}
|
|
@@ -419,8 +419,8 @@ export function summarizeForgeResult(tool: string, success: boolean, output: str
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
const lines = previewLines(body, success ? 5 : 10, success ? 600 : 1200);
|
|
422
|
+
lines.unshift(forgeDivider("Output"));
|
|
422
423
|
if (normalized === "bash") {
|
|
423
|
-
lines.unshift(forgeDivider("Output"));
|
|
424
424
|
if (exitNote) lines.push("", exitNote);
|
|
425
425
|
}
|
|
426
426
|
return {
|
|
@@ -74,33 +74,85 @@ export function renderMarkdownAnsi(text: string, opts: MarkdownAnsiOptions = {})
|
|
|
74
74
|
|
|
75
75
|
const out: string[] = [];
|
|
76
76
|
let inFence = false;
|
|
77
|
+
|
|
78
|
+
// Track the type of the last pushed content line to manage rhythm
|
|
79
|
+
// Types: "empty" | "heading" | "blockquote" | "list" | "code" | "prose"
|
|
80
|
+
let lastType: "empty" | "heading" | "blockquote" | "list" | "code" | "prose" = "empty";
|
|
81
|
+
|
|
82
|
+
const ensureBlankLine = () => {
|
|
83
|
+
if (out.length > 0 && out[out.length - 1] !== "") {
|
|
84
|
+
out.push("");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
77
88
|
for (const line of text.split("\n")) {
|
|
78
|
-
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
|
|
91
|
+
// Check if we are toggling a code fence
|
|
92
|
+
if (/^```/.test(trimmed)) {
|
|
93
|
+
ensureBlankLine();
|
|
79
94
|
inFence = !inFence;
|
|
80
|
-
|
|
95
|
+
lastType = inFence ? "code" : "empty";
|
|
96
|
+
continue;
|
|
81
97
|
}
|
|
98
|
+
|
|
82
99
|
if (inFence) {
|
|
83
|
-
out.push(line); // code bodies verbatim
|
|
100
|
+
out.push(line); // code bodies verbatim
|
|
101
|
+
lastType = "code";
|
|
84
102
|
continue;
|
|
85
103
|
}
|
|
104
|
+
|
|
105
|
+
if (trimmed === "") {
|
|
106
|
+
ensureBlankLine();
|
|
107
|
+
lastType = "empty";
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
87
112
|
if (heading) {
|
|
88
|
-
|
|
89
|
-
// breathing room above it (final-report readability), never a leading blank.
|
|
90
|
-
if (out.length > 0 && out[out.length - 1]!.trim() !== "") out.push("");
|
|
113
|
+
ensureBlankLine();
|
|
91
114
|
out.push(accent(styleInline(heading[2]!)));
|
|
115
|
+
lastType = "heading";
|
|
92
116
|
continue;
|
|
93
117
|
}
|
|
118
|
+
|
|
94
119
|
const quote = line.match(/^>\s+(.+)$/);
|
|
95
120
|
if (quote) {
|
|
121
|
+
if (lastType !== "blockquote" && lastType !== "empty") {
|
|
122
|
+
ensureBlankLine();
|
|
123
|
+
}
|
|
96
124
|
out.push(chalk.dim(`▎ ${styleInline(quote[1]!)}`));
|
|
125
|
+
lastType = "blockquote";
|
|
97
126
|
continue;
|
|
98
127
|
}
|
|
128
|
+
|
|
99
129
|
if (/^[-\*_]{3,}\s*$/.test(line)) {
|
|
130
|
+
ensureBlankLine();
|
|
100
131
|
out.push(chalk.dim("─".repeat(24)));
|
|
132
|
+
ensureBlankLine();
|
|
133
|
+
lastType = "empty";
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if list item
|
|
138
|
+
const isList = /^\s*([*\-+]\s|\d+\.\s)/.test(line);
|
|
139
|
+
if (isList) {
|
|
140
|
+
if (lastType !== "list" && lastType !== "empty") {
|
|
141
|
+
ensureBlankLine();
|
|
142
|
+
}
|
|
143
|
+
out.push(styleInline(line));
|
|
144
|
+
lastType = "list";
|
|
101
145
|
continue;
|
|
102
146
|
}
|
|
147
|
+
|
|
148
|
+
// Default prose line
|
|
149
|
+
if (lastType !== "prose" && lastType !== "empty" && lastType !== "list" && lastType !== "heading") {
|
|
150
|
+
// Transitioning from list/quote/code to prose -> ensure blank line
|
|
151
|
+
ensureBlankLine();
|
|
152
|
+
}
|
|
103
153
|
out.push(styleInline(line));
|
|
154
|
+
lastType = "prose";
|
|
104
155
|
}
|
|
156
|
+
|
|
105
157
|
return out.join("\n").trim();
|
|
106
158
|
}
|
|
@@ -16,14 +16,16 @@ export function providerHint(s: ProviderStatus, unicode = true): string {
|
|
|
16
16
|
return parts.join(" \u00b7 ");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Build provider choices, ready providers first (stable within each group).
|
|
20
|
-
|
|
19
|
+
/** Build provider choices, ready providers first (stable within each group). When
|
|
20
|
+
* `current` is given, that provider's row is marked `· ● current` so the active
|
|
21
|
+
* provider is obvious in the picker (gjc-style). */
|
|
22
|
+
export function buildProviderChoices(statuses: ProviderStatus[], unicode = true, current?: ProviderName): SelectItem<ProviderName>[] {
|
|
21
23
|
const sorted = [...statuses].sort((a, b) => (a.ready === b.ready ? 0 : a.ready ? -1 : 1));
|
|
22
24
|
return sorted.map(s => ({
|
|
23
25
|
value: s.name,
|
|
24
26
|
label: `${s.name} (${companyLabel(s.name)})`,
|
|
25
27
|
group: s.ready ? "ready" : "needs setup",
|
|
26
|
-
hint: providerHint(s, unicode),
|
|
28
|
+
hint: s.name === current ? `${providerHint(s, unicode)} · ${unicode ? "●" : "*"} current` : providerHint(s, unicode),
|
|
27
29
|
}));
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -29,7 +29,10 @@ export interface SectionLabelOpts {
|
|
|
29
29
|
export function sectionLabel(title: string, width: number, opts: SectionLabelOpts = {}): string {
|
|
30
30
|
const unicode = opts.unicode !== false;
|
|
31
31
|
const dash = unicode ? "─" : "-";
|
|
32
|
-
|
|
32
|
+
// A 3-dash lead-in makes the card header read as a distinct boundary (stronger than a
|
|
33
|
+
// single rule) without becoming loud.
|
|
34
|
+
const startDash = unicode ? "───" : "---";
|
|
35
|
+
const head = `${startDash} ${title.trim()} `;
|
|
33
36
|
const headW = visibleWidth(head);
|
|
34
37
|
const fill = Math.max(0, Math.trunc(width) - headW);
|
|
35
38
|
const line = head + dash.repeat(fill);
|
|
@@ -59,12 +62,23 @@ export function stackSections(sections: Section[], opts: StackOptions): string[]
|
|
|
59
62
|
const gap = Math.max(0, opts.gap ?? SECTION_GAP);
|
|
60
63
|
const out: string[] = [];
|
|
61
64
|
for (const section of sections) {
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const lines = [...section.lines];
|
|
66
|
+
while (lines.length && lines[0] === "") {
|
|
67
|
+
lines.shift();
|
|
68
|
+
}
|
|
69
|
+
while (lines.length && lines[lines.length - 1] === "") {
|
|
70
|
+
lines.pop();
|
|
71
|
+
}
|
|
72
|
+
if (!lines.length) continue;
|
|
73
|
+
if (out.length) {
|
|
74
|
+
for (let i = 0; i < gap; i++) out.push("");
|
|
75
|
+
}
|
|
64
76
|
if (section.title) {
|
|
65
77
|
out.push(sectionLabel(section.title, opts.width, { color: opts.color, unicode: opts.unicode }));
|
|
66
78
|
}
|
|
67
|
-
for (const line of
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
out.push(line);
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
return out;
|
|
70
84
|
}
|
|
@@ -4,6 +4,7 @@ import { spinnerFramesFor, stageIndexForStep, clampStageIndex } from "./evolutio
|
|
|
4
4
|
* Stage-aware spinner. Frames evolve with the agent's step against its budget,
|
|
5
5
|
* sourced from the canonical evolution model so the spinner stays in lockstep
|
|
6
6
|
* with the ASCII art, meter, and footer track.
|
|
7
|
+
* Enriched with 3D/dimensional quadrant and twisting braille helix elements.
|
|
7
8
|
*/
|
|
8
9
|
export class Spinner {
|
|
9
10
|
private defaultFrames: string[];
|
|
@@ -5,6 +5,7 @@ import { animatedGradientText, applyBgGradient, hexToRgb, visibleWidth, ColorLev
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import { formatUsage } from "./duration";
|
|
7
7
|
import { formatCost } from "../../ai/pricing";
|
|
8
|
+
import { applyDimensionalGradient, stageGradient, stageIndexForStep } from "./evolution";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* One-row status bar pinned directly above the boxed input (gjc-layout parity):
|
|
@@ -100,7 +101,7 @@ export function renderStatusBar(d: StatusBarData): string {
|
|
|
100
101
|
const level = d.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
101
102
|
const grad = d.gradient ?? { from: "#0a3d62", to: "#48dbfb" };
|
|
102
103
|
const paintedLeft = useColor
|
|
103
|
-
?
|
|
104
|
+
? applyDimensionalGradient(left, Date.now(), grad.from, grad.to, level, true)
|
|
104
105
|
: left;
|
|
105
106
|
return `${paintedLeft}${" ".repeat(gap)}${right}`;
|
|
106
107
|
}
|
|
@@ -151,9 +152,19 @@ export function renderJeoStatus(data: JeoStatusData): string[] {
|
|
|
151
152
|
const elapsed = `${seconds(data.elapsedMs)}s`;
|
|
152
153
|
let msg = data.message ?? "thinking through the next tool call";
|
|
153
154
|
const level = data.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
154
|
-
if (useColor &&
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
if (useColor && level !== ColorLevel.None && (data.isThinking !== false)) {
|
|
156
|
+
let fromHex = "#0a3d62";
|
|
157
|
+
let toHex = "#48dbfb";
|
|
158
|
+
if (data.palette && data.palette.length > 0) {
|
|
159
|
+
fromHex = data.palette[0]!;
|
|
160
|
+
toHex = data.palette[data.palette.length - 1]!;
|
|
161
|
+
} else {
|
|
162
|
+
const stageIdx = stageIndexForStep(data.step ?? 0, data.maxSteps ?? 25);
|
|
163
|
+
const grad = stageGradient(stageIdx);
|
|
164
|
+
fromHex = grad.from;
|
|
165
|
+
toHex = grad.to;
|
|
166
|
+
}
|
|
167
|
+
msg = applyDimensionalGradient(msg, Date.now(), fromHex, toHex, level, false);
|
|
157
168
|
}
|
|
158
169
|
const current = data.currentTool ? `forging ${data.currentTool}` : "forge idle";
|
|
159
170
|
const stage = data.stage ? `${data.stage} · ` : "";
|
|
@@ -260,8 +271,19 @@ export function renderStatusBox(data: StatusBoxData): string[] {
|
|
|
260
271
|
activity = cut + (unicode ? "…" : "...");
|
|
261
272
|
}
|
|
262
273
|
const plainActivityWidth = visibleWidth(activity);
|
|
263
|
-
if (useColor &&
|
|
264
|
-
|
|
274
|
+
if (useColor && level !== ColorLevel.None && (data.isThinking !== false)) {
|
|
275
|
+
let fromHex = "#0a3d62";
|
|
276
|
+
let toHex = "#48dbfb";
|
|
277
|
+
if (data.palette && data.palette.length > 0) {
|
|
278
|
+
fromHex = data.palette[0]!;
|
|
279
|
+
toHex = data.palette[data.palette.length - 1]!;
|
|
280
|
+
} else {
|
|
281
|
+
const stageIdx = stageIndexForStep(data.step ?? 0, data.maxSteps ?? 25);
|
|
282
|
+
const grad = stageGradient(stageIdx);
|
|
283
|
+
fromHex = grad.from;
|
|
284
|
+
toHex = grad.to;
|
|
285
|
+
}
|
|
286
|
+
activity = applyDimensionalGradient(activity, Date.now(), fromHex, toHex, level, false);
|
|
265
287
|
}
|
|
266
288
|
const escPad = esc
|
|
267
289
|
? " ".repeat(Math.max(1, cols - 1 - visibleWidth(headPlain) - plainActivityWidth - visibleWidth(esc))) + dim(esc)
|
|
@@ -131,6 +131,9 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
131
131
|
: [];
|
|
132
132
|
const toolCalls = calls.filter(c => c.tool !== "done");
|
|
133
133
|
if (toolCalls.length > 0) {
|
|
134
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
135
|
+
lines.push("");
|
|
136
|
+
}
|
|
134
137
|
// The matching `Tool [x] result (ok|fail)` user message follows; for a batch it
|
|
135
138
|
// is ONE message with several blocks. Parse verdicts in call order.
|
|
136
139
|
const next = messages[i + 1];
|
|
@@ -153,6 +156,9 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
153
156
|
? ""
|
|
154
157
|
: m.content;
|
|
155
158
|
if (!reason.trim()) continue;
|
|
159
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
156
162
|
lines.push(`${magentaBold(`jeo ${jeoMark}`)}`);
|
|
157
163
|
lines.push(...clipBody(reason.trim(), bodyCap));
|
|
158
164
|
}
|
package/src/util/retry.ts
CHANGED
|
@@ -61,6 +61,15 @@ export function defaultRetryable(err: unknown): boolean {
|
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Transient empty 200s — a provider returned a successful response with no content.
|
|
65
|
+
// This is a known intermittent failure (load/edge races on Anthropic/Gemini/OpenAI), so
|
|
66
|
+
// retry it like an overload instead of letting one empty reply drop the turn. EXCEPTION:
|
|
67
|
+
// deterministic budget exhaustion (max_tokens / length / "output budget exhausted") re-empties
|
|
68
|
+
// on every retry — fail fast so the caller sees the raise-maxTokens/lower-thinking hint.
|
|
69
|
+
if (lowerMessage.includes("returned no content")) {
|
|
70
|
+
return !/max_tokens|max_output_tokens|finish_reason=length|done_reason=length|output budget exhausted/.test(lowerMessage);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
// Numeric `.status` field (structured provider errors, fetch responses).
|
|
65
74
|
if (typeof err === "object" && err !== null) {
|
|
66
75
|
const status = (err as any).status;
|