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 +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/launch.ts +112 -130
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
package/src/commands/launch.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|