jeo-code 0.6.32 → 0.6.34
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 +29 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/engine.ts +9 -1
- package/src/agent/session.ts +43 -5
- package/src/commands/launch/tmux.ts +5 -1
- package/src/commands/launch.ts +53 -14
- package/src/prompts/agents/executor.md +1 -0
- package/src/tui/app.ts +5 -6
- package/src/tui/components/ascii-art.ts +63 -37
- package/src/tui/components/forge.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.33] - 2026-06-19
|
|
10
|
+
## [0.6.34] - 2026-06-20
|
|
11
|
+
_Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate._
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Sessions remember their model (per-session model selection).** The session JSONL header now carries an optional `model` field: `createSession(cwd, id, model?)` pins it, `updateSessionModel(id, model)` rewrites it in place (no message loss, byte-identical no-op when unchanged), and `loadSession`/`listSessions` restore it. In `launch.ts`, every model change — the `/model` picker, a `model …` action, the OpenAI-compatible-endpoint setter, and live picker selections — is persisted into the active session via a best-effort `persistSessionModel()` (a header-rewrite failure never aborts the turn). On `/resume` (and `--resume`) the session's pinned model is restored unless the CLI explicitly pinned one (`--model`/role/provider wins), so each session can carry its own model independent of the global default. The `--resume` list and the resume picker surface the pinned model (`[provider/model]`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **`jeo --tmux` reports a failed attach instead of vanishing.** A nonzero `tmux attach` exit (e.g. `open terminal failed: not a terminal` when stdout isn't a real TTY, a too-small client, or a transient server error) used to be swallowed — jeo returned 0 and left the freshly created session orphaned with no hint. The attach exit code is now surfaced and propagated to `process.exitCode`, and the message is honest about state: it advises `tmux attach -t <session>` only when the session is STILL live, and otherwise reports the session `ended before it could be attached` (so "reattach" is never misleading after an instant inner crash).
|
|
18
|
+
- **tmux session names no longer produce a double dash.** `tmuxSafeNamePart` now trims a trailing dash off the truncated head before appending the disambiguating hash, so a truncation boundary landing right after a `-` yields `name-<hash>` instead of an ugly `name--<hash>`.
|
|
19
|
+
- **`renameSession` shares one header-rewrite path.** Both the rename and the new model-pin go through a single internal `rewriteSessionHeader(id, mutate, cwd)` that locates the JSONL header, applies a mutator (returning `false` to skip the write when nothing changed), and rewrites the file in place — one place for the missing-file/missing-header error handling.
|
|
20
|
+
|
|
21
|
+
### Verified
|
|
22
|
+
- **No bun memory leak / slowdown.** `scripts/mem-probe.ts` drove 2000–4000 realistic LaunchTui turns: the post-GC heap keeps returning to a flat settled floor (~4.3 MB across turns 200→3400, net **+0.52 MB** vs baseline), with `exit`/`resize`/`SIGINT` process-listener counts stable (no accumulation). The probe's net-growth gate was hardened to measure the **settled floor** (min over the trailing half of samples) rather than the single final sample, since Bun's incremental GC leaves the per-sample heap bimodal — a final sample landing on a transient pre-collection peak was a measurement artifact, not retained memory.
|
|
23
|
+
- **`jeo --tmux` live.** `tmux-verify.sh smoke` OK + `battery` **6/6 PASSED** (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
|
|
24
|
+
- **Green gates.** `bun run typecheck` clean; `bun test` **1714 pass / 0 fail** (211 files), including the new per-session-model round-trip (`test/session.test.ts`) and tmux attach-failure / double-dash cases (`test/tmux.test.ts`).
|
|
25
|
+
|
|
26
|
+
_A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification._
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **The forge mark is a hollow line-board emblem now.** `FORGE_MARK_ART`, its grand `FORGE_MARK_ART_GRAND` hero variant, the claw-snap blink frames (`FORGE_MARK_FRAMES`), and every ASCII fallback (`*_ASCII`) were redrawn as the `>-<` silhouette of two pincer CLAWS (집게) whose top arms bend inward toward a narrow central eyeglass-frame (안경태) BRIDGE — each stroke a thick rounded-corner tube (`╭╮╰╯` + `─│`, ASCII `.-'|`) so the shape reads as a heavy neon outline instead of a filled block. The old wordmark glyphs (`J E O`) and the `╳` DNA double-helix nodes are gone; every line stays width-1 and equal-width so the blue→violet→pink flow gradient and the padding/centering math are untouched.
|
|
30
|
+
- **Forge cards render at a compact reduced width.** New `FORGE_SCALE` (1.2) + `scaleForgeWidth(available)` in `forge.ts` divide the caller's available column run down to a compact panel width (floored at 24). `app.ts` routes both the inline `flushForgeCard` and the static forge summary through it, so a card reads as a contained panel instead of stretching edge-to-edge with a dead right margin.
|
|
31
|
+
- **One source for the done-time verification directive.** New `VERIFICATION_DIRECTIVE` constant in `engine.ts` replaces the string that was duplicated verbatim in `executorSystemPrompt`'s default and `launch.ts`'s interactive prompt. It folds in gjc's `<verification>` test-quality contract — written tests must exercise observable behavior, edge values, branch conditions, invariants, and error handling, never asserting defaults or tautologies — and `prompts/agents/executor.md` gains a matching constraint line.
|
|
32
|
+
|
|
33
|
+
### Verified
|
|
34
|
+
- **No bun memory leak / slowdown.** `scripts/mem-probe.ts` drove 2000 realistic LaunchTui turns: post-GC heap returns to baseline (per-turn slope **−480 bytes/turn**, net +3.28 MB held flat), with `exit`/`resize`/`SIGINT` process-listener counts stable (no accumulation).
|
|
35
|
+
- **`jeo --tmux` live.** `scripts/tmux-verify.sh smoke` OK and `battery` **6/6 PASSED** (boot, `/help`, unknown `$skill` feedback, `/agents` roster, `$ultragoal` dispatch, unresolved `/command` report).
|
|
36
|
+
- **Green gates.** `bun run typecheck` clean; `bun test` **1710 pass / 0 fail** across 211 files.
|
|
37
|
+
|
|
9
38
|
## [0.6.32] - 2026-06-19
|
|
10
39
|
_Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification._
|
|
11
40
|
|
package/README.ja.md
CHANGED
|
@@ -200,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
200
200
|
## 変更履歴 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.33]** (2026-06-19)
|
|
204
|
+
- **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
|
|
203
205
|
- **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
206
|
- **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
|
|
205
207
|
- **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
|
|
206
|
-
- **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
|
|
207
|
-
- **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -200,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
200
200
|
## 변경 이력 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.33]** (2026-06-19)
|
|
204
|
+
- **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
|
|
203
205
|
- **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
206
|
- **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
|
|
205
207
|
- **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
|
|
206
|
-
- **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
|
|
207
|
-
- **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -200,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
200
200
|
## Changelog
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.33]** (2026-06-19)
|
|
204
|
+
- **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
|
|
203
205
|
- **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
206
|
- **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
|
|
205
207
|
- **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
|
|
206
|
-
- **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
|
|
207
|
-
- **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -200,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
200
200
|
## 更新日志 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.33]** (2026-06-19)
|
|
204
|
+
- **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
|
|
203
205
|
- **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
|
|
204
206
|
- **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
|
|
205
207
|
- **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
|
|
206
|
-
- **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
|
|
207
|
-
- **[0.6.28]** (2026-06-19) — Signed thinking-block replay: native reasoning is now sent BACK to providers across steps/turns, restoring multi-step reasoning continuity (gajae parity).
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/engine.ts
CHANGED
|
@@ -164,10 +164,18 @@ export const OUTPUT_DISCIPLINE = [
|
|
|
164
164
|
"- Match reply length to the task: a one-line change gets a one-line report.",
|
|
165
165
|
].join("\n");
|
|
166
166
|
|
|
167
|
+
/** gjc-inherited verification directive (plan/gjc-inheritance.md, round 16): the
|
|
168
|
+
* done self-check PLUS gjc's `<verification>` test-quality contract — what makes a
|
|
169
|
+
* test worth writing. Single source consumed by both executorSystemPrompt's default
|
|
170
|
+
* and launch.ts's interactive prompt (was duplicated verbatim in both). */
|
|
171
|
+
export const VERIFICATION_DIRECTIVE =
|
|
172
|
+
"Before calling done, self-check: did I run the test or command that exercises this change, are directly-affected callsites/tests/docs updated, and does my claim match real output? If any answer is no, keep working — do not call done. " +
|
|
173
|
+
"When you write tests, exercise observable behavior, edge values, branch conditions, invariants, and error handling — never assert defaults or tautologies.";
|
|
174
|
+
|
|
167
175
|
export function executorSystemPrompt(
|
|
168
176
|
role = "Executor Agent, a senior software developer",
|
|
169
177
|
protocol: string = TOOL_PROTOCOL,
|
|
170
|
-
verificationDirective =
|
|
178
|
+
verificationDirective = VERIFICATION_DIRECTIVE,
|
|
171
179
|
): string {
|
|
172
180
|
return (
|
|
173
181
|
`You are the ${role}.\n` +
|
package/src/agent/session.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface SessionHeader {
|
|
|
10
10
|
timestamp: string;
|
|
11
11
|
cwd: string;
|
|
12
12
|
title?: string;
|
|
13
|
+
/** Model id pinned to this session; restored on resume so each session can carry
|
|
14
|
+
* its own model independent of the global default (per-session model selection). */
|
|
15
|
+
model?: string;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export interface SessionEntry {
|
|
@@ -35,6 +38,8 @@ export interface SessionSummary {
|
|
|
35
38
|
/** Session file size in bytes (for the resume picker's metadata line). */
|
|
36
39
|
sizeBytes?: number;
|
|
37
40
|
title?: string;
|
|
41
|
+
/** Model id pinned to this session (header `model`), if any. */
|
|
42
|
+
model?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export const SESSION_VERSION = 1;
|
|
@@ -53,7 +58,8 @@ export function sessionPath(id: string, cwd = process.cwd()): string {
|
|
|
53
58
|
|
|
54
59
|
export async function createSession(
|
|
55
60
|
cwd = process.cwd(),
|
|
56
|
-
id = newSessionId()
|
|
61
|
+
id = newSessionId(),
|
|
62
|
+
model?: string
|
|
57
63
|
): Promise<{ id: string; path: string }> {
|
|
58
64
|
const dir = sessionsDir(cwd);
|
|
59
65
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -64,6 +70,7 @@ export async function createSession(
|
|
|
64
70
|
id,
|
|
65
71
|
timestamp: new Date().toISOString(),
|
|
66
72
|
cwd,
|
|
73
|
+
...(model ? { model } : {}),
|
|
67
74
|
};
|
|
68
75
|
|
|
69
76
|
const file = sessionPath(id, cwd);
|
|
@@ -292,6 +299,7 @@ export async function listSessions(cwd = process.cwd()): Promise<SessionSummary[
|
|
|
292
299
|
mtimeMs: stat.mtimeMs,
|
|
293
300
|
sizeBytes: stat.size,
|
|
294
301
|
title: header.title,
|
|
302
|
+
model: header.model,
|
|
295
303
|
});
|
|
296
304
|
} catch {
|
|
297
305
|
// Tolerate malformed files (skip them)
|
|
@@ -309,10 +317,16 @@ export async function latestSessionId(cwd = process.cwd()): Promise<string | und
|
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
320
|
+
* Locate the session's JSONL header, apply `mutate`, and rewrite the file in place.
|
|
321
|
+
* `mutate` returns false to signal "no change needed" (skips the write). Shared by
|
|
322
|
+
* {@link renameSession} and {@link updateSessionModel}. Throws a clear Error when the
|
|
323
|
+
* session file or its header is missing.
|
|
314
324
|
*/
|
|
315
|
-
|
|
325
|
+
async function rewriteSessionHeader(
|
|
326
|
+
id: string,
|
|
327
|
+
mutate: (header: SessionHeader) => boolean,
|
|
328
|
+
cwd: string,
|
|
329
|
+
): Promise<void> {
|
|
316
330
|
const file = sessionPath(id, cwd);
|
|
317
331
|
let content: string;
|
|
318
332
|
try {
|
|
@@ -347,11 +361,35 @@ export async function renameSession(id: string, title: string, cwd = process.cwd
|
|
|
347
361
|
throw new Error(`Session header missing in session ${id}`);
|
|
348
362
|
}
|
|
349
363
|
|
|
350
|
-
header
|
|
364
|
+
if (!mutate(header)) return;
|
|
351
365
|
lines[headerIndex] = JSON.stringify(header);
|
|
352
366
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
353
367
|
}
|
|
354
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Rename a session by updating the title in its JSONL header.
|
|
371
|
+
* Throws a clear Error if the session file does not exist.
|
|
372
|
+
*/
|
|
373
|
+
export async function renameSession(id: string, title: string, cwd = process.cwd()): Promise<void> {
|
|
374
|
+
await rewriteSessionHeader(id, header => {
|
|
375
|
+
header.title = title;
|
|
376
|
+
return true;
|
|
377
|
+
}, cwd);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Pin a model to a session by updating the `model` field in its JSONL header so a
|
|
382
|
+
* later `/resume` restores it. No-op (no write) when the header already names that
|
|
383
|
+
* model. Throws a clear Error if the session file does not exist.
|
|
384
|
+
*/
|
|
385
|
+
export async function updateSessionModel(id: string, model: string, cwd = process.cwd()): Promise<void> {
|
|
386
|
+
await rewriteSessionHeader(id, header => {
|
|
387
|
+
if (header.model === model) return false;
|
|
388
|
+
header.model = model;
|
|
389
|
+
return true;
|
|
390
|
+
}, cwd);
|
|
391
|
+
}
|
|
392
|
+
|
|
355
393
|
/**
|
|
356
394
|
* Delete a session file.
|
|
357
395
|
* Returns false on ENOENT, true on success.
|
|
@@ -14,7 +14,11 @@ function hashString(input: string): string {
|
|
|
14
14
|
function tmuxSafeNamePart(input: string, max = 32): string {
|
|
15
15
|
const safe = input.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "value";
|
|
16
16
|
if (safe.length <= max) return safe;
|
|
17
|
-
|
|
17
|
+
// Trim a trailing dash from the truncated head so a boundary landing right
|
|
18
|
+
// after a `-` doesn't produce an ugly `name--<hash>` (double dash). The head
|
|
19
|
+
// is guaranteed non-empty and to start with an alnum (safe is trimmed).
|
|
20
|
+
const head = safe.slice(0, Math.max(1, max - 7)).replace(/-+$/, "") || safe.slice(0, 1);
|
|
21
|
+
return `${head}-${hashString(input)}`;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function tmuxRuntimeSuffix(flags: LaunchFlags): string {
|
package/src/commands/launch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
|
-
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, type AgentLoopEvents } from "../agent/engine";
|
|
4
|
+
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, VERIFICATION_DIRECTIVE, type AgentLoopEvents } from "../agent/engine";
|
|
5
5
|
import { createOpikTracer, wrapEvents } from "../agent/opik-tracer";
|
|
6
6
|
import { initialDynamicStepLimit } from "../agent/step-budget";
|
|
7
7
|
import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
|
|
@@ -15,7 +15,7 @@ import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
|
15
15
|
import { runTeamEngine, type TeamEngineOptions } from "./team";
|
|
16
16
|
import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
17
17
|
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
18
|
-
import { formatForgeBox } from "../tui/components/forge";
|
|
18
|
+
import { formatForgeBox, scaleForgeWidth } from "../tui/components/forge";
|
|
19
19
|
import { interactiveOAuthLogin } from "./auth";
|
|
20
20
|
import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
|
|
21
21
|
import type { AuthProvider } from "../auth";
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
latestSessionId,
|
|
92
92
|
exportSession,
|
|
93
93
|
renameSession,
|
|
94
|
+
updateSessionModel,
|
|
94
95
|
deleteSession,
|
|
95
96
|
sessionPath,
|
|
96
97
|
appendCompaction,
|
|
@@ -383,8 +384,27 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
383
384
|
stdout: "inherit",
|
|
384
385
|
stderr: "inherit",
|
|
385
386
|
});
|
|
386
|
-
|
|
387
|
+
// A nonzero attach exit (e.g. "open terminal failed: not a terminal" when
|
|
388
|
+
// stdout isn't a real TTY, a too-small client, or a transient server error)
|
|
389
|
+
// otherwise vanished: jeo returned 0 and the freshly created session was left
|
|
390
|
+
// orphaned with no hint. Surface it. Only advise reattach when the session is
|
|
391
|
+
// STILL live — if the inner jeo already exited (bad args, instant crash) the
|
|
392
|
+
// session is gone and "reattach" would be misleading.
|
|
393
|
+
const attachCode = await attach.exited;
|
|
394
|
+
if (attachCode !== 0) {
|
|
395
|
+
const alive = Bun.spawnSync([tmuxBin, "has-session", "-t", `=${sessionName}`], {
|
|
396
|
+
stdout: "ignore",
|
|
397
|
+
stderr: "ignore",
|
|
398
|
+
}).exitCode === 0;
|
|
399
|
+
console.error(
|
|
400
|
+
alive
|
|
401
|
+
? `Error: tmux attach failed (exit ${attachCode}). The session is still running; reattach with: tmux attach -t ${sessionName}`
|
|
402
|
+
: `Error: tmux session ${sessionName} ended before it could be attached (attach exit ${attachCode}).`,
|
|
403
|
+
);
|
|
404
|
+
process.exitCode = attachCode;
|
|
405
|
+
}
|
|
387
406
|
return;
|
|
407
|
+
|
|
388
408
|
} else {
|
|
389
409
|
console.warn("warning: tmux is not available on PATH. Launching directly...");
|
|
390
410
|
}
|
|
@@ -411,7 +431,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
411
431
|
}
|
|
412
432
|
console.log("Saved sessions (newest first):");
|
|
413
433
|
for (const s of sessions) {
|
|
414
|
-
console.log(` ${s.id} ${s.timestamp} (${s.messageCount} msgs) ${s.preview}`);
|
|
434
|
+
console.log(` ${s.id} ${s.timestamp} (${s.messageCount} msgs)${s.model ? ` [${s.model}]` : ""} ${s.preview}`);
|
|
415
435
|
}
|
|
416
436
|
console.log("\nResume with: jeo launch --resume <id>");
|
|
417
437
|
return;
|
|
@@ -470,7 +490,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
470
490
|
preamble + "\n\n" + protocol + "\n\n" +
|
|
471
491
|
WORKING_DISCIPLINE + "\n\n" +
|
|
472
492
|
OUTPUT_DISCIPLINE + "\n\n" +
|
|
473
|
-
|
|
493
|
+
VERIFICATION_DIRECTIVE +
|
|
474
494
|
"\nWhen you have finished the user's request, or need to reply to or ask the user something, call done with {\"reason\": <your natural-language reply to the user>}. The reason text is shown to the user as your message." +
|
|
475
495
|
(allowedTools.has("task") ? "\n\nDelegation: " + taskToolProtocolLine(cfg) +
|
|
476
496
|
" Call task with {\"role\": <one of the advertised roles>, \"task\": <assignment>, \"context\": <optional>} to hand a focused slice to a subagent." : "") +
|
|
@@ -588,23 +608,37 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
588
608
|
const id = flags.resumeId ?? (await latestSessionId(cwd));
|
|
589
609
|
if (!id) {
|
|
590
610
|
console.log("No session to resume. Starting a new one.");
|
|
591
|
-
sessionId = (await createSession(cwd)).id;
|
|
611
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
592
612
|
} else {
|
|
593
613
|
try {
|
|
594
|
-
const { messages } = await loadSession(id, cwd);
|
|
614
|
+
const { header, messages } = await loadSession(id, cwd);
|
|
595
615
|
for (const m of messages) history.push(m);
|
|
596
616
|
sessionId = id;
|
|
597
|
-
|
|
617
|
+
// Restore the model this session was last using unless the CLI explicitly
|
|
618
|
+
// pinned one (flags.model/role/provider → initialSessionModel wins).
|
|
619
|
+
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
620
|
+
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
621
|
+
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
598
622
|
} catch (err) {
|
|
599
623
|
console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
|
|
600
|
-
sessionId = (await createSession(cwd)).id;
|
|
624
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
601
625
|
}
|
|
602
626
|
}
|
|
603
627
|
} else {
|
|
604
|
-
sessionId = (await createSession(cwd)).id;
|
|
628
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
605
629
|
}
|
|
606
630
|
}
|
|
607
631
|
|
|
632
|
+
// Persist the active per-session model into the session header so `/resume` restores
|
|
633
|
+
// it (each session can carry its own model independent of the global default).
|
|
634
|
+
// Best-effort: a header-rewrite failure must never abort the turn.
|
|
635
|
+
const persistSessionModel = async (): Promise<void> => {
|
|
636
|
+
if (flags.noSession || !sessionId || !sessionModel) return;
|
|
637
|
+
try {
|
|
638
|
+
await updateSessionModel(sessionId, sessionModel, cwd);
|
|
639
|
+
} catch { /* best-effort */ }
|
|
640
|
+
};
|
|
641
|
+
|
|
608
642
|
// `step N/M` display seed: the explicit --max-steps cap, else the dynamic budget's
|
|
609
643
|
// rolling base — the engine's onBudget event keeps the denominator honest as it grows.
|
|
610
644
|
const initialStepLimit = flags.maxSteps > 0 ? flags.maxSteps : initialDynamicStepLimit();
|
|
@@ -1007,7 +1041,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1007
1041
|
history.length = 1;
|
|
1008
1042
|
if (sessionId && !flags.noSession) {
|
|
1009
1043
|
try {
|
|
1010
|
-
sessionId = (await createSession(cwd)).id;
|
|
1044
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
1011
1045
|
} catch { /* best-effort: in-memory clear already done */ }
|
|
1012
1046
|
}
|
|
1013
1047
|
console.log("(history cleared)");
|
|
@@ -1194,7 +1228,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1194
1228
|
{
|
|
1195
1229
|
const card = formatForgeBox(
|
|
1196
1230
|
{ title: "[skill]", lines: skillInvocationCard(skill, intent) },
|
|
1197
|
-
{ width: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
1231
|
+
{ width: scaleForgeWidth(Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2))), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
1198
1232
|
);
|
|
1199
1233
|
logLines(card);
|
|
1200
1234
|
}
|
|
@@ -2317,6 +2351,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2317
2351
|
const { resolved, provider } = await describeModel(target);
|
|
2318
2352
|
const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
|
|
2319
2353
|
sessionModel = target;
|
|
2354
|
+
await persistSessionModel();
|
|
2320
2355
|
const defaultThinking = isThinkingLevel(action) ? action : undefined;
|
|
2321
2356
|
if (defaultThinking) {
|
|
2322
2357
|
sessionThinking = defaultThinking;
|
|
@@ -2873,7 +2908,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2873
2908
|
const startFreshSession = async (verb: string): Promise<void> => {
|
|
2874
2909
|
history.length = 1;
|
|
2875
2910
|
if (!flags.noSession) {
|
|
2876
|
-
sessionId = (await createSession(cwd)).id;
|
|
2911
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
2877
2912
|
advanceSessionBoxColor(); // distinct input-box hue per newly opened session
|
|
2878
2913
|
console.log(`(${verb} — new session ${sessionId})`);
|
|
2879
2914
|
} else {
|
|
@@ -2916,10 +2951,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2916
2951
|
const arg = tokens.slice(1).join(" ").trim();
|
|
2917
2952
|
const applyResume = async (rid: string): Promise<void> => {
|
|
2918
2953
|
try {
|
|
2919
|
-
const { messages } = await loadSession(rid, cwd);
|
|
2954
|
+
const { header, messages } = await loadSession(rid, cwd);
|
|
2920
2955
|
history.length = 1;
|
|
2921
2956
|
for (const m of messages) history.push(m);
|
|
2922
2957
|
sessionId = rid;
|
|
2958
|
+
// Restore the model this session was last using (per-session model).
|
|
2959
|
+
if (header.model) sessionModel = header.model;
|
|
2923
2960
|
// Seed /retry + reply marker from the last user/assistant turn.
|
|
2924
2961
|
lastUserInput = ""; lastReply = "";
|
|
2925
2962
|
for (let k = history.length - 1; k >= 1; k--) {
|
|
@@ -3497,6 +3534,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3497
3534
|
const qualified = qualifyModelId(modelArg, "openai");
|
|
3498
3535
|
sessionModel = qualified;
|
|
3499
3536
|
await saveConfigPatch(raw => rememberModelPatch(raw, qualified));
|
|
3537
|
+
await persistSessionModel();
|
|
3500
3538
|
console.log(`OpenAI-compatible endpoint set: ${url} · default model ${qualified} — saved to ~/.jeo/config.json.`);
|
|
3501
3539
|
} else {
|
|
3502
3540
|
console.log(`OpenAI-compatible endpoint set: ${url} — saved to ~/.jeo/config.json.`);
|
|
@@ -4012,6 +4050,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4012
4050
|
// MRU persistence: picking a model IS saving it — the newest pick wins
|
|
4013
4051
|
// as the global default; recents keep the rotation for every session.
|
|
4014
4052
|
await saveConfigPatch(raw => rememberModelPatch(raw, arg));
|
|
4053
|
+
await persistSessionModel();
|
|
4015
4054
|
}
|
|
4016
4055
|
const { resolved, provider } = await describeModel(label);
|
|
4017
4056
|
const st = statuses.find(s => s.name === provider);
|
|
@@ -10,6 +10,7 @@ Turn a bounded task into a working, verified outcome with the smallest correct c
|
|
|
10
10
|
- Keep diffs small and aligned to existing patterns.
|
|
11
11
|
- Do not broaden scope or invent abstractions unless the task requires them.
|
|
12
12
|
- Verify the task before calling done.
|
|
13
|
+
- When you add tests, exercise observable behavior, edge values, branch conditions, invariants, and error handling — never assert defaults or tautologies.
|
|
13
14
|
- Communicate the result through `done.reason` using the required output contract.
|
|
14
15
|
</constraints>
|
|
15
16
|
|
package/src/tui/app.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./comp
|
|
|
24
24
|
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
25
25
|
import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint } from "./components/themes";
|
|
26
26
|
import { detectColorLevel, animatedGradientText, ColorLevel } from "./components/color";
|
|
27
|
-
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, type ForgeSummary } from "./components/forge";
|
|
27
|
+
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, scaleForgeWidth, type ForgeSummary } from "./components/forge";
|
|
28
28
|
import { renderStatusBar, renderStatusBox, type StatusBoxData } from "./components/status";
|
|
29
29
|
import { costForUsage } from "../ai/pricing";
|
|
30
30
|
import { renderMarkdownTables } from "./components/markdown-table";
|
|
@@ -1206,7 +1206,7 @@ export class LaunchTui {
|
|
|
1206
1206
|
* Non-inline modes keep the card in `forgeSummaries` for the final static summary. */
|
|
1207
1207
|
private flushForgeCard(summary: ForgeSummary, success?: boolean): void {
|
|
1208
1208
|
if (!this.inline || this.finished) return;
|
|
1209
|
-
const width =
|
|
1209
|
+
const width = scaleForgeWidth(size().cols - 1);
|
|
1210
1210
|
// gjc D2 (state-encoded border): a FAILED card gets a red border so it pops
|
|
1211
1211
|
// out of scrollback at a glance; OK/neutral cards keep the theme accent
|
|
1212
1212
|
// identity. The ✓/✗ title mark already encodes state, but the border tone
|
|
@@ -1235,10 +1235,9 @@ export class LaunchTui {
|
|
|
1235
1235
|
anim?: { phase: number; colorLevel: ColorLevel; beat: string },
|
|
1236
1236
|
dim = false,
|
|
1237
1237
|
): string[] {
|
|
1238
|
-
|
|
1239
|
-
//
|
|
1240
|
-
|
|
1241
|
-
const boxWidth = Math.max(floor, width);
|
|
1238
|
+
// Forge cards render at a reduced (÷FORGE_SCALE) compact width rather than
|
|
1239
|
+
// spanning the full available column run.
|
|
1240
|
+
const boxWidth = scaleForgeWidth(width);
|
|
1242
1241
|
const paint = this.theme.color ? accentPaint(this.theme) : (s: string) => s;
|
|
1243
1242
|
const lines: string[] = [];
|
|
1244
1243
|
for (const [i, summary] of this.forgeSummaries.slice(-maxEntries).entries()) {
|
|
@@ -412,46 +412,61 @@ export async function animateFrames(stage: AsciiStage, opts: AnimateFramesOption
|
|
|
412
412
|
}
|
|
413
413
|
return total;
|
|
414
414
|
}
|
|
415
|
-
/** The compact jeo forge mark: a symmetrical crayfish (가재) brand emblem
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
415
|
+
/** The compact jeo forge mark: a symmetrical crayfish (가재) brand emblem drawn as a
|
|
416
|
+
* 2.5×-thick LINE-BOARD (hollow outline, no fill) — the `>-<` silhouette of two pincer
|
|
417
|
+
* CLAWS (집게) whose top arms bend (꺾임) inward toward a narrow central BRIDGE (the
|
|
418
|
+
* eyeglass-frame / 안경태 bar). Every stroke is rendered as a thick rounded-corner tube
|
|
419
|
+
* (╭╮╰╯ + ─│) so the shape reads as a heavy neon outline, not a filled block. No letters.
|
|
420
|
+
* Width-1 glyphs only (box drawing) so padding/centering math stays exact, and the
|
|
421
|
+
* blue→violet→pink flow gradient from renderForgeMark supplies the neon glow. Frame 0 is
|
|
422
|
+
* the static symbol. */
|
|
423
423
|
export const FORGE_MARK_ART: string[] = [
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
424
|
+
"╭─────╮ ╭─────╮",
|
|
425
|
+
"│ │ │ │",
|
|
426
|
+
"╰─╮ │ ╭─────────╮ │ ╭─╯",
|
|
427
|
+
" │ │ │ │ │ │ ",
|
|
428
|
+
"╭─╯ ╭─╯ ╰─────────╯ ╰─╮ ╰─╮",
|
|
429
|
+
"│ │ │ │",
|
|
430
|
+
"╰───╯ ╰───╯"
|
|
427
431
|
];
|
|
428
432
|
|
|
429
433
|
export const FORGE_MARK_ART_ASCII: string[] = [
|
|
430
|
-
"
|
|
431
|
-
"
|
|
432
|
-
"
|
|
434
|
+
".-----. .-----.",
|
|
435
|
+
"| | | |",
|
|
436
|
+
"'-. | .---------. | .-'",
|
|
437
|
+
" | | | | | | ",
|
|
438
|
+
".-' .-' '---------' '-. '-.",
|
|
439
|
+
"| | | |",
|
|
440
|
+
"'---' '---'"
|
|
433
441
|
];
|
|
434
442
|
|
|
435
|
-
/** Claw-snap blink frames for the compact lobster forge mark: the
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the static
|
|
443
|
+
/** Claw-snap blink frames for the compact lobster forge mark: the central bridge stays
|
|
444
|
+
* fixed while the four pincer arms snap (open → clenched), so the lobster "clicks" its
|
|
445
|
+
* claws. Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the static
|
|
439
446
|
* symbol. All lines share the same width and width-1 glyphs. */
|
|
440
447
|
export const FORGE_MARK_FRAMES: string[][] = [
|
|
441
448
|
FORGE_MARK_ART,
|
|
442
449
|
[
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
450
|
+
"╭───╮ ╭───╮",
|
|
451
|
+
"│ │ │ │",
|
|
452
|
+
"╰─╮ ╰─╮ ╭─────────╮ ╭─╯ ╭─╯",
|
|
453
|
+
" │ │ │ │ │ │ ",
|
|
454
|
+
"╭─╯ │ ╰─────────╯ │ ╰─╮",
|
|
455
|
+
"│ │ │ │",
|
|
456
|
+
"╰─────╯ ╰─────╯"
|
|
446
457
|
]
|
|
447
458
|
];
|
|
448
459
|
|
|
449
460
|
export const FORGE_MARK_FRAMES_ASCII: string[][] = [
|
|
450
461
|
FORGE_MARK_ART_ASCII,
|
|
451
462
|
[
|
|
452
|
-
"
|
|
453
|
-
"
|
|
454
|
-
"
|
|
463
|
+
".---. .---.",
|
|
464
|
+
"| | | |",
|
|
465
|
+
"'-. '-. .---------. .-' .-'",
|
|
466
|
+
" | | | | | | ",
|
|
467
|
+
".-' | '---------' | '-.",
|
|
468
|
+
"| | | |",
|
|
469
|
+
"'-----' '-----'"
|
|
455
470
|
]
|
|
456
471
|
];
|
|
457
472
|
|
|
@@ -461,23 +476,34 @@ export function forgeMarkFrameCount(): number {
|
|
|
461
476
|
}
|
|
462
477
|
|
|
463
478
|
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the same
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* symmetry, with renderForgeMark's blue→violet→pink flow gradient supplying the neon glow.
|
|
469
|
-
* Width 29 (matches the welcome compact↔grand threshold) and width-1 glyphs only so
|
|
470
|
-
* padding/centering math stays exact. */
|
|
479
|
+
* crayfish line-board emblem rendered large — the pincer claws and central eyeglass
|
|
480
|
+
* bridge drawn as wider 3×-thick rounded outline tubes (╭╮╰╯ + ─│), no fill, no letters.
|
|
481
|
+
* Width-1 glyphs only so padding/centering math stays exact, with renderForgeMark's
|
|
482
|
+
* blue→violet→pink flow gradient supplying the neon glow. */
|
|
471
483
|
export const FORGE_MARK_ART_GRAND: string[] = [
|
|
472
|
-
"
|
|
473
|
-
"
|
|
474
|
-
"
|
|
484
|
+
"╭────────╮ ╭────────╮",
|
|
485
|
+
"│ │ │ │",
|
|
486
|
+
"│ │ │ │",
|
|
487
|
+
"╰──╮ │ ╭──────────────╮ │ ╭──╯",
|
|
488
|
+
" │ │ │ │ │ │ ",
|
|
489
|
+
" │ │ │ │ │ │ ",
|
|
490
|
+
"╭──╯ ╭──╯ ╰──────────────╯ ╰──╮ ╰──╮",
|
|
491
|
+
"│ │ │ │",
|
|
492
|
+
"│ │ │ │",
|
|
493
|
+
"╰─────╯ ╰─────╯"
|
|
475
494
|
];
|
|
476
495
|
|
|
477
496
|
export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
"
|
|
497
|
+
".--------. .--------.",
|
|
498
|
+
"| | | |",
|
|
499
|
+
"| | | |",
|
|
500
|
+
"'--. | .--------------. | .--'",
|
|
501
|
+
" | | | | | | ",
|
|
502
|
+
" | | | | | | ",
|
|
503
|
+
".--' .--' '--------------' '--. '--.",
|
|
504
|
+
"| | | |",
|
|
505
|
+
"| | | |",
|
|
506
|
+
"'-----' '-----'"
|
|
481
507
|
];
|
|
482
508
|
|
|
483
509
|
// Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
|
|
@@ -479,6 +479,15 @@ export function fitForgeBoxes(lines: string[], budget: number): string[] {
|
|
|
479
479
|
return out;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/** Forge cards render at a reduced scale: the available width is divided by this
|
|
483
|
+
* factor so a box reads as a compact panel instead of spanning the full terminal. */
|
|
484
|
+
export const FORGE_SCALE = 1.2;
|
|
485
|
+
|
|
486
|
+
/** Scale a caller's available width down to the forge card's compact render width. */
|
|
487
|
+
export function scaleForgeWidth(available: number): number {
|
|
488
|
+
return Math.max(24, Math.trunc(available / FORGE_SCALE));
|
|
489
|
+
}
|
|
490
|
+
|
|
482
491
|
export function formatForgeBox(summary: ForgeSummary, opts: ForgeBoxOptions = {}): string[] {
|
|
483
492
|
const innerWidth = opts.width ?? 80;
|
|
484
493
|
const floor = Math.min(24, innerWidth);
|