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.
@@ -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 (rid: string): Promise<void> => {
181
+ const applyResume = async (
182
+ rid: string,
183
+ loaded?: Awaited<ReturnType<typeof loadSession>>,
184
+ ): Promise<void> => {
130
185
  try {
131
- const { header, messages } = await loadSession(rid, cwd);
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);
@@ -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
- const loneLfShiftEnter = jeoEnv("MULTILINE") !== "0";
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 in this REPL", group: "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
- if (sections.length > 0) {
13
- const latest = sections[0];
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) {