jeo-code 0.5.5 → 0.5.7

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/README.ja.md CHANGED
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
150
150
  ## 変更履歴 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
154
+ - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
153
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
154
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
155
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
156
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
157
- - **[0.5.1]** (2026-06-14) — cmd-mode `!<command>` shell escape — run a shell command without engaging the agent.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
150
150
  ## 변경 이력 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
154
+ - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
153
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
154
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
155
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
156
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
157
- - **[0.5.1]** (2026-06-14) — cmd-mode `!<command>` shell escape — run a shell command without engaging the agent.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
150
150
  ## Changelog
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
154
+ - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
153
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
154
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
155
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
156
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
157
- - **[0.5.1]** (2026-06-14) — cmd-mode `!<command>` shell escape — run a shell command without engaging the agent.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
150
150
  ## 更新日志 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
154
+ - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
153
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
154
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
155
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
156
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
157
- - **[0.5.1]** (2026-06-14) — cmd-mode `!<command>` shell escape — run a shell command without engaging the agent.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -1931,6 +1931,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1931
1931
  const multilineInput = !!process.stdin.isTTY && jeoEnv("NO_MULTILINE") !== "1";
1932
1932
  const loneLfShiftEnter = jeoEnv("MULTILINE") === "1";
1933
1933
  const expandSentinel = (s: string): string => (multilineInput ? s.split(SENTINEL).join("\n") : s);
1934
+ // Prompt-scoped process listeners (stdin data/keypress, stdout resize). Registered
1935
+ // once per launch but previously anonymous and never removed — benign for a single
1936
+ // CLI run, but repeated launch() (test harness) accumulated them past Node's
1937
+ // 10-listener default → MaxListenersExceededWarning + a real leak. Track each remover
1938
+ // and drain it on every exit path so the process listener set returns to baseline.
1939
+ const promptListenerCleanups: Array<() => void> = [];
1940
+ const drainPromptListeners = () => {
1941
+ for (const off of promptListenerCleanups.splice(0)) { try { off(); } catch { /* best effort */ } }
1942
+ };
1934
1943
  let keyFilter: PassThrough | undefined;
1935
1944
  if (multilineInput) {
1936
1945
  const kf = new PassThrough();
@@ -1951,7 +1960,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1951
1960
  // off) and the xterm "\x1b[27;2;13~" / kitty "\x1b[13;2u" sequences. Enter ("\r")
1952
1961
  // passes through and submits.
1953
1962
  let kfInPaste = false;
1954
- process.stdin.on("data", (chunk: Buffer) => {
1963
+ const kfDataHandler = (chunk: Buffer) => {
1955
1964
  const data = chunk.toString("utf8");
1956
1965
  let out = "";
1957
1966
  let i = 0;
@@ -1972,7 +1981,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1972
1981
  out += data[i]; i += 1;
1973
1982
  }
1974
1983
  kf.write(out);
1975
- });
1984
+ };
1985
+ process.stdin.on("data", kfDataHandler);
1986
+ promptListenerCleanups.push(() => process.stdin.off("data", kfDataHandler));
1976
1987
  keyFilter = kf;
1977
1988
  // readline now decodes keypresses on `keyFilter`; keep process.stdin emitting
1978
1989
  // 'keypress' too so the footer-redraw / paste-marker / picker listeners (registered
@@ -2032,13 +2043,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2032
2043
  const pasteMerge: { buf: string[]; endWaiters: Array<() => void> } = { buf: [], endWaiters: [] };
2033
2044
  let pasteLineFired = false; // the line that resolved rl.question came from inside a paste
2034
2045
  if (process.stdin.isTTY) {
2035
- process.stdin.on("keypress", (_ch: string, key: { name?: string } | undefined) => {
2046
+ const pasteKeypressHandler = (_ch: string, key: { name?: string } | undefined) => {
2036
2047
  if (key?.name === "paste-start") { promptPasteActive = true; pasteMerge.buf = []; }
2037
2048
  else if (key?.name === "paste-end") {
2038
2049
  promptPasteActive = false;
2039
2050
  for (const w of pasteMerge.endWaiters.splice(0)) w();
2040
2051
  }
2041
- });
2052
+ };
2053
+ process.stdin.on("keypress", pasteKeypressHandler);
2054
+ promptListenerCleanups.push(() => process.stdin.off("keypress", pasteKeypressHandler));
2042
2055
  // Enable bracketed paste for the REPL lifetime (restored on exit below):
2043
2056
  // terminals only wrap pastes in the 200~/201~ markers once the app opts in.
2044
2057
  process.stdout.write("\x1b[?2004h");
@@ -2540,28 +2553,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2540
2553
  const notReadyWarning = (st: { name: string; label: string }): string =>
2541
2554
  ` ! ${st.name} is not call-ready yet (${st.label}) — run /provider login antigravity before the first turn.`;
2542
2555
 
2543
- const CORE_MODEL_ACTION_ROLE_ORDER = ["executor", "architect", "planner", "critic"] as const;
2556
+
2544
2557
  const MODEL_BADGE_ROLE_ORDER = ["planner", "architect", "executor", "critic"] as const;
2545
2558
 
2546
2559
  const roleBadgeColor = (roleId: string): ModelAssignmentBadge["color"] =>
2547
2560
  roleId === "executor" || roleId === "architect" || roleId === "planner" || roleId === "critic" ? roleId : "critic";
2548
2561
 
2549
- const orderedModelRoles = (config: Awaited<ReturnType<typeof readGlobalConfig>>) => {
2550
- const roles = allSubagentRoles(config);
2551
- const emitted = new Set<string>();
2552
- const out: ReturnType<typeof allSubagentRoles> = [];
2553
- for (const id of CORE_MODEL_ACTION_ROLE_ORDER) {
2554
- const role = roles.find(r => r.id === id);
2555
- if (role) {
2556
- emitted.add(role.id);
2557
- out.push(role);
2558
- }
2559
- }
2560
- for (const role of roles) {
2561
- if (!emitted.has(role.id)) out.push(role);
2562
- }
2563
- return out;
2564
- };
2562
+
2565
2563
 
2566
2564
  const modelPickerAssignments = async (): Promise<ModelAssignmentBadge[]> => {
2567
2565
  const cfg = await readGlobalConfig();
@@ -2732,7 +2730,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2732
2730
  choices.push({
2733
2731
  value: "heading:default",
2734
2732
  label: "Set as DEFAULT (Default)",
2735
- hint: `${config.defaultModel} (${currentDefaultThinking})`,
2733
+ hint: `${config.defaultModel} (${currentDefaultThinking}) · roles → /agents`,
2736
2734
  disabled: true,
2737
2735
  });
2738
2736
  appendChildren([
@@ -2744,83 +2742,21 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2744
2742
  })),
2745
2743
  ]);
2746
2744
 
2747
- for (const role of orderedModelRoles(config)) {
2748
- const roleThinking = resolveSubagentThinking(role.id, config) ?? "inherit";
2749
- choices.push({
2750
- value: `heading:${role.id}`,
2751
- label: `Set as ${role.title.toUpperCase()} (${role.title})`,
2752
- hint: `${resolveSubagentModel(role.id, config)} (${roleThinking})`,
2753
- disabled: true,
2754
- });
2755
- appendChildren([
2756
- { value: `${role.id}:keep`, label: "Set model only", hint: `keep thinking ${roleThinking}` },
2757
- {
2758
- value: `${role.id}:inherit`,
2759
- label: "thinking inherit",
2760
- hint: `follow default (${config.thinkingLevel ?? "medium"})`,
2761
- },
2762
- ...levels.map(level => ({
2763
- value: `${role.id}:${level}`,
2764
- label: `thinking ${level}`,
2765
- hint: roleThinking === level ? "current" : `~${Math.round(thinkingMaxTokens(level) / 1000)}k tokens`,
2766
- })),
2767
- ]);
2768
- }
2769
-
2770
- choices.push({
2771
- value: "preset:openai-codex",
2772
- label: "Apply OpenAI Codex role preset",
2773
- hint: "Default medium · Executor low · Architect xhigh · Planner medium · Critic high",
2774
- });
2775
2745
  return choices;
2776
2746
  };
2777
2747
 
2778
- const applyOpenAiCodexRolePreset = async (target: string, cfgForPick: Awaited<ReturnType<typeof readGlobalConfig>>): Promise<void> => {
2779
- const roleThinking: Record<(typeof CORE_MODEL_ACTION_ROLE_ORDER)[number], ThinkLevel> = {
2780
- executor: "low",
2781
- architect: "xhigh",
2782
- planner: "medium",
2783
- critic: "high",
2784
- };
2785
- await saveConfigPatch(raw => {
2786
- let subagents = raw.subagents ?? {};
2787
- for (const roleId of CORE_MODEL_ACTION_ROLE_ORDER) {
2788
- subagents = withSubagentSetting({ subagents }, roleId, { model: target, thinking: roleThinking[roleId] });
2789
- }
2790
- return {
2791
- ...rememberModelPatch(raw, target),
2792
- thinkingLevel: "medium",
2793
- subagents,
2794
- };
2795
- });
2796
- sessionModel = target;
2797
- sessionThinking = "medium";
2798
- const { resolved, provider } = await describeModel(target);
2799
- const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2800
- console.log(`OpenAI Codex role preset applied to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })} — Default medium, Executor low, Architect xhigh, Planner medium, Critic high`);
2801
- };
2748
+
2802
2749
 
2803
2750
 
2804
2751
  const applyPickedModelWithTarget = async (target: string): Promise<boolean> => {
2805
2752
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
2806
2753
  const cfgForPick = await readGlobalConfig();
2754
+ // `/model` only assigns the DEFAULT model + (optionally) the default thinking.
2755
+ // Per-role model and thinking are configured in /agents (and /agents edit).
2807
2756
  const choice = await pickFromOptions(`Model Name: ${displayModelName(target)}\n\nAction for: ${target}`, modelActionChoices(cfgForPick)) ?? "default:keep";
2808
- if (choice === "preset:openai-codex") {
2809
- await applyOpenAiCodexRolePreset(target, cfgForPick);
2810
- return true;
2811
- }
2812
- const [applyTo, action = "keep"] = choice.split(":", 2);
2813
- if (applyTo === "heading") return false;
2814
- const roleTarget = applyTo !== "default" ? getSubagentRole(applyTo, cfgForPick) : undefined;
2757
+ const [, action = "keep"] = choice.split(":", 2);
2815
2758
  const { resolved, provider } = await describeModel(target);
2816
2759
  const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2817
- if (roleTarget) {
2818
- const thinkPatch = action === "inherit" ? { thinking: undefined } : isThinkingLevel(action) ? { thinking: action } : {};
2819
- await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, roleTarget.id, { model: target, ...thinkPatch }) }));
2820
- const thinkNote = action !== "keep" ? ` · thinking ${action}` : "";
2821
- console.log(`Subagent '${roleTarget.id}' model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${thinkNote} — saved (change anytime via /model or /agents)`);
2822
- return true;
2823
- }
2824
2760
  sessionModel = target;
2825
2761
  const defaultThinking = isThinkingLevel(action) ? action : undefined;
2826
2762
  if (defaultThinking) {
@@ -2830,7 +2766,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2830
2766
  ...rememberModelPatch(raw, target),
2831
2767
  ...(defaultThinking ? { thinkingLevel: defaultThinking } : {}),
2832
2768
  }));
2833
- console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default`);
2769
+ console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default. Role models/thinking: /agents`);
2834
2770
  return true;
2835
2771
  };
2836
2772
 
@@ -2935,7 +2871,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2935
2871
 
2936
2872
  if (previewEnabled) {
2937
2873
  process.once("exit", () => out.write("\x1b[?25h")); // safety net: never leave the cursor hidden
2938
- process.stdin.on("keypress", (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
2874
+ const footerKeypressHandler = (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
2939
2875
  if (key?.ctrl && key.name === "c") {
2940
2876
  forceExitFromCtrlC();
2941
2877
  return;
@@ -3050,18 +2986,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3050
2986
  drawFooter(previewLines(typedLine));
3051
2987
  } catch { /* ignore render races */ }
3052
2988
  });
3053
- });
2989
+ };
2990
+ process.stdin.on("keypress", footerKeypressHandler);
2991
+ promptListenerCleanups.push(() => process.stdin.off("keypress", footerKeypressHandler));
3054
2992
  // Idle-prompt resize: re-reserve the footer at the new terminal height so the
3055
2993
  // fixed reservation stays accurate (otherwise the next paint would target the
3056
2994
  // old row count and either over-shoot or under-paint the reserved region).
3057
- process.stdout.on("resize", () => {
2995
+ const idleResizeHandler = () => {
3058
2996
  if (!previewArmed) return;
3059
2997
  try {
3060
2998
  disarmPreview();
3061
2999
  armPreview();
3062
3000
  drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
3063
3001
  } catch { /* ignore resize render races */ }
3064
- });
3002
+ };
3003
+ process.stdout.on("resize", idleResizeHandler);
3004
+ promptListenerCleanups.push(() => process.stdout.off("resize", idleResizeHandler));
3065
3005
  }
3066
3006
 
3067
3007
  while (true) {
@@ -3172,7 +3112,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3172
3112
  }
3173
3113
  if (input === "/clear") {
3174
3114
  history.length = 1;
3175
- console.log("(history cleared)");
3115
+ // Back to the initial screen: wipe the conversation, clear the terminal +
3116
+ // scrollback, and re-render the welcome banner so /clear looks like a fresh launch.
3117
+ if (process.stdout.isTTY) {
3118
+ disarmPreview();
3119
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H"); // clear screen + scrollback + cursor home
3120
+ console.log(renderWelcome(welcomeData).join("\n"));
3121
+ }
3122
+ console.log("(history cleared — back to the start screen)");
3176
3123
  continue;
3177
3124
  }
3178
3125
  if (input === "/compact") {
@@ -3276,28 +3223,72 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3276
3223
  continue;
3277
3224
  }
3278
3225
  if (input === "/resume" || input.startsWith("/resume ")) {
3279
- const id = input.substring(7).trim();
3280
- if (!id) {
3281
- const sessions = await listSessions(cwd);
3282
- if (sessions.length === 0) {
3283
- console.log("(no saved sessions)");
3284
- continue;
3285
- }
3286
- console.log("Saved sessions — resume with /resume <id>:");
3287
- for (const s of sessions.slice(0, 15)) {
3288
- const marker = s.id === sessionId ? "*" : " ";
3289
- console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
3226
+ const arg = input.substring(7).trim();
3227
+ // Load a session into history and print its transcript so the resume is visible.
3228
+ const applyResume = async (rid: string): Promise<void> => {
3229
+ try {
3230
+ const { messages } = await loadSession(rid, cwd);
3231
+ history.length = 1;
3232
+ for (const m of messages) history.push(m);
3233
+ sessionId = rid;
3234
+ // Seed /retry + reply marker from the last user/assistant turn.
3235
+ lastUserInput = ""; lastReply = "";
3236
+ for (let k = history.length - 1; k >= 1; k--) {
3237
+ if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
3238
+ if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
3239
+ if (lastUserInput && lastReply) break;
3240
+ }
3241
+ // Seed readline's input history so ↑ in the prompt recalls THIS session's
3242
+ // prior prompts (not just lines typed in the current run). readline history
3243
+ // is newest-first; unshift in chronological order so the session's newest
3244
+ // prompt lands at the front (first ↑). Skip injected/framed messages.
3245
+ const rli = rl as unknown as { history?: string[] };
3246
+ if (Array.isArray(rli.history)) {
3247
+ const priorPrompts = history
3248
+ .filter(m => m.role === "user")
3249
+ .map(m => String(m.content ?? "").trim())
3250
+ .filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
3251
+ for (const p of priorPrompts) {
3252
+ if (rli.history[0] !== p) rli.history.unshift(p);
3253
+ }
3254
+ }
3255
+ const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
3256
+ logLines([
3257
+ sep,
3258
+ `resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
3259
+ sep,
3260
+ ...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
3261
+ sep,
3262
+ ]);
3263
+ } catch (err) {
3264
+ console.log(`! ${(err as Error).message}`);
3290
3265
  }
3266
+ };
3267
+ if (arg) { await applyResume(arg); continue; }
3268
+ // No id → only sessions with a real conversation are resumable (every launch
3269
+ // creates an empty session; those are noise).
3270
+ const sessions = (await listSessions(cwd)).filter(s => s.messageCount > 0);
3271
+ if (sessions.length === 0) {
3272
+ console.log("(no saved sessions with history)");
3291
3273
  continue;
3292
3274
  }
3293
- try {
3294
- const { messages } = await loadSession(id, cwd);
3295
- history.length = 1;
3296
- for (const m of messages) history.push(m);
3297
- sessionId = id;
3298
- console.log(`Resumed session ${id} (${messages.length} messages).`);
3299
- } catch (err) {
3300
- console.log(`! ${(err as Error).message}`);
3275
+ // Interactive arrow-key picker on a TTY: ↑↓ to move, Enter to resume, Esc cancels.
3276
+ if (process.stdin.isTTY && process.stdout.isTTY) {
3277
+ const items: SelectItem<string>[] = sessions.slice(0, 50).map(s => ({
3278
+ value: s.id,
3279
+ label: `${s.title ? `[${s.title}] ` : ""}${(s.preview || s.id).replace(/\s+/g, " ")}`.slice(0, 76) || s.id,
3280
+ hint: `${s.messageCount} msgs${s.id === sessionId ? " · current" : ""}`,
3281
+ }));
3282
+ const picked = await pickFromOptions("Resume a session ↑↓ move · Enter resume · Esc cancel", items);
3283
+ if (picked) await applyResume(picked);
3284
+ else console.log("(resume cancelled)");
3285
+ continue;
3286
+ }
3287
+ // Non-TTY fallback: static list (resume with /resume <id>).
3288
+ console.log("Saved sessions — resume with /resume <id>:");
3289
+ for (const s of sessions.slice(0, 15)) {
3290
+ const marker = s.id === sessionId ? "*" : " ";
3291
+ console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
3301
3292
  }
3302
3293
  continue;
3303
3294
  }
@@ -3992,10 +3983,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3992
3983
  let roleModelArg = (roleMatch[3] ?? "").trim();
3993
3984
  const roleThinking = /^(?:thinking|think)(?:\s+(\S+))?$/i.exec(roleModelArg);
3994
3985
  if (roleThinking) {
3995
- await setRoleThinking(role.id, roleThinking[1]);
3986
+ console.log(`Subagent thinking is set via /agents — try: /agents ${role.id} thinking ${roleThinking[1] ?? "<level|inherit>"} (or /agents edit). /model only sets the default thinking.`);
3996
3987
  continue;
3997
3988
  }
3998
- let roleModelPickedFromSelector = false;
3989
+
3999
3990
  if (!roleModelArg && process.stdin.isTTY && process.stdout.isTTY) {
4000
3991
  const live = await getLiveModels();
4001
3992
  lastPickIndex = flattenModels(live);
@@ -4007,7 +3998,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4007
3998
  continue;
4008
3999
  }
4009
4000
  roleModelArg = qualifyModelId(picked.model, picked.provider);
4010
- roleModelPickedFromSelector = true;
4001
+
4011
4002
  }
4012
4003
  }
4013
4004
  if (roleModelArg && lastPickIndex.length) {
@@ -4019,7 +4010,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4019
4010
  continue;
4020
4011
  }
4021
4012
  roleModelArg = qualifyModelId(sel.entry.model, sel.entry.provider);
4022
- roleModelPickedFromSelector = true;
4013
+
4023
4014
  } else if (sel.kind === "ambiguous") {
4024
4015
  console.log(`'${roleModelArg}' matches ${sel.matches.length} models — be more specific:`);
4025
4016
  for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
@@ -4033,20 +4024,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4033
4024
  continue;
4034
4025
  }
4035
4026
  if (roleModelArg) {
4036
- let thinkPatch: { thinking?: ThinkLevel } = {};
4037
- if (roleModelPickedFromSelector && process.stdin.isTTY && process.stdout.isTTY) {
4038
- const cfgForRole = await readGlobalConfig();
4039
- const lvl = await pickThinkingLevel(
4040
- `Reasoning for ${role.title}: ${roleModelArg}`,
4041
- cfgForRole.subagents?.[role.id]?.thinking,
4042
- `inherit — follow default (${cfgForRole.thinkingLevel ?? "medium"})`,
4043
- );
4044
- thinkPatch = lvl === "inherit" ? { thinking: undefined } : lvl ? { thinking: lvl } : {};
4045
- }
4046
- await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: roleModelArg, ...thinkPatch }) }));
4027
+ await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: roleModelArg }) }));
4047
4028
  const { provider } = await describeModel(roleModelArg);
4048
- const thinkNote = thinkPatch.thinking ? ` · thinking ${thinkPatch.thinking}` : "";
4049
- console.log(`${role.title} model set to ${roleModelArg} (${provider})${thinkNote} — saved to ~/.jeo/config.json`);
4029
+ console.log(`${role.title} model set to ${roleModelArg} (${provider}) — saved to ~/.jeo/config.json. Set its thinking via /agents ${role.id} thinking <level> (or /agents edit).`);
4050
4030
  } else {
4051
4031
  const current = resolveSubagentModel(role.id, await readGlobalConfig());
4052
4032
  const { resolved, provider } = await describeModel(current);
@@ -4350,6 +4330,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4350
4330
  } catch { /* best effort */ }
4351
4331
  process.removeListener("SIGINT", forceExitFromCtrlC);
4352
4332
  process.stdin.off("data", forceExitOnCtrlCByte);
4333
+ drainPromptListeners();
4353
4334
  restorePromptRawMode();
4354
4335
  process.exit(130);
4355
4336
  }
@@ -4365,6 +4346,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4365
4346
  if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
4366
4347
  process.removeListener("SIGINT", forceExitFromCtrlC);
4367
4348
  process.stdin.off("data", forceExitOnCtrlCByte);
4349
+ drainPromptListeners();
4368
4350
  restorePromptRawMode();
4369
4351
  gracefulReadlineClose = true;
4370
4352
  rl.close();