@youdie006/prodex 0.30.0 → 0.31.0

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.
@@ -2502,6 +2502,20 @@ export async function navigateChatGptTabTo(url, options = {}) {
2502
2502
  cdp.close();
2503
2503
  }
2504
2504
  }
2505
+ /** Projects with the id their conversations are tagged with. */
2506
+ export async function listChatGptProjectsWithIds(input = {}) {
2507
+ const port = resolveCdpPort(input.port);
2508
+ const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
2509
+ if (!page.ok || !page.page)
2510
+ return [];
2511
+ const { projectsWithIdsExpression } = await import("./tui.js");
2512
+ try {
2513
+ return (await evaluateOnPage(page.page, projectsWithIdsExpression(), { timeoutMs: 30_000 })) ?? [];
2514
+ }
2515
+ catch {
2516
+ return [];
2517
+ }
2518
+ }
2505
2519
  export async function listRecentChatGptConversations(input = {}) {
2506
2520
  const port = resolveCdpPort(input.port);
2507
2521
  const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
package/dist/cli.js CHANGED
@@ -86,6 +86,10 @@ async function runInteractiveUi(io) {
86
86
  const { navigateChatGptTabTo } = await import("./chatgpt-browser.js");
87
87
  return navigateChatGptTabTo(url, {});
88
88
  },
89
+ listProjectsWithIds: async () => {
90
+ const { listChatGptProjectsWithIds } = await import("./chatgpt-browser.js");
91
+ return listChatGptProjectsWithIds({});
92
+ },
89
93
  listProjects: async () => {
90
94
  const { listChatGptSidebarProjects } = await import("./chatgpt-browser.js");
91
95
  const listed = await listChatGptSidebarProjects({});
package/dist/tui-flow.js CHANGED
@@ -39,11 +39,14 @@ export function destinationChoices(pinnedProject) {
39
39
  * and ChatGPT deselects Pro as soon as an effort is set.
40
40
  */
41
41
  export function effortChoices(pinnedModel) {
42
+ // Reads down from what the repo pinned: the level just under Pro sits just
43
+ // under "Keep", and Instant is at the bottom. Climbing up from Instant put
44
+ // the levels closest to Pro furthest from it.
42
45
  return [
43
46
  { label: pinnedModel ? `Keep ${pinnedModel}` : "Keep the current selection", hint: "whatever this repo pinned" },
44
- { effort: "instant", label: "Instant", hint: "seconds, for a quick question" },
45
- { effort: "medium", label: "Medium" },
47
+ { effort: "max", label: "Extra high", hint: "strongest of the standard levels" },
46
48
  { effort: "high", label: "High" },
47
- { effort: "max", label: "Extra high", hint: "slowest of the standard levels" }
49
+ { effort: "medium", label: "Medium" },
50
+ { effort: "instant", label: "Instant", hint: "seconds, for a quick question" }
48
51
  ];
49
52
  }
package/dist/tui-run.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import readline from "node:readline";
10
10
  import { renderBanner } from "./banner.js";
11
11
  import { destinationChoices, effortChoices, SEND_KINDS } from "./tui-flow.js";
12
- import { consultArgsFromChoices, conversationThreadUrl, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
12
+ import { consultArgsFromChoices, conversationsInProject, conversationThreadUrl, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
13
13
  const ESC = "";
14
14
  const CLEAR = `${ESC}[2J${ESC}[H`;
15
15
  // The alternate screen buffer: the terminal comes back exactly as it was, so a
@@ -205,16 +205,34 @@ export async function runInteractiveConsult(io, deps) {
205
205
  targetUrl = conversationThreadUrl(conversations[chosen].id);
206
206
  }
207
207
  else if (destination === "project") {
208
- const projects = await deps.listProjects();
208
+ const detailed = (await deps.listProjectsWithIds?.()) ?? [];
209
+ const projects = detailed.length > 0 ? detailed : (await deps.listProjects()).map((name) => ({ id: "", name }));
209
210
  if (projects.length === 0) {
210
211
  io.write("\nNo projects are visible in the ChatGPT sidebar.\n");
211
212
  return 1;
212
213
  }
213
- const chosen = await pick(io, { title: "Which project?", options: projects.map((name) => ({ label: name })) }, header);
214
+ const chosen = await pick(io, { title: "Which project?", options: projects.map((entry) => ({ label: entry.name })) }, header);
214
215
  if (chosen === undefined)
215
216
  return cancel(io);
216
217
  projectMode = "existing";
217
- projectName = projects[chosen];
218
+ projectName = projects[chosen].name;
219
+ // Entering a project usually means going back to something in it, so
220
+ // offer its chats before starting yet another one.
221
+ const inside = conversationsInProject((await conversationsPromise) ?? [], projects[chosen].id || undefined);
222
+ if (inside.length > 0) {
223
+ const options = [
224
+ { label: "Start a new chat in this project" },
225
+ ...inside.map((entry) => ({ label: entry.title }))
226
+ ];
227
+ const picked = await pick(io, { title: `${projects[chosen].name}`, options }, header);
228
+ if (picked === undefined)
229
+ return cancel(io);
230
+ if (picked > 0) {
231
+ targetUrl = conversationThreadUrl(inside[picked - 1].id);
232
+ projectMode = "current";
233
+ projectName = undefined;
234
+ }
235
+ }
218
236
  }
219
237
  else if (destination === "project-new") {
220
238
  projectName = await askLine(io, `${CLEAR}${header}New project\n\n name: `);
package/dist/tui.js CHANGED
@@ -57,6 +57,46 @@ export function consultArgsFromChoices(choices) {
57
57
  args.push("--", prompt);
58
58
  return args;
59
59
  }
60
+ /**
61
+ * Projects with the id their conversations are tagged with.
62
+ *
63
+ * The sidebar gives names only, which is enough to ENTER a project but not to
64
+ * tell which chats live in it - and "open the project, then pick the session
65
+ * inside it" needs exactly that link.
66
+ */
67
+ export function projectsWithIdsExpression() {
68
+ return `(async () => {
69
+ let token = "";
70
+ try {
71
+ const session = await fetch("/api/auth/session", { credentials: "include" });
72
+ if (!session.ok) return [];
73
+ const parsed = await session.json();
74
+ token = (parsed && parsed.accessToken) || "";
75
+ } catch (error) {
76
+ return [];
77
+ }
78
+ try {
79
+ const response = await fetch("/backend-api/gizmos/snorlax/sidebar", {
80
+ credentials: "include",
81
+ headers: token ? { Authorization: "Bearer " + token } : {}
82
+ });
83
+ if (!response.ok) return [];
84
+ const listed = await response.json();
85
+ return ((listed && listed.items) || [])
86
+ .map((item) => item && item.gizmo)
87
+ .filter((gizmo) => gizmo && gizmo.id)
88
+ .map((gizmo) => ({ id: gizmo.id, name: ((gizmo.display && gizmo.display.name) || gizmo.name || "").trim() || gizmo.id }));
89
+ } catch (error) {
90
+ return [];
91
+ }
92
+ })()`;
93
+ }
94
+ /** The conversations that live inside a project, or all of them without one. */
95
+ export function conversationsInProject(conversations, projectId) {
96
+ if (!projectId)
97
+ return conversations;
98
+ return conversations.filter((conversation) => conversation.gizmoId === projectId);
99
+ }
60
100
  /**
61
101
  * Recent conversations with their titles, for the "continue an existing chat"
62
102
  * list. Only the sidebar listing is fetched - the transcripts themselves are
@@ -82,7 +122,11 @@ export function recentConversationTitlesExpression(limit = 10) {
82
122
  const listed = await response.json();
83
123
  return ((listed && listed.items) || [])
84
124
  .filter((item) => item && item.id)
85
- .map((item) => ({ id: item.id, title: (item.title || "").trim() || "Untitled" }));
125
+ .map((item) => ({
126
+ id: item.id,
127
+ title: (item.title || "").trim() || "Untitled",
128
+ ...(item.gizmo_id ? { gizmoId: item.gizmo_id } : {})
129
+ }));
86
130
  } catch (error) {
87
131
  return [];
88
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",