gemiterm 2.2.0-beta.1 → 2.2.0-beta.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemiterm",
3
- "version": "2.2.0-beta.1",
3
+ "version": "2.2.0-beta.2",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,7 +11,7 @@ import { formatChatList } from "../../infrastructure/formatters.ts";
11
11
  import type { ChatInfo } from "../../core/types.ts";
12
12
  import { writeTextFile } from "../../infrastructure/io.ts";
13
13
  import { GemitermError } from "../../core/errors.ts";
14
- import { browser, select, type BrowserAction } from "../utils/prompts.ts";
14
+ import { browser, select, text, type BrowserAction } from "../utils/prompts.ts";
15
15
 
16
16
  interface ListCommandOptions {
17
17
  help: boolean;
@@ -166,6 +166,10 @@ export class ListCommand implements CliCommand {
166
166
  if (actionResult === "quit") return;
167
167
  if (actionResult === "back") continue;
168
168
  await this.executeAction(actionResult, result.chat, context);
169
+ if (actionResult === "delete") {
170
+ const idx = chats.findIndex((c) => c.id === result.chat.id);
171
+ if (idx >= 0) chats.splice(idx, 1);
172
+ }
169
173
  }
170
174
  }
171
175
 
@@ -177,6 +181,11 @@ export class ListCommand implements CliCommand {
177
181
  { value: "export-markdown", label: "Export to Markdown" },
178
182
  { value: "export-json", label: "Export to JSON" },
179
183
  { value: "copy-id", label: "Copy conversation ID" },
184
+ {
185
+ value: "delete",
186
+ label: "Delete conversation",
187
+ description: "No confirmation",
188
+ },
180
189
  { value: "back", label: "Back to list" },
181
190
  { value: "quit", label: "Quit" },
182
191
  ],
@@ -185,10 +194,13 @@ export class ListCommand implements CliCommand {
185
194
  }
186
195
 
187
196
  private async executeAction(
188
- action: "view" | "export-markdown" | "export-json" | "copy-id",
197
+ action: BrowserAction,
189
198
  chat: ChatInfo,
190
199
  context: CliCommandContext,
191
200
  ): Promise<void> {
201
+ if (action === "back" || action === "quit") {
202
+ return;
203
+ }
192
204
  if (action === "copy-id") {
193
205
  console.log(chalk.cyan(`Copied: ${chat.id}`));
194
206
  return;
@@ -200,14 +212,29 @@ export class ListCommand implements CliCommand {
200
212
  const fetch = registry.getHandler("fetch");
201
213
  if (fetch) await fetch.execute([chat.id, "--format", "text"], context);
202
214
  } else if (action === "export-markdown") {
215
+ const outPath = await this.promptExportPath(chat.id, "md");
203
216
  const exportCmd = registry.getHandler("export");
204
- if (exportCmd) await exportCmd.execute([chat.id, "--format", "markdown"], context);
217
+ if (exportCmd) await exportCmd.execute([chat.id, "--format", "markdown", "--out", outPath], context);
205
218
  } else if (action === "export-json") {
219
+ const outPath = await this.promptExportPath(chat.id, "json");
206
220
  const exportCmd = registry.getHandler("export");
207
- if (exportCmd) await exportCmd.execute([chat.id, "--format", "json"], context);
221
+ if (exportCmd) await exportCmd.execute([chat.id, "--format", "json", "--out", outPath], context);
222
+ } else if (action === "delete") {
223
+ const deleteCmd = registry.getHandler("delete");
224
+ if (deleteCmd) await deleteCmd.execute([chat.id, "--force"], context);
208
225
  }
209
226
  }
210
227
 
228
+ private async promptExportPath(chatId: string, ext: "md" | "json"): Promise<string> {
229
+ const date = new Date().toISOString().slice(0, 10);
230
+ const defaultPath = `gemini-chat-${chatId}-${date}.${ext}`;
231
+ const entered = await text({
232
+ message: "Output path:",
233
+ default: defaultPath,
234
+ });
235
+ return entered.trim() === "" ? defaultPath : entered.trim();
236
+ }
237
+
211
238
  private parseArgs(args: string[]): ListCommandOptions {
212
239
  const options = { ...DEFAULT_OPTIONS };
213
240
 
@@ -10,6 +10,7 @@ import {
10
10
  useState,
11
11
  useKeypress,
12
12
  useMemo,
13
+ useEffect,
13
14
  makeTheme,
14
15
  isUpKey,
15
16
  isDownKey,
@@ -156,6 +157,7 @@ export type BrowserAction =
156
157
  | "export-markdown"
157
158
  | "export-json"
158
159
  | "copy-id"
160
+ | "delete"
159
161
  | "back"
160
162
  | "quit";
161
163
 
@@ -166,6 +168,7 @@ export type BrowserResult =
166
168
  export interface BrowserConfig {
167
169
  chats: ReadonlyArray<ChatInfo>;
168
170
  initialSort?: "recent" | "oldest" | "alpha";
171
+ pageSize?: number;
169
172
  }
170
173
 
171
174
  function formatDate(timestamp: number): string {
@@ -187,6 +190,7 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
187
190
  const [profileFilter, setProfileFilter] = useState<"all" | string>("all");
188
191
  const [favoritesOnly, setFavoritesOnly] = useState<boolean>(false);
189
192
  const [active, setActive] = useState<number>(0);
193
+ const [windowStart, setWindowStart] = useState<number>(0);
190
194
 
191
195
  const profileNames = useMemo(() => {
192
196
  const seen = new Set<string>();
@@ -217,11 +221,26 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
217
221
  return sorted;
218
222
  }, [config.chats, sort, profileFilter, favoritesOnly]);
219
223
 
224
+ const pageSize = useMemo(() => {
225
+ if (typeof config.pageSize === "number" && config.pageSize > 0) {
226
+ return config.pageSize;
227
+ }
228
+ const rows = process.stdout.rows ?? 24;
229
+ return Math.max(5, Math.floor((rows - 4) * 0.8));
230
+ }, [config.pageSize]);
231
+
232
+ useEffect(() => {
233
+ setActive(0);
234
+ setWindowStart(0);
235
+ }, [filteredSorted]);
236
+
220
237
  useKeypress((key) => {
221
238
  if (key.name === "s") {
222
239
  const next =
223
240
  sort === "recent" ? "oldest" : sort === "oldest" ? "alpha" : "recent";
224
241
  setSort(next);
242
+ setActive(0);
243
+ setWindowStart(0);
225
244
  return;
226
245
  }
227
246
 
@@ -234,11 +253,15 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
234
253
  const nextIndex = (currentIndex + 1) % cycle.length;
235
254
  setProfileFilter(cycle[nextIndex] ?? "all");
236
255
  }
256
+ setActive(0);
257
+ setWindowStart(0);
237
258
  return;
238
259
  }
239
260
 
240
261
  if (key.name === "f") {
241
262
  setFavoritesOnly(!favoritesOnly);
263
+ setActive(0);
264
+ setWindowStart(0);
242
265
  return;
243
266
  }
244
267
 
@@ -251,6 +274,27 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
251
274
  return;
252
275
  }
253
276
 
277
+ if (key.name === "left") {
278
+ const currentPage = Math.floor(windowStart / pageSize);
279
+ const newPage = Math.max(0, currentPage - 1);
280
+ if (newPage !== currentPage) {
281
+ setWindowStart(newPage * pageSize);
282
+ setActive(newPage * pageSize);
283
+ }
284
+ return;
285
+ }
286
+
287
+ if (key.name === "right") {
288
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
289
+ const currentPage = Math.floor(windowStart / pageSize);
290
+ const newPage = Math.min(totalPages - 1, currentPage + 1);
291
+ if (newPage !== currentPage) {
292
+ setWindowStart(newPage * pageSize);
293
+ setActive(newPage * pageSize);
294
+ }
295
+ return;
296
+ }
297
+
254
298
  if (isEnterKey(key)) {
255
299
  const chat = filteredSorted[active];
256
300
  if (chat) {
@@ -265,21 +309,41 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
265
309
  }
266
310
 
267
311
  if (isUpKey(key)) {
268
- setActive(Math.max(0, active - 1));
312
+ const newActive = Math.max(0, active - 1);
313
+ setActive(newActive);
314
+ if (newActive < windowStart) {
315
+ setWindowStart(newActive);
316
+ }
269
317
  return;
270
318
  }
271
319
 
272
320
  if (isDownKey(key)) {
273
- setActive(Math.min(total - 1, active + 1));
321
+ const newActive = Math.min(total - 1, active + 1);
322
+ setActive(newActive);
323
+ if (newActive >= windowStart + pageSize) {
324
+ setWindowStart(newActive - pageSize + 1);
325
+ }
274
326
  return;
275
327
  }
276
328
  });
277
329
 
330
+ const safeActive = Math.min(active, Math.max(0, filteredSorted.length - 1));
331
+ const totalPages = Math.max(
332
+ 1,
333
+ Math.ceil(filteredSorted.length / pageSize),
334
+ );
335
+ const currentPage = Math.min(
336
+ totalPages,
337
+ Math.floor(safeActive / pageSize) + 1,
338
+ );
339
+ const pageIndicator =
340
+ totalPages > 1 ? ` | Page: ${currentPage}/${totalPages}` : "";
341
+
278
342
  const titleBar = chalk.bold(
279
- `Browse conversations (${filteredSorted.length} chats | Sort: ${sort} | Profile: ${profileFilter} | Favorites: ${favoritesOnly ? "on" : "off"})`,
343
+ `Browse conversations (${filteredSorted.length} chats${pageIndicator} | Sort: ${sort} | Profile: ${profileFilter} | Favorites: ${favoritesOnly ? "on" : "off"})`,
280
344
  );
281
345
  const hintLine = chalk.dim(
282
- "↑↓ navigate · s sort · p profile · f favorites · enter pick · q quit",
346
+ "↑↓ navigate · ← → page · s sort · p profile · f favorites · enter pick · q quit",
283
347
  );
284
348
 
285
349
  const renderRow = (item: ChatInfo, isActive: boolean): string => {
@@ -291,15 +355,19 @@ export const browserPrompt = createPrompt<BrowserResult, BrowserConfig>(
291
355
  return `${cursor}${id} ${date} ${title} ${pin}`;
292
356
  };
293
357
 
294
- const safeActive = Math.min(active, Math.max(0, filteredSorted.length - 1));
295
- const rows = filteredSorted
296
- .map((chat, i) => renderRow(chat, i === safeActive))
297
- .join("\n");
298
-
299
358
  if (filteredSorted.length === 0) {
300
359
  return [`${titleBar}\nNo conversations found.`, hintLine];
301
360
  }
302
361
 
362
+ const windowEnd = Math.min(
363
+ filteredSorted.length,
364
+ windowStart + pageSize,
365
+ );
366
+ const visibleItems = filteredSorted.slice(windowStart, windowEnd);
367
+ const rows = visibleItems
368
+ .map((item) => renderRow(item, item === filteredSorted[safeActive]))
369
+ .join("\n");
370
+
303
371
  return [`${titleBar}\n${rows}`, hintLine];
304
372
  },
305
373
  );