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
|
@@ -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:
|
|
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
|
|
package/src/cli/utils/prompts.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
);
|