jeo-code 0.9.9 → 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 +26 -0
- package/README.ja.md +5 -3
- package/README.ko.md +5 -3
- package/README.md +5 -3
- package/README.zh.md +5 -3
- 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/commands/update.ts +61 -32
- 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,
|
package/src/commands/update.ts
CHANGED
|
@@ -43,6 +43,36 @@ export interface UpdateDeps {
|
|
|
43
43
|
activeVersion?: () => string | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Ordered install attempts for `target` (`jeo-code@<version>`).
|
|
48
|
+
*
|
|
49
|
+
* `bun install -g` alone is not enough in practice: right after a publish bun can
|
|
50
|
+
* still hold a stale registry manifest and fail with `No version matching "<v>"
|
|
51
|
+
* found ... (but package exists)`. `--force` re-resolves against the registry and
|
|
52
|
+
* is what actually recovers that state. npm remains the last resort for
|
|
53
|
+
* npm-installed globals. Pure so the policy is unit-testable without spawning.
|
|
54
|
+
*/
|
|
55
|
+
export function installCandidates(target: string): string[][] {
|
|
56
|
+
return [
|
|
57
|
+
["bun", "install", "-g", target],
|
|
58
|
+
// Cache-buster: bun's resolver held a stale manifest for a just-published version.
|
|
59
|
+
["bun", "install", "-g", target, "--force"],
|
|
60
|
+
["npm", "install", "-g", target],
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Version of the `jeo` binary PATH actually resolves, or null when unreadable. */
|
|
65
|
+
function readActiveVersion(): string | null {
|
|
66
|
+
try {
|
|
67
|
+
const proc = Bun.spawnSync(["jeo", "--version"], { stdout: "pipe", stderr: "pipe" });
|
|
68
|
+
if (!proc.success) return null;
|
|
69
|
+
const out = new TextDecoder().decode(proc.stdout);
|
|
70
|
+
const m = out.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
71
|
+
return m ? m[1] : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
46
76
|
export const defaultDeps: UpdateDeps = {
|
|
47
77
|
fetchJson: async (url: string, options?: { signal?: AbortSignal }) => {
|
|
48
78
|
const res = await fetch(url, options);
|
|
@@ -65,17 +95,20 @@ export const defaultDeps: UpdateDeps = {
|
|
|
65
95
|
// Self-update the global install via the SAME toolchain it was installed with. jeo
|
|
66
96
|
// ships as a `#!/usr/bin/env bun` script so bun is the common case, but npm-installed
|
|
67
97
|
// globals exist too — try bun first, fall back to npm (and tolerate either missing).
|
|
68
|
-
// `@<version>` (the resolved latest) forces the newest publish past a stale global cache.
|
|
69
98
|
const target = `jeo-code@${version ?? "latest"}`;
|
|
70
|
-
const managers: string[][] = [
|
|
71
|
-
["bun", "install", "-g", target],
|
|
72
|
-
["npm", "install", "-g", target],
|
|
73
|
-
];
|
|
74
99
|
let lastErr = "";
|
|
75
|
-
for (const cmd of
|
|
100
|
+
for (const cmd of installCandidates(target)) {
|
|
76
101
|
try {
|
|
77
102
|
const proc = Bun.spawnSync(cmd, { stdout: "inherit", stderr: "inherit" });
|
|
78
|
-
if (proc.success)
|
|
103
|
+
if (proc.success) {
|
|
104
|
+
// A zero exit is NOT proof the user's `jeo` moved: npm's global prefix is
|
|
105
|
+
// often a different directory than the bun prefix PATH actually resolves,
|
|
106
|
+
// so an npm fallback can "succeed" while the active binary is untouched.
|
|
107
|
+
// Only stop once the binary on PATH really reports the requested version.
|
|
108
|
+
if (!version || readActiveVersion() === version) return { success: true };
|
|
109
|
+
lastErr = `${cmd[0]} reported success but the active 'jeo' is still ${readActiveVersion() ?? "unknown"}`;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
79
112
|
lastErr = `${cmd[0]} exited with code ${proc.exitCode}`;
|
|
80
113
|
} catch (err: any) {
|
|
81
114
|
lastErr = `${cmd[0]} unavailable: ${err?.message ?? String(err)}`;
|
|
@@ -84,20 +117,10 @@ export const defaultDeps: UpdateDeps = {
|
|
|
84
117
|
return { success: false, stderr: lastErr };
|
|
85
118
|
}
|
|
86
119
|
,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const proc = Bun.spawnSync(["jeo", "--version"], { stdout: "pipe", stderr: "pipe" });
|
|
93
|
-
if (!proc.success) return null;
|
|
94
|
-
const out = new TextDecoder().decode(proc.stdout);
|
|
95
|
-
const m = out.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
96
|
-
return m ? m[1] : null;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
120
|
+
// Version of the `jeo` actually on PATH — may differ from this process's bundled
|
|
121
|
+
// version after a self-update (e.g. npm installed into a prefix PATH does not
|
|
122
|
+
// resolve). Detects the silent "installed but PATH unchanged" no-op.
|
|
123
|
+
activeVersion: readActiveVersion
|
|
101
124
|
,
|
|
102
125
|
showWhatsNew: () => {
|
|
103
126
|
try {
|
|
@@ -279,25 +302,31 @@ export async function runUpdateCommandWith(args: string[], deps: UpdateDeps): Pr
|
|
|
279
302
|
try {
|
|
280
303
|
const result = await deps.install(latest ?? undefined);
|
|
281
304
|
if (result.success) {
|
|
305
|
+
// A package manager's zero exit does not prove the user's `jeo` moved: npm's
|
|
306
|
+
// global prefix is frequently a different directory than the one PATH resolves,
|
|
307
|
+
// so the install lands somewhere the user never runs. Verify the ACTIVE binary
|
|
308
|
+
// and treat a still-stale one as a failed update (exit 1) rather than printing
|
|
309
|
+
// a success line the machine contradicts.
|
|
310
|
+
const active = deps.activeVersion?.();
|
|
311
|
+
const stale = !!(active && latest && compareVersions(active, latest) < 0);
|
|
282
312
|
if (hasJson) {
|
|
283
313
|
console.log(JSON.stringify({
|
|
284
314
|
current,
|
|
285
315
|
latest,
|
|
286
316
|
upToDate: false,
|
|
287
|
-
installed:
|
|
317
|
+
installed: !stale,
|
|
318
|
+
...(stale ? { activeVersion: active, error: "active jeo still reports an older version" } : {}),
|
|
288
319
|
}));
|
|
320
|
+
if (stale) process.exitCode = 1;
|
|
321
|
+
} else if (stale) {
|
|
322
|
+
console.error(`[FAILED] Installed jeo-code@${latest}, but the active 'jeo' on PATH still reports ${active}.`);
|
|
323
|
+
console.error(`The install landed in a prefix your PATH does not use. Fix it with one of:`);
|
|
324
|
+
console.error(` bun install -g jeo-code@${latest} --force`);
|
|
325
|
+
console.error(` npm install -g jeo-code@${latest}`);
|
|
326
|
+
console.error(`Then confirm with: jeo --version`);
|
|
327
|
+
process.exitCode = 1;
|
|
289
328
|
} else {
|
|
290
329
|
console.log(`Successfully installed jeo-code@${latest}`);
|
|
291
|
-
// Verify the `jeo` on PATH actually picked up the new install — a bun-vs-npm or
|
|
292
|
-
// PATH mismatch can leave an older binary in front, which looks like "update did
|
|
293
|
-
// nothing". Surface that loudly with the manual fix instead of failing silently.
|
|
294
|
-
const active = deps.activeVersion?.();
|
|
295
|
-
if (active && latest && compareVersions(active, latest) < 0) {
|
|
296
|
-
console.warn(`Warning: installed ${latest}, but the active 'jeo' on PATH still reports ${active}.`);
|
|
297
|
-
console.warn(`Your PATH points at a different install. Fix it with one of:`);
|
|
298
|
-
console.warn(` npm install -g jeo-code@${latest}`);
|
|
299
|
-
console.warn(` bun install -g jeo-code@${latest}`);
|
|
300
|
-
}
|
|
301
330
|
deps.showWhatsNew?.();
|
|
302
331
|
}
|
|
303
332
|
} else {
|
|
@@ -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) {
|