jeo-code 0.7.31 → 0.7.32
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 +23 -0
- package/README.ja.md +3 -1
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/package.json +3 -3
- package/scripts/install.sh +19 -0
- package/scripts/uninstall.sh +2 -2
- package/src/agent/AGENTS.md +12 -0
- package/src/agent/ast-edit-tool.ts +123 -0
- package/src/agent/ast-file-scan.ts +62 -0
- package/src/agent/ast-grep-tool.ts +107 -0
- package/src/agent/ast-match.ts +241 -0
- package/src/agent/debug-session.ts +435 -0
- package/src/agent/debug-tool.ts +89 -0
- package/src/agent/engine.ts +69 -12
- package/src/agent/goal-tool.ts +57 -0
- package/src/agent/irc-tool.ts +71 -0
- package/src/agent/job-registry.ts +163 -0
- package/src/agent/job-tool.ts +106 -0
- package/src/agent/lsp-rename-tool.ts +108 -0
- package/src/agent/lsp-tool.ts +177 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +40 -4
- package/src/agent/subagent-tool.ts +27 -7
- package/src/agent/subagents.ts +1 -1
- package/src/agent/task-tool.ts +27 -5
- package/src/agent/tools.ts +68 -1
- package/src/agent/ts-language-service.ts +173 -0
- package/src/commands/launch/agents-slash.ts +489 -0
- package/src/commands/launch/flags.ts +27 -9
- package/src/commands/launch/model-slash.ts +290 -0
- package/src/commands/launch/session-slash.ts +305 -0
- package/src/commands/launch/slash-handlers.ts +6 -0
- package/src/commands/launch/system-slash.ts +81 -0
- package/src/commands/launch.ts +208 -749
package/src/commands/launch.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { createTaskTool, taskToolProtocolLine, type TaskSubEvent } from "../agen
|
|
|
9
9
|
import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subagent-tool";
|
|
10
10
|
import { SubagentRegistry } from "../agent/subagent-registry";
|
|
11
11
|
import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
|
|
12
|
+
import { createJobTool, JOB_TOOL_PROTOCOL_LINE } from "../agent/job-tool";
|
|
13
|
+
import { JobRegistry } from "../agent/job-registry";
|
|
14
|
+
import { createIrcTool, IRC_TOOL_PROTOCOL_LINE } from "../agent/irc-tool";
|
|
15
|
+
import { createGoalTool, GOAL_TOOL_PROTOCOL_LINE } from "../agent/goal-tool";
|
|
12
16
|
import { LaunchTui } from "../tui/app";
|
|
13
17
|
import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-interview";
|
|
14
18
|
import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
@@ -46,7 +50,7 @@ import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/op
|
|
|
46
50
|
|
|
47
51
|
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting, applyTargetChoices } from "../agent/subagents";
|
|
48
52
|
import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
|
|
49
|
-
|
|
53
|
+
|
|
50
54
|
import {
|
|
51
55
|
formatModelLine,
|
|
52
56
|
formatProviderPanel,
|
|
@@ -70,7 +74,7 @@ import { renderStatusBar } from "../tui/components/status";
|
|
|
70
74
|
import { detectColorLevel, ColorLevel, visibleWidth } from "../tui/components/color";
|
|
71
75
|
import { readClipboardImage } from "../util/clipboard-image";
|
|
72
76
|
import { attachImagePaths, insertImageTag } from "../util/file-attachment";
|
|
73
|
-
|
|
77
|
+
|
|
74
78
|
import { copyTextToClipboard, readClipboardText } from "../tui/clipboard";
|
|
75
79
|
import { loadInputHistory, appendInputHistory } from "../agent/input-history";
|
|
76
80
|
import type { ImageAttachment } from "../ai/types";
|
|
@@ -95,11 +99,9 @@ import {
|
|
|
95
99
|
listSessions,
|
|
96
100
|
latestSessionId,
|
|
97
101
|
exportSession,
|
|
98
|
-
renameSession,
|
|
99
102
|
updateSessionModel,
|
|
100
|
-
deleteSession,
|
|
101
|
-
sessionPath,
|
|
102
103
|
appendCompaction,
|
|
104
|
+
resolveSessionRef,
|
|
103
105
|
} from "../agent/session";
|
|
104
106
|
import { clearLine, cursorUp, toColumn, truncate as truncateAnsi, size as terminalSize, resetMouseTracking, clearScreen, clearVisible, clearToEnd, enableModifyOtherKeys, disableModifyOtherKeys, enableKittyKeyboard, disableKittyKeyboard } from "../tui/terminal";
|
|
105
107
|
|
|
@@ -197,7 +199,11 @@ import {
|
|
|
197
199
|
handleSearchSlash,
|
|
198
200
|
} from "./launch/code-slash";
|
|
199
201
|
import { handleUndoSlash } from "./launch/git-slash";
|
|
202
|
+
import { runSessionSlash } from "./launch/session-slash";
|
|
203
|
+
import { runModelSlash } from "./launch/model-slash";
|
|
204
|
+
import { runAgentsSlash, runRolesSlash, runFastSlash, runThinkingSlash } from "./launch/agents-slash";
|
|
200
205
|
import { wikiRootPromptLine, decideWikiSlash } from "./launch/wiki-slash";
|
|
206
|
+
import { decideThemeSlash, buildConfigPanelLines, runEvolveSimulation } from "./launch/system-slash";
|
|
201
207
|
|
|
202
208
|
|
|
203
209
|
|
|
@@ -267,6 +273,12 @@ export function normalizeSlashAlias(input: string): string {
|
|
|
267
273
|
if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
|
|
268
274
|
if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
|
|
269
275
|
if (input === "/resume" || input.startsWith("/resume ")) return `/session resume${input.slice("/resume".length)}`;
|
|
276
|
+
// gjc-parity: /new, /drop, /rename, /sessions are top-level palette aliases for
|
|
277
|
+
// their /session <sub> implementations (same pattern as /resume above).
|
|
278
|
+
if (input === "/new") return "/session new";
|
|
279
|
+
if (input === "/drop") return "/session drop";
|
|
280
|
+
if (input === "/rename" || input.startsWith("/rename ")) return `/session rename${input.slice("/rename".length)}`;
|
|
281
|
+
if (input === "/sessions") return "/session list";
|
|
270
282
|
return input;
|
|
271
283
|
}
|
|
272
284
|
|
|
@@ -513,7 +525,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
513
525
|
// pi-style: load project context (JEO.md / AGENTS.md / .jeo/context.md / CLAUDE.md) into the prompt.
|
|
514
526
|
const contextFiles = await loadProjectContext(cwd);
|
|
515
527
|
|
|
516
|
-
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo", "subagent"]);
|
|
528
|
+
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo", "subagent", "job", "irc", "goal", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug"]);
|
|
517
529
|
let allowedTools = new Set(KNOWN_TOOLS);
|
|
518
530
|
|
|
519
531
|
if (flags.noTools) {
|
|
@@ -584,6 +596,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
584
596
|
(allowedTools.has("todo") ? "\n\nPlanning: " + TODO_TOOL_PROTOCOL_LINE : "") +
|
|
585
597
|
(allowedTools.has("subagent") ? "\n\nDetached subagents: " + SUBAGENT_TOOL_PROTOCOL_LINE +
|
|
586
598
|
" Launch background work with task {\"detached\": true, \"role\": <role>, \"task\": <assignment>}; it returns a subagent id immediately so you can keep working and collect the result later." : "") +
|
|
599
|
+
(allowedTools.has("job") ? "\n\nBackground jobs: " + JOB_TOOL_PROTOCOL_LINE : "") +
|
|
600
|
+
(allowedTools.has("irc") ? "\n\nPeer messaging: " + IRC_TOOL_PROTOCOL_LINE : "") +
|
|
601
|
+
(allowedTools.has("goal") ? "\n\nGoal tracking: " + GOAL_TOOL_PROTOCOL_LINE : "") +
|
|
587
602
|
(effectiveNoSkills ? "" :
|
|
588
603
|
"\n\nJEO workflow routing:\n" +
|
|
589
604
|
"- Answer the user's request DIRECTLY. Never reply with a catalog, list, or summary of skills unless the user explicitly asks what skills exist.\n" +
|
|
@@ -710,9 +725,50 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
710
725
|
let sessionId: string | undefined;
|
|
711
726
|
let compactionSeq = 0;
|
|
712
727
|
if (!flags.noSession) {
|
|
713
|
-
if (flags.
|
|
714
|
-
|
|
715
|
-
|
|
728
|
+
if (flags.resumeInteractive) {
|
|
729
|
+
// Bare `--resume`/`-r` (no value) at cold startup (gjc-parity): the interactive
|
|
730
|
+
// session picker — added right before the REPL loop starts, below — takes over
|
|
731
|
+
// and replaces this session before the user ever sees it, but ONLY in a TTY.
|
|
732
|
+
// Start with a fresh session here as the safe default either way.
|
|
733
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
734
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
735
|
+
// No TTY means no picker is possible — fall back to the same silent-latest
|
|
736
|
+
// behavior as `--continue` rather than silently doing nothing.
|
|
737
|
+
const id = await latestSessionId(cwd);
|
|
738
|
+
if (id) {
|
|
739
|
+
try {
|
|
740
|
+
const { header, messages } = await loadSession(id, cwd);
|
|
741
|
+
for (const m of messages) history.push(m);
|
|
742
|
+
sessionId = id;
|
|
743
|
+
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
744
|
+
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
745
|
+
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
} else if (flags.resume) {
|
|
752
|
+
let id: string | undefined;
|
|
753
|
+
let skipResume = false;
|
|
754
|
+
if (flags.resumeId) {
|
|
755
|
+
// gjc-parity: resolve as a full id OR a short id prefix.
|
|
756
|
+
const resolved = await resolveSessionRef(flags.resumeId, cwd);
|
|
757
|
+
if (resolved.kind === "ok") {
|
|
758
|
+
id = resolved.id;
|
|
759
|
+
} else if (resolved.kind === "ambiguous") {
|
|
760
|
+
console.log(`Ambiguous session id '${flags.resumeId}' — matches: ${resolved.matches.slice(0, 5).join(", ")}${resolved.matches.length > 5 ? " …" : ""}. Use more characters or run with --resume (no value) to pick interactively.`);
|
|
761
|
+
skipResume = true;
|
|
762
|
+
} else {
|
|
763
|
+
console.log(`Session '${flags.resumeId}' not found. Starting a new one.`);
|
|
764
|
+
skipResume = true;
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
id = await latestSessionId(cwd);
|
|
768
|
+
}
|
|
769
|
+
if (skipResume) {
|
|
770
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
771
|
+
} else if (!id) {
|
|
716
772
|
console.log("No session to resume. Starting a new one.");
|
|
717
773
|
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
718
774
|
} else {
|
|
@@ -964,6 +1020,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
964
1020
|
// #9: per-turn registry for DETACHED subagents (task{detached:true}); the
|
|
965
1021
|
// `subagent` tool controls them and cancelAll() in finally prevents orphans.
|
|
966
1022
|
const subagentRegistry = new SubagentRegistry();
|
|
1023
|
+
const jobRegistry = new JobRegistry();
|
|
967
1024
|
try {
|
|
968
1025
|
// Per-turn todo snapshot: drives the done-time reconciliation gate (the
|
|
969
1026
|
// Todos checklist used to end a finished turn stuck at "✓0 ◐1 ·4 / 5"
|
|
@@ -1025,6 +1082,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1025
1082
|
}),
|
|
1026
1083
|
todo: createTodoTool({ onChange: items => { turnTodos = items; tui?.setTodos(items); } }),
|
|
1027
1084
|
subagent: createSubagentTool(subagentRegistry),
|
|
1085
|
+
job: createJobTool(jobRegistry),
|
|
1086
|
+
irc: createIrcTool(subagentRegistry),
|
|
1087
|
+
goal: createGoalTool(),
|
|
1028
1088
|
};
|
|
1029
1089
|
const tools = filterToolMap(fullTools, Array.from(allowedTools));
|
|
1030
1090
|
// Opik observability (opt-in via JEO_OPIK): one trace per turn, spans per
|
|
@@ -1083,6 +1143,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1083
1143
|
} finally {
|
|
1084
1144
|
harness.dispose();
|
|
1085
1145
|
subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
|
|
1146
|
+
jobRegistry.cancelAll(); // background jobs are turn-scoped too — no orphaned processes
|
|
1086
1147
|
// Steering typed but never drained (e.g. entered just after the final step)
|
|
1087
1148
|
// must not be lost — fold it into the next prompt draft so it runs next.
|
|
1088
1149
|
const leftover = steerInbox.splice(0, steerInbox.length).map(s => s.trim()).filter(Boolean);
|
|
@@ -1217,6 +1278,74 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1217
1278
|
console.log("Tools: read / write / edit / bash / find / search. Sessions persist to .jeo/sessions/.");
|
|
1218
1279
|
return;
|
|
1219
1280
|
}
|
|
1281
|
+
if (cmd === "/usage") {
|
|
1282
|
+
const cfg = await readGlobalConfig();
|
|
1283
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
1284
|
+
const result = handleUsage(ctx, sessionUsage);
|
|
1285
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (cmd === "/context") {
|
|
1289
|
+
const cfg = await readGlobalConfig();
|
|
1290
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
1291
|
+
const result = await handleContext(ctx);
|
|
1292
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (cmd === "/tools") {
|
|
1296
|
+
const cfg = await readGlobalConfig();
|
|
1297
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
1298
|
+
const result = await handleTools(ctx);
|
|
1299
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (cmd === "/hotkeys") {
|
|
1303
|
+
const cfg = await readGlobalConfig();
|
|
1304
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
1305
|
+
const result = handleHotkeys(ctx);
|
|
1306
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (cmd === "/theme" || cmd.startsWith("/theme ")) {
|
|
1310
|
+
const decision = decideThemeSlash(cmd, listThemes(), resolveTheme().name);
|
|
1311
|
+
for (const line of decision.lines) console.log(line);
|
|
1312
|
+
if (decision.kind === "set") {
|
|
1313
|
+
process.env.JEO_TUI_THEME = decision.themeName!;
|
|
1314
|
+
await saveConfigPatch(raw => ({ theme: decision.themeName }));
|
|
1315
|
+
// No refreshUiTheme() here — one-shot mode returns immediately after this
|
|
1316
|
+
// block, so there is no live TUI paint state to re-resolve (and
|
|
1317
|
+
// refreshUiTheme is declared later in this closure, out of scope here).
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (cmd === "/wiki" || cmd.startsWith("/wiki ")) {
|
|
1322
|
+
const decision = decideWikiSlash(
|
|
1323
|
+
cmd,
|
|
1324
|
+
resolveWikiRoot(await readGlobalConfig()),
|
|
1325
|
+
!!jeoEnv("WIKI_ROOT"),
|
|
1326
|
+
);
|
|
1327
|
+
for (const line of decision.lines) console.log(line);
|
|
1328
|
+
if (decision.kind === "clear") {
|
|
1329
|
+
await saveConfigPatch(() => ({ wikiRoot: undefined }));
|
|
1330
|
+
delete process.env.JEO_WIKI_ROOT;
|
|
1331
|
+
sessionWikiRoot = undefined;
|
|
1332
|
+
await refreshSessionMemory("");
|
|
1333
|
+
} else if (decision.kind === "set") {
|
|
1334
|
+
await saveConfigPatch(() => ({ wikiRoot: decision.persistArg }));
|
|
1335
|
+
process.env.JEO_WIKI_ROOT = decision.root;
|
|
1336
|
+
sessionWikiRoot = decision.root;
|
|
1337
|
+
await refreshSessionMemory("");
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (cmd === "/config" || cmd === "/settings") {
|
|
1342
|
+
for (const line of await buildConfigPanelLines({ sessionModel, sessionThinking, sessionId })) console.log(line);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (cmd === "/evolve") {
|
|
1346
|
+
await runEvolveSimulation(animateAsciiArt);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1220
1349
|
}
|
|
1221
1350
|
// One skill run (bundle workflow → engine; regular skill → agent turn). Shared by the
|
|
1222
1351
|
// single-invocation path and the `$a $b …` chain path so every `$` skill actually runs.
|
|
@@ -2379,14 +2508,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2379
2508
|
}
|
|
2380
2509
|
};
|
|
2381
2510
|
|
|
2382
|
-
// Antigravity with ANY Google OAuth (own login or the gemini-cli fallback) stays
|
|
2383
|
-
// SELECTABLE in pickers even when not call-ready: picking the model is how users
|
|
2384
|
-
// reach the flow, and the auth layer gives actionable login guidance on the first
|
|
2385
|
-
// call if the fallback token is rejected (403). Refusing selection was a dead end.
|
|
2386
|
-
const selectableThoughNotReady = (st?: { name: string; kind: string }): boolean =>
|
|
2387
|
-
!!st && st.name === "antigravity" && st.kind === "oauth";
|
|
2388
|
-
const notReadyWarning = (st: { name: string; label: string }): string =>
|
|
2389
|
-
` ! ${st.name} is not call-ready yet (${st.label}) — run /provider login antigravity before the first turn.`;
|
|
2390
2511
|
|
|
2391
2512
|
|
|
2392
2513
|
const MODEL_BADGE_ROLE_ORDER = ["planner", "architect", "executor", "critic"] as const;
|
|
@@ -3088,6 +3209,21 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3088
3209
|
});
|
|
3089
3210
|
}
|
|
3090
3211
|
|
|
3212
|
+
// Bare `--resume`/`-r` (no value) at cold startup (gjc-parity): show the SAME
|
|
3213
|
+
// interactive session picker `/session resume` (no arg) uses, in place of the
|
|
3214
|
+
// fresh session created above. TTY-only — non-TTY already fell back to
|
|
3215
|
+
// silent-latest resume in the startup block above.
|
|
3216
|
+
if (flags.resumeInteractive && !flags.noSession && process.stdin.isTTY && process.stdout.isTTY) {
|
|
3217
|
+
const pickResult = await runSessionSlash("/session resume", {
|
|
3218
|
+
cwd, history, noSession: flags.noSession, sessionId, sessionModel, rl,
|
|
3219
|
+
advanceSessionBoxColor, disarmPreview, clearScreen, freshWelcomeLines, logLines, runSelectPicker,
|
|
3220
|
+
});
|
|
3221
|
+
sessionId = pickResult.sessionId;
|
|
3222
|
+
if (pickResult.sessionModel !== undefined) sessionModel = pickResult.sessionModel;
|
|
3223
|
+
if (pickResult.lastUserInput !== undefined) lastUserInput = pickResult.lastUserInput;
|
|
3224
|
+
if (pickResult.lastReply !== undefined) lastReply = pickResult.lastReply;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3091
3227
|
while (true) {
|
|
3092
3228
|
drainPendingTtyInput();
|
|
3093
3229
|
// "New input first": queued FULL lines from the previous turn are folded
|
|
@@ -3227,10 +3363,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3227
3363
|
const res = await maybeCompact(history, { model: activeModel, force: true, contextTokens });
|
|
3228
3364
|
if (res.error) {
|
|
3229
3365
|
console.error(chalk.red(res.error));
|
|
3230
|
-
} else if (res.compacted
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3366
|
+
} else if (res.compacted) {
|
|
3367
|
+
if (sessionId && res.replacesThrough !== undefined) {
|
|
3368
|
+
const touchedNote = res.touchedFiles?.length ? ` Files touched: ${res.touchedFiles.join(", ")}.` : "";
|
|
3369
|
+
const summaryText = res.summary ?? `[Earlier conversation omitted: ${res.removed} messages — summary unavailable.${touchedNote}]`;
|
|
3370
|
+
await appendCompaction(sessionId, ++compactionSeq, summaryText, res.replacesThrough, cwd);
|
|
3371
|
+
}
|
|
3234
3372
|
console.log(`(compacted ${res.removed} older messages${res.touchedFiles?.length ? `; kept ${res.touchedFiles.length} file refs` : ""})`);
|
|
3235
3373
|
} else {
|
|
3236
3374
|
console.log("(nothing to compact)");
|
|
@@ -3238,226 +3376,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3238
3376
|
continue;
|
|
3239
3377
|
}
|
|
3240
3378
|
if (input === "/session" || input.startsWith("/session ")) {
|
|
3241
|
-
const
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
console.log(`(${verb} — new session ${sessionId})`);
|
|
3250
|
-
} else {
|
|
3251
|
-
sessionId = undefined;
|
|
3252
|
-
console.log(`(${verb} — sessions disabled)`);
|
|
3253
|
-
}
|
|
3254
|
-
};
|
|
3255
|
-
|
|
3256
|
-
if (sub === "new") {
|
|
3257
|
-
await startFreshSession("started fresh");
|
|
3258
|
-
continue;
|
|
3259
|
-
}
|
|
3260
|
-
if (sub === "drop" || sub === "delete") {
|
|
3261
|
-
if (sessionId) {
|
|
3262
|
-
const removed = await deleteSession(sessionId, cwd);
|
|
3263
|
-
console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
|
|
3264
|
-
}
|
|
3265
|
-
await startFreshSession("dropped");
|
|
3266
|
-
continue;
|
|
3267
|
-
}
|
|
3268
|
-
if (sub === "rename") {
|
|
3269
|
-
const title = tokens.slice(1).join(" ").trim();
|
|
3270
|
-
if (!title) {
|
|
3271
|
-
console.log("Usage: /session rename <title>");
|
|
3272
|
-
continue;
|
|
3273
|
-
}
|
|
3274
|
-
if (!sessionId) {
|
|
3275
|
-
console.log("(sessions are disabled — nothing to rename)");
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
try {
|
|
3279
|
-
await renameSession(sessionId, title, cwd);
|
|
3280
|
-
console.log(`(session renamed to '${title}')`);
|
|
3281
|
-
} catch (err) {
|
|
3282
|
-
console.log(`! rename failed: ${(err as Error).message}`);
|
|
3283
|
-
}
|
|
3284
|
-
continue;
|
|
3285
|
-
}
|
|
3286
|
-
if (sub === "resume") {
|
|
3287
|
-
const arg = tokens.slice(1).join(" ").trim();
|
|
3288
|
-
const applyResume = async (rid: string): Promise<void> => {
|
|
3289
|
-
try {
|
|
3290
|
-
const { header, messages } = await loadSession(rid, cwd);
|
|
3291
|
-
history.length = 1;
|
|
3292
|
-
for (const m of messages) history.push(m);
|
|
3293
|
-
sessionId = rid;
|
|
3294
|
-
// Restore the model this session was last using (per-session model).
|
|
3295
|
-
if (header.model) sessionModel = header.model;
|
|
3296
|
-
// Seed /retry + reply marker from the last user/assistant turn.
|
|
3297
|
-
lastUserInput = ""; lastReply = "";
|
|
3298
|
-
for (let k = history.length - 1; k >= 1; k--) {
|
|
3299
|
-
if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
|
|
3300
|
-
if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
|
|
3301
|
-
if (lastUserInput && lastReply) break;
|
|
3302
|
-
}
|
|
3303
|
-
// Seed readline's input history so ↑ in the prompt recalls THIS session's
|
|
3304
|
-
// prior prompts (not just lines typed in the current run). readline history
|
|
3305
|
-
// is newest-first; unshift in chronological order so the session's newest
|
|
3306
|
-
// prompt lands at the front (first ↑). Skip injected/framed messages.
|
|
3307
|
-
const rli = rl as unknown as { history?: string[] };
|
|
3308
|
-
if (Array.isArray(rli.history)) {
|
|
3309
|
-
const priorPrompts = history
|
|
3310
|
-
.filter(m => m.role === "user")
|
|
3311
|
-
.map(m => String(m.content ?? "").trim())
|
|
3312
|
-
.filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
|
|
3313
|
-
for (const p of priorPrompts) {
|
|
3314
|
-
if (rli.history[0] !== p) rli.history.unshift(p);
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
// Clean restore: wipe the screen + scrollback BEFORE replaying the
|
|
3318
|
-
// transcript so it can't collide with picker remnants, the prior
|
|
3319
|
-
// conversation, or the live input frame — the "/resume corrupts the
|
|
3320
|
-
// TUI" fix. Same proven path as /clear; re-render the welcome banner
|
|
3321
|
-
// so the resumed view reads like a fresh, intact screen.
|
|
3322
|
-
if (process.stdout.isTTY) {
|
|
3323
|
-
disarmPreview();
|
|
3324
|
-
process.stdout.write(clearScreen());
|
|
3325
|
-
console.log(freshWelcomeLines().join("\n"));
|
|
3326
|
-
}
|
|
3327
|
-
const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
|
|
3328
|
-
logLines([
|
|
3329
|
-
sep,
|
|
3330
|
-
`resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
|
|
3331
|
-
sep,
|
|
3332
|
-
...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
|
|
3333
|
-
sep,
|
|
3334
|
-
]);
|
|
3335
|
-
} catch (err) {
|
|
3336
|
-
console.log(`! ${(err as Error).message}`);
|
|
3337
|
-
}
|
|
3338
|
-
};
|
|
3339
|
-
if (arg) { await applyResume(arg); continue; }
|
|
3340
|
-
// No id → only sessions with a real conversation are resumable (every launch
|
|
3341
|
-
// creates an empty session; those are noise).
|
|
3342
|
-
let pool = (await listSessions(cwd)).filter(s => s.messageCount > 0);
|
|
3343
|
-
if (pool.length === 0) {
|
|
3344
|
-
console.log("(no saved sessions with history)");
|
|
3345
|
-
continue;
|
|
3346
|
-
}
|
|
3347
|
-
// Interactive gjc-style picker on a TTY: type to filter, ↑↓/PgUp/PgDn to
|
|
3348
|
-
// move, Enter resumes, Del deletes (press Del twice to confirm), Esc cancels.
|
|
3349
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3350
|
-
// Loop so a delete refreshes the list and re-opens the picker in place.
|
|
3351
|
-
for (;;) {
|
|
3352
|
-
const picker = new SessionPicker(pool);
|
|
3353
|
-
let action: { kind: "resume" | "delete"; id: string } | undefined;
|
|
3354
|
-
let confirmDeleteId: string | undefined;
|
|
3355
|
-
await runSelectPicker(
|
|
3356
|
-
(cols, rows) => renderSessionPicker(picker, {
|
|
3357
|
-
title: "Resume a session",
|
|
3358
|
-
cols,
|
|
3359
|
-
rows: Math.max(8, rows),
|
|
3360
|
-
unicode: true,
|
|
3361
|
-
color: true,
|
|
3362
|
-
confirmDeleteId,
|
|
3363
|
-
}),
|
|
3364
|
-
(ch, key) => {
|
|
3365
|
-
if (key?.name === "up") { confirmDeleteId = undefined; picker.up(); return false; }
|
|
3366
|
-
if (key?.name === "down") { confirmDeleteId = undefined; picker.down(); return false; }
|
|
3367
|
-
if (key?.name === "pageup") { confirmDeleteId = undefined; picker.page(-1); return false; }
|
|
3368
|
-
if (key?.name === "pagedown") { confirmDeleteId = undefined; picker.page(1); return false; }
|
|
3369
|
-
if (key?.name === "escape" || (key?.ctrl && key.name === "c")) return true;
|
|
3370
|
-
if (key?.name === "delete") {
|
|
3371
|
-
const sel = picker.selected();
|
|
3372
|
-
if (!sel) return false;
|
|
3373
|
-
if (confirmDeleteId === sel.id) { action = { kind: "delete", id: sel.id }; return true; }
|
|
3374
|
-
confirmDeleteId = sel.id;
|
|
3375
|
-
return false;
|
|
3376
|
-
}
|
|
3377
|
-
if (key?.name === "return" || key?.name === "enter") {
|
|
3378
|
-
const sel = picker.selected();
|
|
3379
|
-
if (sel) { action = { kind: "resume", id: sel.id }; return true; }
|
|
3380
|
-
return false;
|
|
3381
|
-
}
|
|
3382
|
-
confirmDeleteId = undefined;
|
|
3383
|
-
if (key?.name === "backspace") { picker.backspace(); return false; }
|
|
3384
|
-
if (ch && ch >= " " && !key?.ctrl && !key?.meta) picker.typeChar(ch);
|
|
3385
|
-
return false;
|
|
3386
|
-
},
|
|
3387
|
-
);
|
|
3388
|
-
if (!action) { console.log("(resume cancelled)"); break; }
|
|
3389
|
-
if (action.kind === "resume") { await applyResume(action.id); break; }
|
|
3390
|
-
// Delete: drop the file, refresh the pool, and re-open the picker.
|
|
3391
|
-
const delId = action.id;
|
|
3392
|
-
try {
|
|
3393
|
-
const removed = await deleteSession(delId, cwd);
|
|
3394
|
-
console.log(removed ? `(deleted session ${delId})` : `(session ${delId} already gone)`);
|
|
3395
|
-
} catch (err) {
|
|
3396
|
-
console.log(`! delete failed: ${(err as Error).message}`);
|
|
3397
|
-
}
|
|
3398
|
-
if (delId === sessionId) await startFreshSession("dropped current session");
|
|
3399
|
-
pool = pool.filter(s => s.id !== delId);
|
|
3400
|
-
if (pool.length === 0) { console.log("(no saved sessions with history)"); break; }
|
|
3401
|
-
}
|
|
3402
|
-
continue;
|
|
3403
|
-
}
|
|
3404
|
-
// Non-TTY fallback: static list (resume with /session resume <id>).
|
|
3405
|
-
console.log("Saved sessions — resume with /session resume <id>:");
|
|
3406
|
-
for (const s of pool.slice(0, 15)) {
|
|
3407
|
-
const marker = s.id === sessionId ? "*" : " ";
|
|
3408
|
-
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
|
|
3409
|
-
}
|
|
3410
|
-
continue;
|
|
3411
|
-
}
|
|
3412
|
-
if (sub === "list") {
|
|
3413
|
-
const sessions = await listSessions(cwd);
|
|
3414
|
-
if (sessions.length === 0) console.log("(no saved sessions)");
|
|
3415
|
-
for (const s of sessions) {
|
|
3416
|
-
const marker = s.id === sessionId ? "*" : " ";
|
|
3417
|
-
const title = s.title ? `[${s.title}] ` : "";
|
|
3418
|
-
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
|
|
3419
|
-
}
|
|
3420
|
-
continue;
|
|
3421
|
-
}
|
|
3422
|
-
if (sub === "info") {
|
|
3423
|
-
if (!sessionId) {
|
|
3424
|
-
console.log("Session: disabled (--no-session)");
|
|
3425
|
-
continue;
|
|
3426
|
-
}
|
|
3427
|
-
const all = await listSessions(cwd);
|
|
3428
|
-
const current = all.find(s => s.id === sessionId);
|
|
3429
|
-
console.log("Session info:");
|
|
3430
|
-
console.log(` id ${sessionId}`);
|
|
3431
|
-
if (current?.title) console.log(` title ${current.title}`);
|
|
3432
|
-
console.log(` file ${sessionPath(sessionId, cwd)}`);
|
|
3433
|
-
console.log(` started ${current?.timestamp ?? "(this run)"}`);
|
|
3434
|
-
console.log(` messages ${current?.messageCount ?? Math.max(0, history.length - 1)} persisted · ${history.length - 1} in context`);
|
|
3435
|
-
console.log(` workspace ${cwd}`);
|
|
3436
|
-
continue;
|
|
3437
|
-
}
|
|
3438
|
-
if (sub && sub !== "info") {
|
|
3439
|
-
console.log("Usage: /session [list|info|new|drop|rename <title>|resume [id]]");
|
|
3440
|
-
continue;
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
|
-
// Default: list sessions AND show current session info
|
|
3444
|
-
const sessions = await listSessions(cwd);
|
|
3445
|
-
if (sessions.length === 0) {
|
|
3446
|
-
console.log("(no saved sessions)");
|
|
3447
|
-
} else {
|
|
3448
|
-
console.log("Saved sessions:");
|
|
3449
|
-
for (const s of sessions) {
|
|
3450
|
-
const marker = s.id === sessionId ? "*" : " ";
|
|
3451
|
-
const title = s.title ? `[${s.title}] ` : "";
|
|
3452
|
-
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
if (sessionId) {
|
|
3456
|
-
const current = sessions.find(s => s.id === sessionId);
|
|
3457
|
-
console.log(`\nCurrent session: ${sessionId}${current?.title ? ` [${current.title}]` : ""}`);
|
|
3458
|
-
} else {
|
|
3459
|
-
console.log("\nCurrent session: disabled (--no-session)");
|
|
3460
|
-
}
|
|
3379
|
+
const result = await runSessionSlash(input, {
|
|
3380
|
+
cwd, history, noSession: flags.noSession, sessionId, sessionModel, rl,
|
|
3381
|
+
advanceSessionBoxColor, disarmPreview, clearScreen, freshWelcomeLines, logLines, runSelectPicker,
|
|
3382
|
+
});
|
|
3383
|
+
sessionId = result.sessionId;
|
|
3384
|
+
if (result.sessionModel !== undefined) sessionModel = result.sessionModel;
|
|
3385
|
+
if (result.lastUserInput !== undefined) lastUserInput = result.lastUserInput;
|
|
3386
|
+
if (result.lastReply !== undefined) lastReply = result.lastReply;
|
|
3461
3387
|
continue;
|
|
3462
3388
|
}
|
|
3463
3389
|
if (input === "/retry") {
|
|
@@ -3632,22 +3558,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3632
3558
|
continue;
|
|
3633
3559
|
}
|
|
3634
3560
|
if (input === "/theme" || input.startsWith("/theme ")) {
|
|
3635
|
-
const
|
|
3636
|
-
const
|
|
3637
|
-
if (
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
continue;
|
|
3642
|
-
}
|
|
3643
|
-
if (!themes.some(t => t.name === want)) {
|
|
3644
|
-
console.log(`Unknown theme '${want}'. Known: ${themes.map(t => t.name).join(", ")}.`);
|
|
3645
|
-
continue;
|
|
3561
|
+
const decision = decideThemeSlash(input, listThemes(), resolveTheme().name);
|
|
3562
|
+
for (const line of decision.lines) console.log(line);
|
|
3563
|
+
if (decision.kind === "set") {
|
|
3564
|
+
process.env.JEO_TUI_THEME = decision.themeName!;
|
|
3565
|
+
await saveConfigPatch(raw => ({ theme: decision.themeName }));
|
|
3566
|
+
refreshUiTheme(); // re-resolve the keystroke-hot theme handle immediately
|
|
3646
3567
|
}
|
|
3647
|
-
process.env.JEO_TUI_THEME = want;
|
|
3648
|
-
await saveConfigPatch(raw => ({ theme: want }));
|
|
3649
|
-
refreshUiTheme(); // re-resolve the keystroke-hot theme handle immediately
|
|
3650
|
-
console.log(`Theme set to ${want} — saved to ~/.jeo/config.json`);
|
|
3651
3568
|
continue;
|
|
3652
3569
|
}
|
|
3653
3570
|
if (input === "/wiki" || input.startsWith("/wiki ")) {
|
|
@@ -3671,12 +3588,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3671
3588
|
continue;
|
|
3672
3589
|
}
|
|
3673
3590
|
if (input === "/evolve") {
|
|
3674
|
-
|
|
3675
|
-
for (const stage of EVOLUTION_STAGES) {
|
|
3676
|
-
console.log(`\nStage: ${stage.name}`);
|
|
3677
|
-
await animateAsciiArt(stage, { delayMs: 40 });
|
|
3678
|
-
}
|
|
3679
|
-
console.log("\n=== Evolved to Singularity! ===");
|
|
3591
|
+
await runEvolveSimulation(animateAsciiArt);
|
|
3680
3592
|
continue;
|
|
3681
3593
|
}
|
|
3682
3594
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
@@ -3947,508 +3859,55 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3947
3859
|
await refreshLiveModelsCache();
|
|
3948
3860
|
continue;
|
|
3949
3861
|
}
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
const cfgNow = await readGlobalConfig();
|
|
3958
|
-
const subcommand = roleArg?.toLowerCase();
|
|
3959
|
-
const printRoster = () => {
|
|
3960
|
-
console.log("Subagent roles (used by 'jeo team'):");
|
|
3961
|
-
for (const line of formatAgentsPanel(allSubagentRoles(cfgNow), r => ({
|
|
3962
|
-
model: resolveSubagentModel(r.id, cfgNow),
|
|
3963
|
-
maxSteps: resolveSubagentMaxSteps(r.id, cfgNow),
|
|
3964
|
-
thinking: resolveSubagentThinking(r.id, cfgNow),
|
|
3965
|
-
}))) console.log(line);
|
|
3966
|
-
console.log("Detail: /agents <role> · set model: /agents <role> <model|#N> · provider: /agents <role> provider <name> [model] · thinking: /agents <role> thinking <level|inherit> · steps: /agents <role> maxSteps <N> · picker: /agents edit");
|
|
3967
|
-
console.log("Tip: primary model flow: /model → pick model → choose default or subagent role → choose thinking level");
|
|
3968
|
-
console.log(`Available: ${allSubagentRoles(cfgNow).map(r => r.id).join(", ")} (declare custom roles in config.subagents)`);
|
|
3969
|
-
console.log("Subcommands: edit, <role> <model|#N>, <role> thinking <level|inherit>, <role> provider <name> [model], <role> maxSteps <N>, <role> reset");
|
|
3970
|
-
};
|
|
3971
|
-
if (!roleArg || roleArg === "/" || roleArg === "?" || subcommand === "help") {
|
|
3972
|
-
printRoster();
|
|
3973
|
-
continue;
|
|
3974
|
-
}
|
|
3975
|
-
if (subcommand === "edit" || subcommand === "picker") {
|
|
3976
|
-
printRoster();
|
|
3977
|
-
// Interactive editor (TTY): role picker → action picker → live model /
|
|
3978
|
-
// thinking / reset — the arrows+Enter way to CHANGE an existing setting.
|
|
3979
|
-
const rolePick = await pickFromOptions(
|
|
3980
|
-
"Edit a subagent role (ESC to skip)",
|
|
3981
|
-
allSubagentRoles(cfgNow).map(r => ({
|
|
3982
|
-
value: r.id,
|
|
3983
|
-
label: `${r.id} — ${r.title}`,
|
|
3984
|
-
hint: `${resolveSubagentModel(r.id, cfgNow)} · ${resolveSubagentMaxSteps(r.id, cfgNow)} steps${cfgNow.subagents?.[r.id]?.model ? "" : " (default)"}`,
|
|
3985
|
-
})),
|
|
3986
|
-
);
|
|
3987
|
-
const editRole = rolePick ? getSubagentRole(rolePick, cfgNow) : undefined;
|
|
3988
|
-
if (!editRole) continue;
|
|
3989
|
-
const action = await pickFromOptions(`${editRole.title} — choose action`, [
|
|
3990
|
-
{ value: "model", label: "change model", hint: resolveSubagentModel(editRole.id, cfgNow) },
|
|
3991
|
-
{ value: "thinking", label: "change thinking", hint: resolveSubagentThinking(editRole.id, cfgNow) ?? `inherit (${cfgNow.thinkingLevel ?? "medium"})` },
|
|
3992
|
-
{ value: "reset", label: "reset to defaults", hint: "clears model + maxSteps + thinking override" },
|
|
3993
|
-
]);
|
|
3994
|
-
if (action === "reset") {
|
|
3995
|
-
await saveConfigPatch(raw => ({ subagents: clearSubagentSetting(raw, editRole.id) }));
|
|
3996
|
-
console.log(`${editRole.title} settings reset to defaults → ~/.jeo/config.json`);
|
|
3997
|
-
} else if (action === "model") {
|
|
3998
|
-
const live = await getLiveModels();
|
|
3999
|
-
const entries = flattenModels(live);
|
|
4000
|
-
const picked = await pickLiveProviderModel(`${editRole.id}`, entries, resolveSubagentModel(editRole.id, cfgNow));
|
|
4001
|
-
if (picked) {
|
|
4002
|
-
const pinned = qualifyModelId(picked.model, picked.provider);
|
|
4003
|
-
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, editRole.id, { model: pinned }) }));
|
|
4004
|
-
console.log(`Subagent '${editRole.id}' model set to ${pinned} → ~/.jeo/config.json`);
|
|
4005
|
-
}
|
|
4006
|
-
} else if (action === "thinking") {
|
|
4007
|
-
const lvl = await pickThinkingLevel(
|
|
4008
|
-
`Reasoning for ${editRole.title}`,
|
|
4009
|
-
cfgNow.subagents?.[editRole.id]?.thinking,
|
|
4010
|
-
`inherit — follow default (${cfgNow.thinkingLevel ?? "medium"})`,
|
|
4011
|
-
);
|
|
4012
|
-
if (lvl) await setRoleThinking(editRole.id, lvl);
|
|
4013
|
-
}
|
|
4014
|
-
continue;
|
|
4015
|
-
}
|
|
4016
|
-
const role = getSubagentRole(roleArg, cfgNow);
|
|
4017
|
-
if (!role) {
|
|
4018
|
-
console.log(`Unknown role '${roleArg}'. Known: ${allSubagentRoles(cfgNow).map(r => r.id).join(", ")}.`);
|
|
4019
|
-
continue;
|
|
4020
|
-
}
|
|
4021
|
-
if (modelArg?.toLowerCase() === "reset") {
|
|
4022
|
-
await saveConfigPatch(raw => ({ subagents: clearSubagentSetting(raw, role.id) }));
|
|
4023
|
-
console.log(`${role.title} settings reset to defaults → ~/.jeo/config.json`);
|
|
4024
|
-
continue;
|
|
4025
|
-
}
|
|
4026
|
-
if (modelArg?.toLowerCase() === "maxsteps" || modelArg?.toLowerCase() === "steps") {
|
|
4027
|
-
const maxSteps = parseMaxSteps(tokens[2]);
|
|
4028
|
-
if (!maxSteps) {
|
|
4029
|
-
console.log(`Usage: /agents ${role.id} maxSteps <positive-number>`);
|
|
4030
|
-
continue;
|
|
4031
|
-
}
|
|
4032
|
-
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { maxSteps }) }));
|
|
4033
|
-
console.log(`${role.title} maxSteps set to ${maxSteps} → ~/.jeo/config.json`);
|
|
4034
|
-
continue;
|
|
4035
|
-
}
|
|
4036
|
-
if (modelArg?.toLowerCase() === "thinking" || modelArg?.toLowerCase() === "think") {
|
|
4037
|
-
await setRoleThinking(role.id, tokens[2]);
|
|
4038
|
-
continue;
|
|
4039
|
-
}
|
|
4040
|
-
if (modelArg?.toLowerCase() === "provider") {
|
|
4041
|
-
const want = (tokens[2] ?? "").toLowerCase();
|
|
4042
|
-
if (!isProviderName(want)) {
|
|
4043
|
-
console.log(`Usage: /agents ${role.id} provider <name> [model|#N] — e.g. anthropic, openai, gemini, groq, deepseek, openrouter (any configured provider)`);
|
|
4044
|
-
continue;
|
|
4045
|
-
}
|
|
4046
|
-
const st = (await describeAllProviders()).find(s => s.name === want);
|
|
4047
|
-
if (st && !st.ready) {
|
|
4048
|
-
if (selectableThoughNotReady(st)) {
|
|
4049
|
-
console.log(notReadyWarning(st));
|
|
4050
|
-
} else {
|
|
4051
|
-
console.log(`Cannot pin ${role.title} to ${want}: not ready (${st.label}). Set ${st.envVar ?? "the provider key"} first.`);
|
|
4052
|
-
continue;
|
|
4053
|
-
}
|
|
4054
|
-
}
|
|
4055
|
-
const live = await getLiveModels();
|
|
4056
|
-
const forProvider = providerPickEntries(live, want);
|
|
4057
|
-
const liveForProvider = live.some(r => r.ok && r.provider === want && r.models.length > 0);
|
|
4058
|
-
const explicit = tokens[3];
|
|
4059
|
-
let chosenModel: string;
|
|
4060
|
-
if (explicit && forProvider.length) {
|
|
4061
|
-
const sel = resolveSelection(forProvider, explicit);
|
|
4062
|
-
if (sel.kind === "index" || sel.kind === "match") chosenModel = qualifyModelId(sel.entry.model, want);
|
|
4063
|
-
else if (sel.kind === "ambiguous") {
|
|
4064
|
-
console.log(`'${explicit}' matches ${sel.matches.length} ${want} models — be more specific:`);
|
|
4065
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model}`);
|
|
4066
|
-
continue;
|
|
4067
|
-
} else if (sel.kind === "out-of-range") {
|
|
4068
|
-
console.log(`#${explicit.slice(1)} is out of range for ${want} (1-${sel.max}).`);
|
|
4069
|
-
continue;
|
|
4070
|
-
} else {
|
|
4071
|
-
chosenModel = qualifyModelId(explicit, want);
|
|
4072
|
-
}
|
|
4073
|
-
} else if (explicit) {
|
|
4074
|
-
chosenModel = qualifyModelId(explicit, want);
|
|
4075
|
-
} else if (forProvider.length) {
|
|
4076
|
-
// No model given → the provider's first known model, provider-qualified.
|
|
4077
|
-
chosenModel = qualifyModelId(forProvider[0]!.model, want);
|
|
4078
|
-
} else {
|
|
4079
|
-
chosenModel = providerDefaultModel(want);
|
|
4080
|
-
}
|
|
4081
|
-
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
4082
|
-
console.log(`${role.title} pinned to ${want} via model ${chosenModel} — saved to ~/.jeo/config.json`);
|
|
4083
|
-
if (forProvider.length) {
|
|
4084
|
-
lastPickIndex = forProvider;
|
|
4085
|
-
const sourceNote = liveForProvider ? "Live" : "Catalog";
|
|
4086
|
-
const tail = liveForProvider ? "" : " (log in to list live models)";
|
|
4087
|
-
console.log(`${sourceNote} ${want} models — refine with /agents ${role.id} #N:${tail}`);
|
|
4088
|
-
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: chosenModel, cap: 12 })) console.log(line);
|
|
4089
|
-
}
|
|
4090
|
-
continue;
|
|
4091
|
-
}
|
|
4092
|
-
if (modelArg) {
|
|
4093
|
-
let chosenModel = modelArg;
|
|
4094
|
-
let entries = lastPickIndex;
|
|
4095
|
-
if (modelArg.startsWith("#") && entries.length === 0) {
|
|
4096
|
-
const live = await getLiveModels();
|
|
4097
|
-
entries = flattenModels(live);
|
|
4098
|
-
}
|
|
4099
|
-
if (entries.length) {
|
|
4100
|
-
const sel = resolveSelection(entries, modelArg);
|
|
4101
|
-
if (sel.kind === "index" || sel.kind === "match") {
|
|
4102
|
-
chosenModel = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
4103
|
-
const bad = (await describeAllProviders()).find(s => s.name === sel.entry.provider && !s.ready);
|
|
4104
|
-
if (bad) {
|
|
4105
|
-
if (selectableThoughNotReady(bad)) {
|
|
4106
|
-
console.log(notReadyWarning(bad));
|
|
4107
|
-
} else {
|
|
4108
|
-
console.log(`Cannot pin ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad.label}). Set ${bad.envVar ?? "the provider key"} first.`);
|
|
4109
|
-
continue;
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
} else if (sel.kind === "ambiguous") {
|
|
4113
|
-
console.log(`'${modelArg}' matches ${sel.matches.length} live models — be more specific:`);
|
|
4114
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4115
|
-
continue;
|
|
4116
|
-
} else if (sel.kind === "out-of-range") {
|
|
4117
|
-
console.log(`#${modelArg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4118
|
-
continue;
|
|
4119
|
-
}
|
|
4120
|
-
} else if (modelArg.startsWith("#")) {
|
|
4121
|
-
console.log("Use /model first to build the numbered live model list.");
|
|
4122
|
-
continue;
|
|
4123
|
-
}
|
|
4124
|
-
// Persist a per-role model override to ~/.jeo/config.json (consumed by 'jeo team').
|
|
4125
|
-
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
4126
|
-
const { provider } = await describeModel(chosenModel);
|
|
4127
|
-
console.log(`${role.title} model set to ${chosenModel} (${provider}) — saved to ~/.jeo/config.json`);
|
|
4128
|
-
const live = await getLiveModels();
|
|
4129
|
-
if (!liveModelKnown(live, chosenModel)) {
|
|
4130
|
-
console.log(` (note: '${chosenModel}' is not in any live model list — verify it is valid for ${provider})`);
|
|
4131
|
-
}
|
|
4132
|
-
continue;
|
|
4133
|
-
}
|
|
4134
|
-
for (const line of formatAgentDetail(role, {
|
|
4135
|
-
model: resolveSubagentModel(role.id, cfgNow),
|
|
4136
|
-
maxSteps: resolveSubagentMaxSteps(role.id, cfgNow),
|
|
4137
|
-
thinking: resolveSubagentThinking(role.id, cfgNow),
|
|
4138
|
-
})) console.log(line);
|
|
4139
|
-
const live = await getLiveModels();
|
|
4140
|
-
const agentPick = flattenModels(live);
|
|
4141
|
-
if (agentPick.length) {
|
|
4142
|
-
lastPickIndex = agentPick;
|
|
4143
|
-
console.log(`Live models for ${role.title} — pin with /agents ${role.id} #N:`);
|
|
4144
|
-
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: resolveSubagentModel(role.id, cfgNow), cap: 20 })) console.log(line);
|
|
4145
|
-
}
|
|
3862
|
+
if (input === "/agents" || input.startsWith("/agents ")) {
|
|
3863
|
+
const agentsResult = await runAgentsSlash(input, {
|
|
3864
|
+
sessionModel, sessionThinking, lastPickIndex,
|
|
3865
|
+
getLiveModels, pickLiveProviderModel, setRoleThinking, pickFromOptions, pickThinkingLevel,
|
|
3866
|
+
});
|
|
3867
|
+
if (agentsResult.sessionThinking !== undefined) sessionThinking = agentsResult.sessionThinking;
|
|
3868
|
+
if (agentsResult.lastPickIndex !== undefined) lastPickIndex = agentsResult.lastPickIndex;
|
|
4146
3869
|
continue;
|
|
4147
3870
|
}
|
|
4148
3871
|
if (input === "/config") {
|
|
4149
|
-
const
|
|
4150
|
-
const label = sessionModel || cfgNow.defaultModel;
|
|
4151
|
-
const { resolved, provider } = await describeModel(label);
|
|
4152
|
-
console.log("Effective runtime config:");
|
|
4153
|
-
for (const line of formatConfigPanel({
|
|
4154
|
-
model: label,
|
|
4155
|
-
resolved,
|
|
4156
|
-
provider,
|
|
4157
|
-
thinkingLevel: sessionThinking ?? cfgNow.thinkingLevel ?? "medium",
|
|
4158
|
-
ollamaBaseUrl: cfgNow.ollamaBaseUrl,
|
|
4159
|
-
openaiBaseUrl: cfgNow.openaiBaseUrl,
|
|
4160
|
-
requestMaxRetries: cfgNow.retry?.requestMaxRetries,
|
|
4161
|
-
sessionId,
|
|
4162
|
-
})) console.log(line);
|
|
3872
|
+
for (const line of await buildConfigPanelLines({ sessionModel, sessionThinking, sessionId })) console.log(line);
|
|
4163
3873
|
continue;
|
|
4164
3874
|
}
|
|
4165
3875
|
if (input.startsWith("/roles") && (input === "/roles" || input[6] === " ")) {
|
|
4166
|
-
const
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
let entries = lastPickIndex;
|
|
4173
|
-
if (chosenModel.startsWith("#") && entries.length === 0) {
|
|
4174
|
-
const live = await getLiveModels();
|
|
4175
|
-
entries = flattenModels(live);
|
|
4176
|
-
}
|
|
4177
|
-
if (entries.length) {
|
|
4178
|
-
const sel = resolveSelection(entries, chosenModel);
|
|
4179
|
-
if (sel.kind === "index" || sel.kind === "match") {
|
|
4180
|
-
chosenModel = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
4181
|
-
const bad = (await describeAllProviders()).find(s => s.name === sel.entry.provider && !s.ready);
|
|
4182
|
-
if (bad) {
|
|
4183
|
-
console.log(`Cannot set role ${tier} to ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad.label}). Set ${bad.envVar ?? "the provider key"} first.`);
|
|
4184
|
-
continue;
|
|
4185
|
-
}
|
|
4186
|
-
} else if (sel.kind === "ambiguous") {
|
|
4187
|
-
console.log(`'${chosenModel}' matches ${sel.matches.length} live models — be more specific:`);
|
|
4188
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4189
|
-
continue;
|
|
4190
|
-
} else if (sel.kind === "out-of-range") {
|
|
4191
|
-
console.log(`#${chosenModel.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4192
|
-
continue;
|
|
4193
|
-
}
|
|
4194
|
-
} else if (chosenModel.startsWith("#")) {
|
|
4195
|
-
console.log("Use /model first to build the numbered live model list.");
|
|
4196
|
-
continue;
|
|
4197
|
-
}
|
|
4198
|
-
await saveConfigPatch(raw => ({ roles: { ...(raw.roles ?? {}), [tier]: chosenModel } }));
|
|
4199
|
-
console.log(`Role '${tier}' model set to ${chosenModel} → ~/.jeo/config.json`);
|
|
4200
|
-
continue;
|
|
4201
|
-
}
|
|
4202
|
-
console.log("Model role tiers (fall back to the default model):");
|
|
4203
|
-
for (const tier of TIERS) {
|
|
4204
|
-
const { provider } = await describeModel(resolveRoleModel(tier, cfgNow));
|
|
4205
|
-
console.log(` ${tier.padEnd(5)} ${resolveRoleModel(tier, cfgNow)} (${provider})`);
|
|
4206
|
-
}
|
|
4207
|
-
console.log("Set a tier: /roles <smol|slow|plan> <model>");
|
|
4208
|
-
const live = await getLiveModels();
|
|
4209
|
-
const rolePick = flattenModels(live);
|
|
4210
|
-
if (rolePick.length) {
|
|
4211
|
-
lastPickIndex = rolePick;
|
|
4212
|
-
console.log("Live models for role tiers — set with /roles <tier> #N:");
|
|
4213
|
-
for (const line of formatPickListWithCapabilities(lastPickIndex, { cap: 15 })) console.log(line);
|
|
4214
|
-
}
|
|
3876
|
+
const rolesResult = await runRolesSlash(input, {
|
|
3877
|
+
sessionModel, sessionThinking, lastPickIndex,
|
|
3878
|
+
getLiveModels, pickLiveProviderModel, setRoleThinking, pickFromOptions, pickThinkingLevel,
|
|
3879
|
+
});
|
|
3880
|
+
if (rolesResult.sessionThinking !== undefined) sessionThinking = rolesResult.sessionThinking;
|
|
3881
|
+
if (rolesResult.lastPickIndex !== undefined) lastPickIndex = rolesResult.lastPickIndex;
|
|
4215
3882
|
continue;
|
|
4216
3883
|
}
|
|
4217
3884
|
if (input.startsWith("/fast") && (input === "/fast" || input[5] === " ")) {
|
|
4218
|
-
const
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
const status = fastLevel && currentThinking === fastLevel ? "on" : "off";
|
|
4225
|
-
if (arg === "status") {
|
|
4226
|
-
const support = fastLevel ? `supported (thinking ${fastLevel})` : "unsupported";
|
|
4227
|
-
console.log(`Fast mode: ${status} · ${support} · ${formatModelLine({ label: currentModel, resolved, provider })} · current thinking ${currentThinking}`);
|
|
4228
|
-
continue;
|
|
4229
|
-
}
|
|
4230
|
-
if (arg === "on") {
|
|
4231
|
-
if (!fastLevel) {
|
|
4232
|
-
console.log(`Fast mode is not advertised for ${formatModelLine({ label: currentModel, resolved, provider })}; pick a thinking-capable model with /model.`);
|
|
4233
|
-
continue;
|
|
4234
|
-
}
|
|
4235
|
-
sessionThinking = fastLevel;
|
|
4236
|
-
console.log(`Fast mode on: ${formatModelLine({ label: currentModel, resolved, provider })} · thinking ${fastLevel} (~${thinkingMaxTokens(fastLevel)} max tokens/step)`);
|
|
4237
|
-
continue;
|
|
4238
|
-
}
|
|
4239
|
-
if (arg === "off") {
|
|
4240
|
-
sessionThinking = cfgNow.thinkingLevel ?? "medium";
|
|
4241
|
-
console.log(`Fast mode off: restored thinking ${sessionThinking} (~${thinkingMaxTokens(sessionThinking)} max tokens/step)`);
|
|
4242
|
-
continue;
|
|
4243
|
-
}
|
|
4244
|
-
console.log("Usage: /fast [on|off|status]");
|
|
3885
|
+
const fastResult = await runFastSlash(input, {
|
|
3886
|
+
sessionModel, sessionThinking, lastPickIndex,
|
|
3887
|
+
getLiveModels, pickLiveProviderModel, setRoleThinking, pickFromOptions, pickThinkingLevel,
|
|
3888
|
+
});
|
|
3889
|
+
if (fastResult.sessionThinking !== undefined) sessionThinking = fastResult.sessionThinking;
|
|
3890
|
+
if (fastResult.lastPickIndex !== undefined) lastPickIndex = fastResult.lastPickIndex;
|
|
4245
3891
|
continue;
|
|
4246
3892
|
}
|
|
4247
3893
|
if (input.startsWith("/thinking") && (input === "/thinking" || input[9] === " ")) {
|
|
4248
|
-
const
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
if (
|
|
4254
|
-
sessionThinking = arg;
|
|
4255
|
-
console.log(`Thinking set to ${arg} (~${thinkingMaxTokens(arg)} max tokens/step)`);
|
|
4256
|
-
} else {
|
|
4257
|
-
console.log(`Invalid level '${arg}'. Use: minimal | low | medium | high | xhigh.`);
|
|
4258
|
-
}
|
|
3894
|
+
const thinkingResult = await runThinkingSlash(input, {
|
|
3895
|
+
sessionModel, sessionThinking, lastPickIndex,
|
|
3896
|
+
getLiveModels, pickLiveProviderModel, setRoleThinking, pickFromOptions, pickThinkingLevel,
|
|
3897
|
+
});
|
|
3898
|
+
if (thinkingResult.sessionThinking !== undefined) sessionThinking = thinkingResult.sessionThinking;
|
|
3899
|
+
if (thinkingResult.lastPickIndex !== undefined) lastPickIndex = thinkingResult.lastPickIndex;
|
|
4259
3900
|
continue;
|
|
4260
3901
|
}
|
|
4261
3902
|
if (input.startsWith("/model") && (input === "/model" || input[6] === " ")) {
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
if (sel.kind === "index" || sel.kind === "match") toSave = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
4271
|
-
else if (sel.kind === "ambiguous") {
|
|
4272
|
-
console.log(`'${toSave}' matches ${sel.matches.length} models — be more specific:`);
|
|
4273
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4274
|
-
continue;
|
|
4275
|
-
} else if (sel.kind === "out-of-range") {
|
|
4276
|
-
console.log(`#${toSave.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4277
|
-
continue;
|
|
4278
|
-
}
|
|
4279
|
-
// kind "none" → treat `toSave` as a literal model id/alias.
|
|
4280
|
-
} else if (toSave.startsWith("#")) {
|
|
4281
|
-
console.log("Use /model first to build the numbered list.");
|
|
4282
|
-
continue;
|
|
4283
|
-
}
|
|
4284
|
-
// Fall back to the FRESH on-disk default (not the stale session-start snapshot) so a
|
|
4285
|
-
// bare `/model save` after a prior `/model save <id>` never reverts the saved default.
|
|
4286
|
-
const finalSave = toSave || sessionModel || (await readGlobalConfig()).defaultModel;
|
|
4287
|
-
await saveConfigPatch(raw => rememberModelPatch(raw, finalSave));
|
|
4288
|
-
const { resolved, provider } = await describeModel(finalSave);
|
|
4289
|
-
console.log(`Default model saved: ${formatModelLine({ label: finalSave, resolved, provider })} → ~/.jeo/config.json`);
|
|
4290
|
-
continue;
|
|
4291
|
-
}
|
|
4292
|
-
const modelThinking = /^(?:thinking|think)(?:\s+(\S+))?$/i.exec(arg);
|
|
4293
|
-
if (modelThinking) {
|
|
4294
|
-
const level = (modelThinking[1] ?? "").toLowerCase();
|
|
4295
|
-
if (!isThinkingLevel(level)) {
|
|
4296
|
-
console.log("Usage: /model thinking <minimal|low|medium|high|xhigh>");
|
|
4297
|
-
continue;
|
|
4298
|
-
}
|
|
4299
|
-
sessionThinking = level;
|
|
4300
|
-
await saveConfigPatch(() => ({ thinkingLevel: level }));
|
|
4301
|
-
console.log(`Default thinking set to ${level} (~${thinkingMaxTokens(level)} max tokens/step) → ~/.jeo/config.json`);
|
|
4302
|
-
continue;
|
|
4303
|
-
}
|
|
4304
|
-
const statuses = await describeAllProviders();
|
|
4305
|
-
const disabledModelProviders = statuses.filter(s => !s.ready && !selectableThoughNotReady(s)).map(s => s.name);
|
|
4306
|
-
const roleMatch = /^(subagent|role)\s+(\S+)(?:\s+(.+))?$/i.exec(arg);
|
|
4307
|
-
if (roleMatch) {
|
|
4308
|
-
const role = getSubagentRole(roleMatch[2] ?? "", await readGlobalConfig());
|
|
4309
|
-
if (!role) {
|
|
4310
|
-
console.log("Usage: /model subagent <executor|planner|architect|critic> [model|#N]");
|
|
4311
|
-
continue;
|
|
4312
|
-
}
|
|
4313
|
-
let roleModelArg = (roleMatch[3] ?? "").trim();
|
|
4314
|
-
const roleThinking = /^(?:thinking|think)(?:\s+(\S+))?$/i.exec(roleModelArg);
|
|
4315
|
-
if (roleThinking) {
|
|
4316
|
-
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.`);
|
|
4317
|
-
continue;
|
|
4318
|
-
}
|
|
4319
|
-
|
|
4320
|
-
if (!roleModelArg && process.stdin.isTTY && process.stdout.isTTY) {
|
|
4321
|
-
const live = await getLiveModels();
|
|
4322
|
-
lastPickIndex = flattenModels(live);
|
|
4323
|
-
if (lastPickIndex.length) {
|
|
4324
|
-
const currentResolved = (await describeModel(resolveSubagentModel(role.id, await readGlobalConfig()))).resolved;
|
|
4325
|
-
const picked = await pickLiveProviderModel(role.id, lastPickIndex, currentResolved, disabledModelProviders);
|
|
4326
|
-
if (!picked) {
|
|
4327
|
-
console.log("(cancelled)");
|
|
4328
|
-
continue;
|
|
4329
|
-
}
|
|
4330
|
-
roleModelArg = qualifyModelId(picked.model, picked.provider);
|
|
4331
|
-
|
|
4332
|
-
}
|
|
4333
|
-
}
|
|
4334
|
-
if (roleModelArg && lastPickIndex.length) {
|
|
4335
|
-
const sel = resolveSelection(lastPickIndex, roleModelArg);
|
|
4336
|
-
if (sel.kind === "index" || sel.kind === "match") {
|
|
4337
|
-
if (disabledModelProviders.includes(sel.entry.provider)) {
|
|
4338
|
-
const bad = statuses.find(s => s.name === sel.entry.provider);
|
|
4339
|
-
console.log(`Cannot select ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad?.label ?? "not ready"}). Set ${bad?.envVar ?? "the provider key"} first.`);
|
|
4340
|
-
continue;
|
|
4341
|
-
}
|
|
4342
|
-
roleModelArg = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
4343
|
-
|
|
4344
|
-
} else if (sel.kind === "ambiguous") {
|
|
4345
|
-
console.log(`'${roleModelArg}' matches ${sel.matches.length} models — be more specific:`);
|
|
4346
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4347
|
-
continue;
|
|
4348
|
-
} else if (sel.kind === "out-of-range") {
|
|
4349
|
-
console.log(`#${roleModelArg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4350
|
-
continue;
|
|
4351
|
-
}
|
|
4352
|
-
} else if (roleModelArg.startsWith("#")) {
|
|
4353
|
-
console.log("Use /model first to build the numbered list.");
|
|
4354
|
-
continue;
|
|
4355
|
-
}
|
|
4356
|
-
if (roleModelArg) {
|
|
4357
|
-
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: roleModelArg }) }));
|
|
4358
|
-
const { provider } = await describeModel(roleModelArg);
|
|
4359
|
-
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).`);
|
|
4360
|
-
} else {
|
|
4361
|
-
const current = resolveSubagentModel(role.id, await readGlobalConfig());
|
|
4362
|
-
const { resolved, provider } = await describeModel(current);
|
|
4363
|
-
console.log(`${role.title} model: ${formatModelLine({ label: current, resolved, provider })}`);
|
|
4364
|
-
const live = await getLiveModels();
|
|
4365
|
-
lastPickIndex = flattenModels(live);
|
|
4366
|
-
if (lastPickIndex.length) {
|
|
4367
|
-
console.log(`Live models for ${role.title} — set with /model subagent ${role.id} #N:`);
|
|
4368
|
-
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: resolved, cap: 20 })) console.log(line);
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
continue;
|
|
4372
|
-
}
|
|
4373
|
-
let modelPickedFromSelector = false;
|
|
4374
|
-
if (!arg && process.stdin.isTTY && process.stdout.isTTY) {
|
|
4375
|
-
const live = await getLiveModels();
|
|
4376
|
-
lastPickIndex = flattenModels(live);
|
|
4377
|
-
if (lastPickIndex.length) {
|
|
4378
|
-
const currentResolved = (await describeModel(sessionModel || defaultModel)).resolved;
|
|
4379
|
-
const picked = await pickLiveProviderModel("live", lastPickIndex, currentResolved, disabledModelProviders);
|
|
4380
|
-
if (!picked) {
|
|
4381
|
-
console.log("(cancelled)");
|
|
4382
|
-
continue;
|
|
4383
|
-
}
|
|
4384
|
-
arg = qualifyModelId(picked.model, picked.provider);
|
|
4385
|
-
modelPickedFromSelector = true;
|
|
4386
|
-
}
|
|
4387
|
-
}
|
|
4388
|
-
// Selection from the last numbered pick list (`#N`) or a fuzzy substring.
|
|
4389
|
-
if (arg && lastPickIndex.length) {
|
|
4390
|
-
const sel = resolveSelection(lastPickIndex, arg);
|
|
4391
|
-
if (sel.kind === "index" || sel.kind === "match") {
|
|
4392
|
-
if (disabledModelProviders.includes(sel.entry.provider)) {
|
|
4393
|
-
const bad = statuses.find(s => s.name === sel.entry.provider);
|
|
4394
|
-
console.log(`Cannot select ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad?.label ?? "not ready"}). Set ${bad?.envVar ?? "the provider key"} first.`);
|
|
4395
|
-
continue;
|
|
4396
|
-
}
|
|
4397
|
-
arg = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
4398
|
-
modelPickedFromSelector = true;
|
|
4399
|
-
} else if (sel.kind === "ambiguous") {
|
|
4400
|
-
console.log(`'${arg}' matches ${sel.matches.length} models — be more specific:`);
|
|
4401
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4402
|
-
continue;
|
|
4403
|
-
} else if (sel.kind === "out-of-range") {
|
|
4404
|
-
console.log(`#${arg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4405
|
-
continue;
|
|
4406
|
-
}
|
|
4407
|
-
// kind "none" → fall through and treat `arg` as a literal model id/alias.
|
|
4408
|
-
} else if (arg.startsWith("#")) {
|
|
4409
|
-
console.log("Use /model first to build the numbered list.");
|
|
4410
|
-
continue;
|
|
4411
|
-
}
|
|
4412
|
-
const label = arg || (sessionModel || defaultModel);
|
|
4413
|
-
if (arg && modelPickedFromSelector && await applyPickedModelWithTarget(arg)) {
|
|
4414
|
-
continue;
|
|
4415
|
-
}
|
|
4416
|
-
if (arg) {
|
|
4417
|
-
sessionModel = arg;
|
|
4418
|
-
// MRU persistence: picking a model IS saving it — the newest pick wins
|
|
4419
|
-
// as the global default; recents keep the rotation for every session.
|
|
4420
|
-
await saveConfigPatch(raw => rememberModelPatch(raw, arg));
|
|
4421
|
-
await persistSessionModel();
|
|
4422
|
-
}
|
|
4423
|
-
const { resolved, provider } = await describeModel(label);
|
|
4424
|
-
const st = statuses.find(s => s.name === provider);
|
|
4425
|
-
console.log(`${arg ? "Model set to" : "Current model"}: ${formatModelLine({ label, resolved, provider, ready: st?.ready })}${arg ? " — saved as default" : ""}`);
|
|
4426
|
-
if (st && !st.ready) console.log(` ! ${provider} is not ready (${st.label}) — set ${st.envVar ?? "the provider key"} or run 'jeo setup'.`);
|
|
4427
|
-
// ChatGPT OAuth only serves the Codex models; warn before the turn fails if the user
|
|
4428
|
-
// pins a non-Codex id with no local base URL to fall back to (gjc-parity readiness guard).
|
|
4429
|
-
if (arg && provider === "openai" && st?.kind === "oauth" && !CODEX_MODELS.includes(resolved)) {
|
|
4430
|
-
const hasLocalBase = !!((await readGlobalConfig()).openaiBaseUrl || process.env.OPENAI_BASE_URL);
|
|
4431
|
-
if (!hasLocalBase) {
|
|
4432
|
-
console.log(` ! ChatGPT OAuth serves only Codex models (${CODEX_MODELS.join(", ")}); '${resolved}' will be rejected at runtime — pick one of those, or set OPENAI_API_KEY / OPENAI_BASE_URL.`);
|
|
4433
|
-
}
|
|
4434
|
-
}
|
|
4435
|
-
if (arg && liveModelsCache && resolved === label && !liveModelKnown(liveModelsCache, resolved)) {
|
|
4436
|
-
console.log(` (note: '${resolved}' is not in the live ${provider} catalog — use /model to pick a valid id)`);
|
|
4437
|
-
}
|
|
4438
|
-
const meta = catalogMetadata(resolved);
|
|
4439
|
-
if (meta) console.log(` ${formatCapabilityLine(meta)}`);
|
|
4440
|
-
if (!arg) {
|
|
4441
|
-
const recents = recentModelsForDisplay(await readGlobalConfig());
|
|
4442
|
-
if (recents.length > 1) {
|
|
4443
|
-
console.log("Recent models (newest first):");
|
|
4444
|
-
recents.slice(0, 5).forEach((m, i) => console.log(` ${i + 1}. ${m}${i === 0 ? " ◀ default" : ""}`));
|
|
4445
|
-
}
|
|
4446
|
-
const live = await getLiveModels();
|
|
4447
|
-
lastPickIndex = flattenModels(live);
|
|
4448
|
-
console.log("Live models (logged-in providers) — set with /model #N:");
|
|
4449
|
-
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: resolved, cap: 20 })) console.log(line);
|
|
4450
|
-
}
|
|
4451
|
-
console.log(" (model picks persist automatically — newest selection is the default everywhere)");
|
|
3903
|
+
const modelResult = await runModelSlash(input, {
|
|
3904
|
+
sessionModel, sessionThinking, defaultModel, lastPickIndex, liveModelsCache,
|
|
3905
|
+
isTTY: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
3906
|
+
getLiveModels, applyPickedModelWithTarget, persistSessionModel, pickLiveProviderModel,
|
|
3907
|
+
});
|
|
3908
|
+
if (modelResult.sessionModel !== undefined) sessionModel = modelResult.sessionModel;
|
|
3909
|
+
if (modelResult.sessionThinking !== undefined) sessionThinking = modelResult.sessionThinking;
|
|
3910
|
+
if (modelResult.lastPickIndex !== undefined) lastPickIndex = modelResult.lastPickIndex;
|
|
4452
3911
|
continue;
|
|
4453
3912
|
}
|
|
4454
3913
|
if (input.startsWith("/view") && (input === "/view" || input[5] === " ")) {
|