jeo-code 0.9.10 → 0.9.11
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 +16 -0
- package/README.ja.md +4 -2
- package/README.ko.md +4 -2
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/package.json +2 -1
- package/src/agent/gjc-session-import.ts +1632 -0
- package/src/agent/session.ts +48 -7
- package/src/ai/model-cache.ts +57 -20
- package/src/ai/model-catalog.ts +93 -36
- package/src/ai/model-discovery.ts +31 -4
- package/src/commands/launch/jobs-slash.ts +66 -0
- package/src/commands/launch/session-slash.ts +146 -2
- package/src/commands/launch.ts +17 -1
- package/src/tui/components/slash.ts +3 -1
- package/src/tui/components/welcome.ts +2 -2
|
@@ -13,6 +13,8 @@ import type { Message } from "../../agent/loop";
|
|
|
13
13
|
import { createSession, deleteSession, renameSession, loadSession, listSessions, sessionPath, resolveSessionRef } from "../../agent/session";
|
|
14
14
|
import { SessionPicker, renderSessionPicker } from "../../tui/components/session-picker";
|
|
15
15
|
import { formatTranscript } from "../../tui/components/transcript";
|
|
16
|
+
import { importGjcSession, listGjcSessions, type GjcSessionLeaf } from "../../agent/gjc-session-import";
|
|
17
|
+
import { SelectList, renderSelectList, type SelectItem } from "../../tui/components/select-list";
|
|
16
18
|
|
|
17
19
|
export interface SessionSlashCtx {
|
|
18
20
|
cwd: string;
|
|
@@ -54,6 +56,56 @@ export interface SessionSlashResult {
|
|
|
54
56
|
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
interface GjcResumeRequest {
|
|
60
|
+
sessionId?: string;
|
|
61
|
+
leafId?: string;
|
|
62
|
+
anyCwd: boolean;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseGjcResumeArgument(arg: string): GjcResumeRequest | undefined {
|
|
67
|
+
const tokens = arg.trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
if (!tokens.some(token => token.toLowerCase().startsWith("gajae:"))) return undefined;
|
|
69
|
+
|
|
70
|
+
let refToken: string | undefined;
|
|
71
|
+
let anyCwd = false;
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
if (token === "--any-cwd") {
|
|
74
|
+
anyCwd = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (refToken !== undefined) {
|
|
78
|
+
return { anyCwd, error: `Unexpected argument '${token}'.` };
|
|
79
|
+
}
|
|
80
|
+
refToken = token;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!refToken || !refToken.toLowerCase().startsWith("gajae:")) {
|
|
84
|
+
return { anyCwd, error: "Use /resume gajae:<session-id>[#<leaf-entry-id>] [--any-cwd]." };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const reference = refToken.slice("gajae:".length);
|
|
88
|
+
if (!reference) return { anyCwd };
|
|
89
|
+
const pieces = reference.split("#");
|
|
90
|
+
if (pieces.length > 2 || !pieces[0]) {
|
|
91
|
+
return { anyCwd, error: "Malformed GJC reference. Use gajae:<session-id>[#<leaf-entry-id>]." };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
sessionId: pieces[0],
|
|
96
|
+
...(pieces[1] ? { leafId: pieces[1] } : {}),
|
|
97
|
+
anyCwd,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function gjcResumeItems(pool: GjcSessionLeaf[]): SelectItem<GjcSessionLeaf>[] {
|
|
102
|
+
return pool.map(item => ({
|
|
103
|
+
value: item,
|
|
104
|
+
label: `${item.sessionId}#${item.leafId || "(no-leaf)"}`,
|
|
105
|
+
hint: item.selected ? "selected leaf" : item.cwd,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
/**
|
|
58
110
|
* Handle `/session [new|drop|delete|rename <title>|resume [id]|list|info]`
|
|
59
111
|
* (default: list + current-session summary). Extracted verbatim from
|
|
@@ -126,9 +178,13 @@ export async function runSessionSlash(input: string, ctx: SessionSlashCtx): Prom
|
|
|
126
178
|
}
|
|
127
179
|
if (sub === "resume") {
|
|
128
180
|
const arg = tokens.slice(1).join(" ").trim();
|
|
129
|
-
const applyResume = async (
|
|
181
|
+
const applyResume = async (
|
|
182
|
+
rid: string,
|
|
183
|
+
loaded?: Awaited<ReturnType<typeof loadSession>>,
|
|
184
|
+
): Promise<void> => {
|
|
130
185
|
try {
|
|
131
|
-
const
|
|
186
|
+
const loadedSession = loaded ?? await loadSession(rid, cwd);
|
|
187
|
+
const { header, messages } = loadedSession;
|
|
132
188
|
history.length = 1;
|
|
133
189
|
for (const m of messages) history.push(m);
|
|
134
190
|
sessionId = rid;
|
|
@@ -181,6 +237,94 @@ export async function runSessionSlash(input: string, ctx: SessionSlashCtx): Prom
|
|
|
181
237
|
console.log(`! ${(err as Error).message}`);
|
|
182
238
|
}
|
|
183
239
|
};
|
|
240
|
+
const gjcRequest = parseGjcResumeArgument(arg);
|
|
241
|
+
if (gjcRequest) {
|
|
242
|
+
if (gjcRequest.error) {
|
|
243
|
+
console.log(`! ${gjcRequest.error}`);
|
|
244
|
+
return result();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const importOne = async (source: GjcSessionLeaf | undefined): Promise<void> => {
|
|
248
|
+
if (!source) return;
|
|
249
|
+
try {
|
|
250
|
+
const imported = await importGjcSession({
|
|
251
|
+
sessionId: gjcRequest.sessionId ?? source.sessionId,
|
|
252
|
+
...(gjcRequest.leafId || source.leafId ? { leafId: gjcRequest.leafId ?? source.leafId } : {}),
|
|
253
|
+
cwd,
|
|
254
|
+
anyCwd: gjcRequest.anyCwd,
|
|
255
|
+
});
|
|
256
|
+
console.log(
|
|
257
|
+
`(imported GJC v5 session ${imported.sourceSessionId}#${imported.sourceLeafId} as Jeo session ${imported.sessionId}${imported.reused ? " (reused)" : ""})`,
|
|
258
|
+
);
|
|
259
|
+
await applyResume(imported.sessionId);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.log(`! GJC /resume failed: ${(err as Error).message}`);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (gjcRequest.sessionId) {
|
|
266
|
+
await importOne({
|
|
267
|
+
rootPath: "",
|
|
268
|
+
sourcePath: "",
|
|
269
|
+
sessionId: gjcRequest.sessionId,
|
|
270
|
+
leafId: gjcRequest.leafId ?? "",
|
|
271
|
+
cwd,
|
|
272
|
+
selected: false,
|
|
273
|
+
});
|
|
274
|
+
return result();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let sourcePool: GjcSessionLeaf[];
|
|
278
|
+
try {
|
|
279
|
+
sourcePool = await listGjcSessions({ cwd, anyCwd: gjcRequest.anyCwd });
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.log(`! GJC /resume listing failed: ${(err as Error).message}`);
|
|
282
|
+
return result();
|
|
283
|
+
}
|
|
284
|
+
if (sourcePool.length === 0) {
|
|
285
|
+
console.log("(no GJC v5 sessions for this workspace)");
|
|
286
|
+
console.log("Usage: /resume gajae:<session-id>[#<leaf-entry-id>] [--any-cwd]");
|
|
287
|
+
return result();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
291
|
+
const picker = new SelectList(gjcResumeItems(sourcePool));
|
|
292
|
+
let selected: SelectItem<GjcSessionLeaf> | undefined;
|
|
293
|
+
await runSelectPicker(
|
|
294
|
+
(cols, rows) => renderSelectList(picker, {
|
|
295
|
+
title: "Import a GJC v5 session",
|
|
296
|
+
cols,
|
|
297
|
+
rows: Math.max(8, rows),
|
|
298
|
+
unicode: true,
|
|
299
|
+
color: true,
|
|
300
|
+
}),
|
|
301
|
+
(ch, key) => {
|
|
302
|
+
if (key?.name === "up") { picker.up(); return false; }
|
|
303
|
+
if (key?.name === "down") { picker.down(); return false; }
|
|
304
|
+
if (key?.name === "pageup") { picker.page(-1); return false; }
|
|
305
|
+
if (key?.name === "pagedown") { picker.page(1); return false; }
|
|
306
|
+
if (key?.name === "backspace") { picker.backspace(); return false; }
|
|
307
|
+
if (key?.name === "escape" || (key?.ctrl && key.name === "c")) return true;
|
|
308
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
309
|
+
selected = picker.selected();
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (ch && ch >= " " && !key?.ctrl && !key?.meta) picker.typeChar(ch);
|
|
313
|
+
return false;
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
await importOne(selected?.value);
|
|
317
|
+
return result();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log("GJC v5 sessions — resume with /resume gajae:<session-id>[#<leaf-entry-id>]:");
|
|
321
|
+
for (const source of sourcePool.slice(0, 20)) {
|
|
322
|
+
const selected = source.selected ? " *" : "";
|
|
323
|
+
console.log(` ${source.sessionId}#${source.leafId || "(no-leaf)"}${selected} ${source.cwd}`);
|
|
324
|
+
}
|
|
325
|
+
console.log("Add --any-cwd to include sessions from other workspaces.");
|
|
326
|
+
return result();
|
|
327
|
+
}
|
|
184
328
|
if (arg) {
|
|
185
329
|
// gjc-parity: resolve `arg` as a full id OR a short id prefix before resuming.
|
|
186
330
|
const resolved = await resolveSessionRef(arg, cwd);
|
package/src/commands/launch.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { renderWelcome, playWelcomeSweep } from "../tui/components/welcome";
|
|
|
46
46
|
import { jeoEnv } from "../util/env";
|
|
47
47
|
import { renderUpdateBox } from "../tui/components/update-box";
|
|
48
48
|
import { consumeLaunchWhatsNew } from "../util/whats-new";
|
|
49
|
+
import { runWhatsNewCommand } from "./whats-new";
|
|
49
50
|
import { maybeBell } from "../util/notify";
|
|
50
51
|
import { supportsUnicode } from "../tui/components/capability";
|
|
51
52
|
import pkg from "../../package.json";
|
|
@@ -225,6 +226,7 @@ import {
|
|
|
225
226
|
} from "./launch/code-slash";
|
|
226
227
|
import { handleUndoSlash } from "./launch/git-slash";
|
|
227
228
|
import { runSessionSlash } from "./launch/session-slash";
|
|
229
|
+
import { runJobsSlash } from "./launch/jobs-slash";
|
|
228
230
|
import { runModelSlash } from "./launch/model-slash";
|
|
229
231
|
import { runRouteSlash } from "./launch/route-slash";
|
|
230
232
|
import { runComputerSlash } from "./launch/computer-slash";
|
|
@@ -2416,7 +2418,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2416
2418
|
// protocol nor xterm modifyOtherKeys. JEO_NO_MULTILINE=1 fully disables the
|
|
2417
2419
|
// filter (reads stdin directly).
|
|
2418
2420
|
const multilineInput = !!process.stdin.isTTY && jeoEnv("NO_MULTILINE") !== "1";
|
|
2419
|
-
|
|
2421
|
+
// tmux-owned panes can translate the plain Enter key to LF after the launcher's
|
|
2422
|
+
// keyboard modes are enabled. In that mode LF must remain readline's submit key;
|
|
2423
|
+
// explicit Shift+Enter escape sequences still become MULTILINE_SENTINEL below.
|
|
2424
|
+
const loneLfShiftEnter =
|
|
2425
|
+
jeoEnv("MULTILINE") !== "0" && jeoEnv("TMUX_LAUNCHED") !== "1";
|
|
2420
2426
|
const expandSentinel = (s: string): string => (multilineInput ? s.split(SENTINEL).join("\n") : s);
|
|
2421
2427
|
// Prompt-scoped process listeners (stdin data/keypress, stdout resize). Registered
|
|
2422
2428
|
// once per launch but previously anonymous and never removed — benign for a single
|
|
@@ -4323,6 +4329,16 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4323
4329
|
logLines(handoffLines(res, process.stdout.columns));
|
|
4324
4330
|
continue;
|
|
4325
4331
|
}
|
|
4332
|
+
if (input === "/changelog" || input.startsWith("/changelog ")) {
|
|
4333
|
+
const args = input.slice("/changelog".length).trim().split(/\s+/).filter(Boolean)
|
|
4334
|
+
.map(arg => arg === "--full" ? "--all" : arg);
|
|
4335
|
+
await runWhatsNewCommand(args);
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
if (input === "/jobs" || input.startsWith("/jobs ")) {
|
|
4339
|
+
await runJobsSlash(input, jobRegistry, cwd, lines => logLines(lines));
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4326
4342
|
if (input === "/session" || input.startsWith("/session ")) {
|
|
4327
4343
|
const result = await runSessionSlash(input, {
|
|
4328
4344
|
cwd, history, noSession: flags.noSession, sessionId, sessionModel, rl,
|
|
@@ -27,7 +27,8 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
27
27
|
{ command: "/drop", usage: "/drop", description: "Delete the current session and start a new one", group: "session" },
|
|
28
28
|
{ command: "/session", usage: "/session [info|delete]", description: "Show current session info, or delete it", group: "session" },
|
|
29
29
|
{ command: "/rename", usage: "/rename <title>", description: "Rename the current session", group: "session" },
|
|
30
|
-
{ command: "/resume", usage: "/resume [id]", description: "Resume a saved session
|
|
30
|
+
{ command: "/resume", usage: "/resume [id|gajae:<session-id>[#<leaf>]] [--any-cwd]", description: "Resume a saved session or import a read-only GJC v5 session", group: "session" },
|
|
31
|
+
{ command: "/changelog", usage: "/changelog [--full]", description: "Show recent release notes (use --full for the complete changelog)", group: "system" },
|
|
31
32
|
{ command: "/retry", usage: "/retry", description: "Retry the last request", group: "session" },
|
|
32
33
|
{ command: "/undo", usage: "/undo", description: "Undo the last jeo auto-commit and restore the working tree", group: "session" },
|
|
33
34
|
{ command: "/history", usage: "/history [n|all]", description: "Re-print the worked turn history (prompts, tool steps, replies) into scrollback", group: "session" },
|
|
@@ -53,6 +54,7 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
53
54
|
{ command: "/search", usage: "/search <pat> [glob]", description: "Search the repo for a pattern", group: "code" },
|
|
54
55
|
|
|
55
56
|
{ command: "/sessions", usage: "/sessions", description: "List saved sessions", group: "session" },
|
|
57
|
+
{ command: "/jobs", usage: "/jobs [list|tail|await|cancel]", description: "List, inspect, await, or cancel background jobs in this session", group: "system" },
|
|
56
58
|
{ command: "/usage", usage: "/usage", description: "Show cumulative token usage for this session", group: "system" },
|
|
57
59
|
{ command: "/context", usage: "/context", description: "Show context token usage breakdown", group: "system" },
|
|
58
60
|
{ command: "/tools", usage: "/tools", description: "Show the tools currently visible to the agent", group: "system" },
|
|
@@ -9,8 +9,8 @@ import { parseChangelogSections, changelogText } from "../../util/whats-new";
|
|
|
9
9
|
function getLatestChangelogItems(): { version: string; items: string[] } {
|
|
10
10
|
try {
|
|
11
11
|
const sections = parseChangelogSections(changelogText);
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const latest = sections.find(section => section.groups.some(group => group.items.length > 0));
|
|
13
|
+
if (latest) {
|
|
14
14
|
const items: string[] = [];
|
|
15
15
|
for (const g of latest.groups) {
|
|
16
16
|
for (const item of g.items) {
|