jeo-code 0.6.35 → 0.6.37
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 +27 -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/commands/deep-interview.ts +16 -7
- package/src/commands/launch/input.ts +27 -0
- package/src/commands/launch/slash-views.ts +2 -0
- package/src/commands/launch/tmux.ts +38 -4
- package/src/commands/launch.ts +52 -6
- package/src/commands/ralplan.ts +6 -0
- package/src/commands/team.ts +20 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ 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.37] - 2026-06-20
|
|
10
|
+
_Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The boxed prompt's ↑/↓ recalls input history again on a soft-wrapped line.** A long single line that the box wraps to several visual rows is no longer treated as multi-line: ↑/↓ fall through to readline's history recall (the dominant REPL expectation) instead of dragging the wrapped tail up a visual row. In-box vertical caret nav (textarea feel) is now gated behind a GENUINE multi-line draft — one carrying an explicit Shift+Enter / pasted break, stored as the private-use `MULTILINE_SENTINEL` — and still yields the arrows to an open slash list or the Ctrl+O history panel. New `isGenuineMultilineDraft` / `shouldBoxVerticalNav` helpers make the gate unit-testable independent of the live readline/PTY wiring.
|
|
14
|
+
- **Every terminating Spec-first workflow stage now surfaces a user-visible answer.** Three stages could previously reach a terminal state with no message explaining the outcome:
|
|
15
|
+
- **`team`** routes all subagent output through the engine `log()`/`io.output` sink (zero raw `console.log` in `executeTaskWithAgent`) and prints a `<role> report:` header followed by every line of the subagent's reason on success.
|
|
16
|
+
- **`deep-interview`** gates its `[Handoff Ready]` / `onProgress(complete)` signal on a real frozen seed: `freezeSeed` now returns `Promise<boolean>`, and a freeze failure emits `[HOLD]` and keeps the interview open instead of falsely claiming the requirement is crystallized.
|
|
17
|
+
- **`ralplan`** reports a discarded revision: an invalid `[ITERATE]` revision now logs `discarding the revision; the [ITERATE] verdict stands` instead of silently surfacing the stale verdict.
|
|
18
|
+
|
|
19
|
+
### Verified
|
|
20
|
+
- `scripts/mem-probe.ts` (2000 LaunchTui turns) shows a flat post-GC heap — per-turn slope **−556 bytes/turn**, returning to its settled floor — with a single `exit` listener and zero `process` SIGINT/listener accumulation; `scripts/tmux-verify.sh smoke` boots `jeo --tmux` to a clean input box + model bar (EXIT 0). `bun run typecheck` is clean and `bun test` is **1751 pass / 0 fail** across 216 files, including the new `test/box-vertical-nav.test.ts` and the no-answer-deadend targeted suite (`team-run`/`team-schema`/`team-subagent`/`deep-interview`/`deep-interview-noninteractive`/`workflow-integrity`/`approve`/`parse-role-gate-verdict` → 71 pass / 0 fail).
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [0.6.36] - 2026-06-20
|
|
24
|
+
_When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot._
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **A `--tmux` mouse drag-select now reaches the system clipboard instead of vanishing.** Turning the mouse on (so on-screen text can be selected) re-routes a drag into tmux copy-mode, where the selection used to die inside tmux's own buffer — `cmd/ctrl+v` got nothing. The launch path now applies the same clipboard repair to the CURRENT session that `tmuxProfileCommands` applies to jeo-owned sessions: new `currentTmuxClipboardCommands(env, deps)` emits `set-option set-clipboard on` (lets the copy-mode selection escape via OSC 52) and, when a local clipboard tool is on PATH, `set-option copy-command <tool>` (pipes the drag-select straight to `pbcopy` / `wl-copy` / `xclip` / `xsel` / `clip` for terminals that don't honor OSC 52). Both are written **session-locally — never `-t` or `-g`** — so the user's other tmux sessions are untouched; `JEO_TMUX_PROFILE=0` opts out, and `copy-command` is skipped when no tool is found.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **`/help` documents the drag-to-copy behavior and its escape hatch.** Two hotkey rows now explain that a drag selects on-screen text (copies on `cmd/ctrl+c`, and auto-copies to the system clipboard under `--tmux`) and that **Shift-drag** (iTerm/macOS: **Option-drag**) forces the terminal's own selection when tmux owns the mouse.
|
|
31
|
+
|
|
32
|
+
### Verified
|
|
33
|
+
- `scripts/mem-probe.ts` (2000 LaunchTui turns) shows a flat post-GC heap — per-turn slope **−541 bytes/turn**, returning to its settled floor — with zero `process` SIGINT/listener accumulation; `scripts/tmux-verify.sh smoke` boots `jeo --tmux` to a clean input box + model bar (EXIT 0). `bun run typecheck` clean and `bun test` **1748 pass / 0 fail** across 215 files, including the new `currentTmuxClipboardCommands` session-local / no-tool / opt-out cases in `test/tmux.test.ts`.
|
|
34
|
+
|
|
35
|
+
|
|
9
36
|
## [0.6.35] - 2026-06-20
|
|
10
37
|
_The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check._
|
|
11
38
|
|
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.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
204
|
+
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
203
205
|
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
204
206
|
- **[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.
|
|
205
207
|
- **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
206
|
-
- **[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.
|
|
207
|
-
- **[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.
|
|
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.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
204
|
+
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
203
205
|
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
204
206
|
- **[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.
|
|
205
207
|
- **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
206
|
-
- **[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.
|
|
207
|
-
- **[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.
|
|
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.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
204
|
+
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
203
205
|
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
204
206
|
- **[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.
|
|
205
207
|
- **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
206
|
-
- **[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.
|
|
207
|
-
- **[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.
|
|
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.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
204
|
+
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
203
205
|
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
204
206
|
- **[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.
|
|
205
207
|
- **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
|
|
206
|
-
- **[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.
|
|
207
|
-
- **[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.
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -567,7 +567,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
567
567
|
let ambiguity = state.current_ambiguity ?? 1.0;
|
|
568
568
|
let lastParsed: SocraticResponse | undefined;
|
|
569
569
|
|
|
570
|
-
const freezeSeed = async (parsed: SocraticResponse): Promise<
|
|
570
|
+
const freezeSeed = async (parsed: SocraticResponse): Promise<boolean> => {
|
|
571
571
|
const readiness = freezeReadiness(parsed);
|
|
572
572
|
if (!readiness.ok) throw new Error(`Refusing to freeze seed: ${readiness.reason}.`);
|
|
573
573
|
|
|
@@ -594,7 +594,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
594
594
|
`[ERROR] Seed round-trip self-check FAILED — the acceptance criteria would not survive ultragoal's parser ` +
|
|
595
595
|
`(writer/parser drift). NOT freezing the seed. Got back: ${JSON.stringify(parsedBack)}`,
|
|
596
596
|
);
|
|
597
|
-
return;
|
|
597
|
+
return false;
|
|
598
598
|
}
|
|
599
599
|
await fs.writeFile(seedPath, seedContent, "utf-8");
|
|
600
600
|
state!.current_phase = "complete";
|
|
@@ -603,6 +603,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
603
603
|
state!.current_ambiguity = Math.min(state!.current_ambiguity ?? threshold, threshold);
|
|
604
604
|
await writeWorkflowState("deep-interview", state!, cwd);
|
|
605
605
|
log(`Saved frozen requirements spec seed to: ${seedPath}`);
|
|
606
|
+
return true;
|
|
606
607
|
};
|
|
607
608
|
|
|
608
609
|
while (round <= 10) {
|
|
@@ -629,12 +630,20 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
629
630
|
const readiness = freezeReadiness(parsed);
|
|
630
631
|
if (ambiguity <= threshold && readiness.ok) {
|
|
631
632
|
log(`\n[SUCCESS] Ambiguity is <= ${(threshold * 100).toFixed(0)}%! Concluding requirements gather.`);
|
|
632
|
-
await freezeSeed(parsed);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
opts.onProgress
|
|
633
|
+
const frozen = await freezeSeed(parsed);
|
|
634
|
+
if (frozen) {
|
|
635
|
+
log("\n[Handoff Ready] Requirement is crystallized. Next, run 'jeo ralplan' to build a plan.");
|
|
636
|
+
if (opts.onProgress) {
|
|
637
|
+
opts.onProgress({ skill: "deep-interview", phase: "complete" });
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
636
640
|
}
|
|
637
|
-
|
|
641
|
+
// Freeze failed (e.g. round-trip self-check): do NOT emit a false
|
|
642
|
+
// "crystallized" handoff. Fall through to the normal question loop
|
|
643
|
+
// below — it already asks the follow-up, pushes history, and bumps
|
|
644
|
+
// the round — so the interview simply stays open.
|
|
645
|
+
log("\n[HOLD] Could not freeze the requirements seed — see the error above. Keeping the interview open.");
|
|
646
|
+
|
|
638
647
|
}
|
|
639
648
|
|
|
640
649
|
let nextQuestion = parsed.nextQuestion?.trim() || interviewLanguage.acceptanceFollowup;
|
|
@@ -51,6 +51,33 @@ export function isStandaloneBackspace(chunk: string): boolean {
|
|
|
51
51
|
return chunk.length > 0 && /^[\x7f\b]+$/.test(chunk);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Private-use sentinel the input filter substitutes for an EXPLICIT line break
|
|
55
|
+
* (Shift+Enter / a pasted newline) before the bytes reach readline, so the draft can
|
|
56
|
+
* carry hard newlines through readline's single-line buffer. A line that merely
|
|
57
|
+
* SOFT-WRAPS at the box width contains NO sentinel — that distinction is the whole
|
|
58
|
+
* point of `isGenuineMultilineDraft`. */
|
|
59
|
+
export const MULTILINE_SENTINEL = "\uE000";
|
|
60
|
+
|
|
61
|
+
/** True when the draft has at least one EXPLICIT line break (a sentinel) — i.e. the
|
|
62
|
+
* user deliberately made it multi-line. A long single line that the box soft-wraps to
|
|
63
|
+
* several visual rows is NOT multi-line and returns false. */
|
|
64
|
+
export function isGenuineMultilineDraft(line: string): boolean {
|
|
65
|
+
return line.includes(MULTILINE_SENTINEL);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether an Up/Down keystroke should move the caret BETWEEN the box's visual rows
|
|
69
|
+
* (textarea feel) instead of falling through to readline's input-history recall. True
|
|
70
|
+
* only for a genuinely multi-line draft with no slash dropdown or history panel owning
|
|
71
|
+
* the arrows. A soft-wrapped one-liner returns false, so ↑ recalls the previous prompt
|
|
72
|
+
* rather than dragging the wrapped tail up a visual row. */
|
|
73
|
+
export function shouldBoxVerticalNav(
|
|
74
|
+
line: string,
|
|
75
|
+
opts: { slashMatchCount: number; historyPanelOpen: boolean },
|
|
76
|
+
): boolean {
|
|
77
|
+
return isGenuineMultilineDraft(line) && opts.slashMatchCount === 0 && !opts.historyPanelOpen;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
54
81
|
/**
|
|
55
82
|
* macOS / fixterms combo-key normalization for the boxed prompt's line editor.
|
|
56
83
|
*
|
|
@@ -23,6 +23,8 @@ export function hotkeysLines(): string[] {
|
|
|
23
23
|
" @path mention a file (Tab completes relative paths)",
|
|
24
24
|
" Ctrl-V paste a copied image from the clipboard into the next message",
|
|
25
25
|
" drag-drop drop an image file onto the box to attach it (its path becomes [image #N])",
|
|
26
|
+
" drag select on-screen text to copy — copies on cmd/ctrl+c; under --tmux a drag auto-copies to the system clipboard",
|
|
27
|
+
" Shift-drag force the terminal's own selection when tmux owns the mouse (iTerm/macOS: Option-drag)",
|
|
26
28
|
];
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -106,8 +106,8 @@ export function shellQuote(arg: string): string {
|
|
|
106
106
|
*/
|
|
107
107
|
export function shouldEnableCurrentTmuxMouse(env: Record<string, string | undefined>): boolean {
|
|
108
108
|
return !!env.TMUX
|
|
109
|
-
&&
|
|
110
|
-
&&
|
|
109
|
+
&& env.JEO_TMUX_LAUNCHED !== "1"
|
|
110
|
+
&& env.JEO_TMUX_MOUSE !== "0";
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
@@ -160,7 +160,7 @@ export function tmuxProfileCommands(
|
|
|
160
160
|
): TmuxProfileCommand[] {
|
|
161
161
|
const t = `=${target}:`;
|
|
162
162
|
const commands: TmuxProfileCommand[] = [];
|
|
163
|
-
if (
|
|
163
|
+
if (env.JEO_TMUX_MOUSE !== "0") {
|
|
164
164
|
commands.push({
|
|
165
165
|
description: "enable tmux mouse scrolling (wheel-up → copy-mode over real history)",
|
|
166
166
|
args: ["set-option", "-t", t, "mouse", "on"],
|
|
@@ -182,7 +182,7 @@ export function tmuxProfileCommands(
|
|
|
182
182
|
args: ["set-option", "-t", t, "@jeo-project", meta.project],
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
|
-
if (
|
|
185
|
+
if (env.JEO_TMUX_PROFILE !== "0") {
|
|
186
186
|
commands.push(
|
|
187
187
|
{
|
|
188
188
|
description: "enable tmux clipboard integration",
|
|
@@ -210,6 +210,40 @@ export function tmuxProfileCommands(
|
|
|
210
210
|
return commands;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Clipboard set-options for the CURRENT (foreign) tmux session that the in-session
|
|
215
|
+
* `jeo --tmux` path turns `mouse on` for. Enabling the mouse re-routes a plain
|
|
216
|
+
* drag into copy-mode, so without these a drag-select no longer lands anywhere:
|
|
217
|
+
* - `set-clipboard on` lets the copy-mode selection reach the outer terminal via OSC52;
|
|
218
|
+
* - `copy-command` pipes that selection straight to the local clipboard tool
|
|
219
|
+
* (pbcopy / wl-copy / xclip / xsel / clip), so a drag-select copies for `cmd+v`
|
|
220
|
+
* even where OSC52 is not honored.
|
|
221
|
+
* Applied WITHOUT `-t` (the current session only — never -g, so the user's other
|
|
222
|
+
* sessions are untouched). `JEO_TMUX_PROFILE=0` opts out; `copy-command` is skipped
|
|
223
|
+
* when no clipboard tool is on PATH. This is the in-session analogue of the
|
|
224
|
+
* clipboard block in {@link tmuxProfileCommands} for jeo-owned sessions.
|
|
225
|
+
*/
|
|
226
|
+
export function currentTmuxClipboardCommands(
|
|
227
|
+
env: Record<string, string | undefined>,
|
|
228
|
+
deps: { platform?: NodeJS.Platform; which?: (bin: string) => string | null } = {},
|
|
229
|
+
): TmuxProfileCommand[] {
|
|
230
|
+
if (env.JEO_TMUX_PROFILE === "0") return [];
|
|
231
|
+
const commands: TmuxProfileCommand[] = [
|
|
232
|
+
{
|
|
233
|
+
description: "enable tmux clipboard integration",
|
|
234
|
+
args: ["set-option", "set-clipboard", "on"],
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
const copyCmd = tmuxCopyCommand(deps.platform ?? process.platform, deps.which ?? ((bin: string) => Bun.which(bin)));
|
|
238
|
+
if (copyCmd) {
|
|
239
|
+
commands.push({
|
|
240
|
+
description: "pipe copy-mode selection to the system clipboard",
|
|
241
|
+
args: ["set-option", "copy-command", copyCmd],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return commands;
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
/**
|
|
214
248
|
* Resolve a git worktree path (gjc `--worktree <path>` parity). If the path
|
|
215
249
|
* already exists it is reused as-is; otherwise a new worktree is created on a
|
package/src/commands/launch.ts
CHANGED
|
@@ -118,6 +118,7 @@ import {
|
|
|
118
118
|
shouldEnableCurrentTmuxMouse,
|
|
119
119
|
tmuxLaunchCommand,
|
|
120
120
|
tmuxProfileCommands,
|
|
121
|
+
currentTmuxClipboardCommands,
|
|
121
122
|
resolveWorktree,
|
|
122
123
|
shellQuote,
|
|
123
124
|
type TmuxCreateResult,
|
|
@@ -135,6 +136,9 @@ import {
|
|
|
135
136
|
matchTerminalReport,
|
|
136
137
|
stripMouseReports,
|
|
137
138
|
rewriteCursorCombos,
|
|
139
|
+
MULTILINE_SENTINEL,
|
|
140
|
+
isGenuineMultilineDraft,
|
|
141
|
+
shouldBoxVerticalNav,
|
|
138
142
|
queuePromptInputChunk,
|
|
139
143
|
captureLivePromptInputChunk,
|
|
140
144
|
restoreQueuedLinesToPrefill,
|
|
@@ -186,6 +190,7 @@ export {
|
|
|
186
190
|
shouldEnableCurrentTmuxMouse,
|
|
187
191
|
tmuxLaunchCommand,
|
|
188
192
|
tmuxProfileCommands,
|
|
193
|
+
currentTmuxClipboardCommands,
|
|
189
194
|
resolveWorktree,
|
|
190
195
|
shellQuote,
|
|
191
196
|
type TmuxCreateResult,
|
|
@@ -201,6 +206,9 @@ export {
|
|
|
201
206
|
matchTerminalReport,
|
|
202
207
|
stripMouseReports,
|
|
203
208
|
rewriteCursorCombos,
|
|
209
|
+
MULTILINE_SENTINEL,
|
|
210
|
+
isGenuineMultilineDraft,
|
|
211
|
+
shouldBoxVerticalNav,
|
|
204
212
|
queuePromptInputChunk,
|
|
205
213
|
captureLivePromptInputChunk,
|
|
206
214
|
restoreQueuedLinesToPrefill,
|
|
@@ -420,6 +428,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
420
428
|
const tmuxBin = Bun.which("tmux");
|
|
421
429
|
if (tmuxBin) {
|
|
422
430
|
try { Bun.spawnSync([tmuxBin, "set-option", "mouse", "on"]); } catch { /* best-effort */ }
|
|
431
|
+
// Enabling the mouse re-routes a drag into copy-mode; set-clipboard +
|
|
432
|
+
// copy-command make that drag-select actually land on the system clipboard.
|
|
433
|
+
for (const c of currentTmuxClipboardCommands(process.env)) {
|
|
434
|
+
try { Bun.spawnSync([tmuxBin, ...c.args]); } catch { /* best-effort */ }
|
|
435
|
+
}
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
438
|
}
|
|
@@ -1353,7 +1366,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1353
1366
|
// readline sees them — readline inserts it as an ordinary character (no per-line
|
|
1354
1367
|
// submit/race), the box renders it as a real line break, and it is expanded back to
|
|
1355
1368
|
// "\n" on submit. On for any interactive TTY; JEO_NO_MULTILINE=1 reads stdin directly.
|
|
1356
|
-
const SENTINEL =
|
|
1369
|
+
const SENTINEL = MULTILINE_SENTINEL;
|
|
1357
1370
|
const SHIFT_ENTER_SEQS = ["\u001b[27;2;13~", "\u001b[13;2u"];
|
|
1358
1371
|
// Multi-line input filter is ON for any interactive TTY: reliable multi-line paste
|
|
1359
1372
|
// (fills the box, submits intact into the user card) is the default. The lone-"\n"
|
|
@@ -1431,14 +1444,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1431
1444
|
// Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
|
|
1432
1445
|
const combo = matchCursorCombo(data, i);
|
|
1433
1446
|
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
1434
|
-
// Up/Down inside a multi-line
|
|
1435
|
-
// visual rows (textarea feel).
|
|
1436
|
-
//
|
|
1437
|
-
// readline so
|
|
1447
|
+
// Up/Down inside a GENUINELY multi-line draft (explicit Shift+Enter breaks →
|
|
1448
|
+
// SENTINEL) move the caret between the box's visual rows (textarea feel). A
|
|
1449
|
+
// single line that merely SOFT-WRAPS is NOT treated as multi-line: ↑/↓ fall
|
|
1450
|
+
// through to readline so they recall input history — the dominant REPL
|
|
1451
|
+
// expectation. (Without this gate, ↑ on a wrapped one-liner jumped the caret up
|
|
1452
|
+
// a visual row instead of recalling the previous prompt, so the last wrapped
|
|
1453
|
+
// word appeared to "follow the caret up".) Skipped when a slash list or history
|
|
1454
|
+
// panel owns ↑/↓, and at the top/bottom edge the keys still reach history.
|
|
1438
1455
|
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
1439
1456
|
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
1440
1457
|
const line = activeRl?.line ?? "";
|
|
1441
|
-
if (line
|
|
1458
|
+
if (shouldBoxVerticalNav(line, { slashMatchCount: navMatches.length, historyPanelOpen: promptHistoryLines != null }) && activeRl) {
|
|
1442
1459
|
const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
1443
1460
|
const textWidth = Math.max(1, Math.max(24, winCols) - 6);
|
|
1444
1461
|
const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
|
|
@@ -2579,6 +2596,35 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2579
2596
|
return;
|
|
2580
2597
|
}
|
|
2581
2598
|
if (!previewArmed || pickerActive) return;
|
|
2599
|
+
// Drag-and-drop file attach: a terminal delivers a dropped file as its PATH typed
|
|
2600
|
+
// into the box, wrapped in a bracketed paste. On paste-end, swap any readable image
|
|
2601
|
+
// path LIVE for the same `[image #N]` tag Ctrl+V uses, so the box shows the tag
|
|
2602
|
+
// instead of a long raw filesystem path. Non-image / unreadable paths and ordinary
|
|
2603
|
+
// text pastes match nothing and are a no-op. Submit-time attach (below) still covers
|
|
2604
|
+
// terminals that deliver a drop WITHOUT bracketing it.
|
|
2605
|
+
if (key?.name === "paste-end" && !pasteInFlight && !pasteLineFired) {
|
|
2606
|
+
pasteInFlight = true;
|
|
2607
|
+
// Defer one tick: the dropped bytes reach readline through the keyFilter
|
|
2608
|
+
// PassThrough, so rl.line is only settled after the current stream turn.
|
|
2609
|
+
setImmediate(() => {
|
|
2610
|
+
void (async () => {
|
|
2611
|
+
try {
|
|
2612
|
+
const rli = rl as unknown as { line: string; cursor: number };
|
|
2613
|
+
const before = rli.line;
|
|
2614
|
+
const dropped = await attachImagePaths(before, pendingImages.length + 1);
|
|
2615
|
+
if (dropped.images.length === 0 || dropped.text === before) return;
|
|
2616
|
+
pendingImages.push(...dropped.images);
|
|
2617
|
+
rli.line = dropped.text;
|
|
2618
|
+
rli.cursor = dropped.text.length; // drop lands at the caret (line end in the common case)
|
|
2619
|
+
typedLine = dropped.text;
|
|
2620
|
+
if (previewArmed) drawFooter(previewLines(typedLine, navIdx));
|
|
2621
|
+
} finally {
|
|
2622
|
+
pasteInFlight = false;
|
|
2623
|
+
}
|
|
2624
|
+
})();
|
|
2625
|
+
});
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2582
2628
|
// Ctrl+L: redraw / re-anchor the prompt. The recovery for a footer whose in-place
|
|
2583
2629
|
// anchor drifted after the screen scrolled (typed text stops showing in the box).
|
|
2584
2630
|
if (key?.ctrl && key.name === "l") {
|
package/src/commands/ralplan.ts
CHANGED
|
@@ -257,6 +257,12 @@ export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise
|
|
|
257
257
|
cleanPlan = revised;
|
|
258
258
|
await fs.writeFile(planPath, cleanPlan, "utf-8");
|
|
259
259
|
gate = await runConsensusCriticGate({ cwd, seedContent, plan: cleanPlan, signal: opts.signal });
|
|
260
|
+
} else {
|
|
261
|
+
// The revision did not parse as a schema/role-valid plan, so it cannot be
|
|
262
|
+
// re-gated — the original [ITERATE] verdict stands. Report this explicitly
|
|
263
|
+
// instead of silently discarding the revision attempt (which otherwise
|
|
264
|
+
// surfaces only as the unchanged "verdict: ITERATE" failure below).
|
|
265
|
+
log(`[ralplan] The revised plan was not schema/role-valid — discarding the revision; the [ITERATE] verdict stands.`);
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
ralplanState.plan_path = planPath;
|
package/src/commands/team.ts
CHANGED
|
@@ -347,6 +347,7 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
347
347
|
cwd,
|
|
348
348
|
roleId: roleByIndex[activeIndex],
|
|
349
349
|
strictMutations: opts.strictMutations ?? false,
|
|
350
|
+
log,
|
|
350
351
|
});
|
|
351
352
|
|
|
352
353
|
if (opts.signal?.aborted) {
|
|
@@ -393,13 +394,14 @@ export async function runTeamCommand(args: string[] = []): Promise<void> {
|
|
|
393
394
|
}
|
|
394
395
|
}
|
|
395
396
|
|
|
396
|
-
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string; strictMutations?: boolean }): Promise<boolean> {
|
|
397
|
+
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string; strictMutations?: boolean; log: (line: string) => void }): Promise<boolean> {
|
|
398
|
+
const log = ctx.log;
|
|
397
399
|
const config = await readGlobalConfig();
|
|
398
400
|
const role = getSubagentRole(ctx.roleId, config) ?? defaultSubagentRole();
|
|
399
401
|
const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
|
|
400
402
|
const model = resolveSubagentModel(role.id, config);
|
|
401
403
|
const maxSteps = resolveSubagentMaxSteps(role.id, config);
|
|
402
|
-
|
|
404
|
+
log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
|
|
403
405
|
|
|
404
406
|
const contextTokens = catalogMetadata(model)?.contextTokens;
|
|
405
407
|
|
|
@@ -428,13 +430,13 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
428
430
|
events: {
|
|
429
431
|
onAssistant: (_raw, invocation) => {
|
|
430
432
|
if (!invocation) {
|
|
431
|
-
|
|
433
|
+
log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
|
|
432
434
|
} else if (invocation.tool !== "done") {
|
|
433
|
-
|
|
435
|
+
log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
|
|
434
436
|
}
|
|
435
437
|
},
|
|
436
438
|
onStep: async step => {
|
|
437
|
-
|
|
439
|
+
log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`, renderOpts));
|
|
438
440
|
try {
|
|
439
441
|
await maybeCompact(history, { model, contextTokens });
|
|
440
442
|
} catch (err) {
|
|
@@ -446,27 +448,27 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
446
448
|
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
|
|
447
449
|
else if (tool === "bash") bashRuns++;
|
|
448
450
|
}
|
|
449
|
-
|
|
451
|
+
log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
|
|
450
452
|
},
|
|
451
|
-
onNotice: msg =>
|
|
453
|
+
onNotice: msg => log(formatRalphStreamEvent("step", msg, renderOpts)),
|
|
452
454
|
},
|
|
453
455
|
});
|
|
454
456
|
|
|
455
457
|
const reason = result.doneReason?.trim() || `${role.title} did not converge within ${result.steps} steps`;
|
|
456
458
|
if (!result.done) {
|
|
457
|
-
|
|
459
|
+
log(formatRalphStreamEvent("error", reason, renderOpts));
|
|
458
460
|
return false;
|
|
459
461
|
}
|
|
460
462
|
|
|
461
463
|
const contract = validateSubagentDoneReason(role, reason);
|
|
462
464
|
if (!contract.ok) {
|
|
463
|
-
|
|
465
|
+
log(formatRalphStreamEvent("error", `${role.title} report incomplete: missing ${contract.missing?.join(", ")}`, renderOpts));
|
|
464
466
|
return false;
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
const gate = parseRoleGateVerdict(role.id, reason);
|
|
468
470
|
if (!gate.ok) {
|
|
469
|
-
|
|
471
|
+
log(formatRalphStreamEvent("error", gate.message ?? `${role.title} blocked execution`, renderOpts));
|
|
470
472
|
return false;
|
|
471
473
|
}
|
|
472
474
|
|
|
@@ -483,11 +485,17 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
483
485
|
const hardFail = ctx.strictMutations && bashRuns === 0;
|
|
484
486
|
// Round-12: separate the tones so a passing advisory run doesn't masquerade
|
|
485
487
|
// as a stream:error — only a real hard-fail is red; an advisory note is warn.
|
|
486
|
-
|
|
488
|
+
log(formatRalphStreamEvent(hardFail ? "error" : "warn", msg, renderOpts));
|
|
487
489
|
if (hardFail) {
|
|
488
490
|
return false;
|
|
489
491
|
}
|
|
490
492
|
}
|
|
491
|
-
|
|
493
|
+
// Surface the subagent's ACTUAL report to the user — not just a "finished"
|
|
494
|
+
// status. Previously `reason` was consumed only for validation/gating and the
|
|
495
|
+
// report content was discarded, so `team` runs produced no visible answer.
|
|
496
|
+
log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
|
|
497
|
+
log(`\n${role.title} report:`);
|
|
498
|
+
log(reason); // log() already splits multi-line strings across the io.output sink
|
|
499
|
+
|
|
492
500
|
return true;
|
|
493
501
|
}
|