@hienlh/ppm 0.9.2 → 0.9.4

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/web/assets/{browser-tab-CjUzlPYv.js → browser-tab-B8EBpCT6.js} +1 -1
  3. package/dist/web/assets/chat-tab-DVuWNtNl.js +8 -0
  4. package/dist/web/assets/{code-editor-aQQZUc2m.js → code-editor-Bre-_HLS.js} +1 -1
  5. package/dist/web/assets/{database-viewer-ChyP1N3c.js → database-viewer-As9Pwu58.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-ktwO5JbX.js → diff-viewer-DTw_Cqpy.js} +1 -1
  7. package/dist/web/assets/{extension-webview-Bx1TlP6q.js → extension-webview-CgobD-e5.js} +1 -1
  8. package/dist/web/assets/{git-graph-BIrGMX6e.js → git-graph-CA1TsLfJ.js} +1 -1
  9. package/dist/web/assets/index-Dx21KBME.css +2 -0
  10. package/dist/web/assets/{index-C6KLr58u.js → index-iaC9D6YI.js} +4 -4
  11. package/dist/web/assets/keybindings-store-BzWEhrZs.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-A7J2gdKT.js → markdown-renderer-zGERfKXC.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-C9-Acry_.js → postgres-viewer-q-a11Zs_.js} +1 -1
  14. package/dist/web/assets/{settings-tab-C17exmRv.js → settings-tab-9ISb6u9A.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-Dr5oWCWA.js → sqlite-viewer-CEfqfnpC.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-CpyKvyfC.js → terminal-tab-BGthbegR.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-BjPAik5w.js → use-monaco-theme-0QrlcbfM.js} +1 -1
  18. package/dist/web/index.html +2 -2
  19. package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
  20. package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
  21. package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
  22. package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
  23. package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
  24. package/dist/web/sw.js +1 -1
  25. package/docs/project-roadmap.md +3 -3
  26. package/docs/streaming-input-guide.md +267 -0
  27. package/package.json +1 -1
  28. package/packages/ext-database/package.json +9 -2
  29. package/packages/ext-database/src/connection-tree.ts +64 -5
  30. package/packages/ext-database/src/extension.ts +234 -2
  31. package/packages/ext-database/src/query-panel.ts +29 -16
  32. package/packages/ext-database/src/table-viewer-panel.ts +16 -6
  33. package/snapshot-state.md +1526 -0
  34. package/src/providers/claude-agent-sdk.ts +16 -11
  35. package/src/web/components/chat/chat-tab.tsx +1 -0
  36. package/src/web/components/chat/message-list.tsx +8 -4
  37. package/src/web/components/layout/mobile-nav.tsx +46 -42
  38. package/test-session-ops.mjs +444 -0
  39. package/test-tokens.mjs +212 -0
  40. package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
  41. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  42. package/dist/web/assets/chat-tab-moB4W7-w.js +0 -8
  43. package/dist/web/assets/index-DpBKDbIW.css +0 -2
  44. package/dist/web/assets/keybindings-store-D3Y5c5uS.js +0 -1
@@ -96,6 +96,67 @@ export function activate(context: ExtensionContext, vscode: VscodeApi): void {
96
96
  }),
97
97
  );
98
98
 
99
+ context.subscriptions.push(
100
+ vscode.commands.registerCommand("ppm-db.editConnection", async (...args: unknown[]) => {
101
+ const connectionId = args[0] as number;
102
+ if (connectionId) await editConnection(vscode, treeProvider, connectionId);
103
+ }),
104
+ );
105
+
106
+ context.subscriptions.push(
107
+ vscode.commands.registerCommand("ppm-db.deleteConnection", async (...args: unknown[]) => {
108
+ const connectionId = args[0] as number;
109
+ const connectionName = (args[1] as string) ?? "this connection";
110
+ if (!connectionId) return;
111
+ const confirm = await vscode.window.showQuickPick(["Yes, delete", "Cancel"], {
112
+ placeHolder: `Delete "${connectionName}"?`,
113
+ });
114
+ if (confirm !== "Yes, delete") return;
115
+ try {
116
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, { method: "DELETE" });
117
+ const json = await res.json() as { ok: boolean; error?: string };
118
+ if (json.ok) {
119
+ await vscode.window.showInformationMessage(`Connection "${connectionName}" deleted`);
120
+ treeProvider.refresh();
121
+ } else {
122
+ await vscode.window.showErrorMessage(json.error ?? "Failed to delete connection");
123
+ }
124
+ } catch (e) {
125
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
126
+ }
127
+ }),
128
+ );
129
+
130
+ context.subscriptions.push(
131
+ vscode.commands.registerCommand("ppm-db.testConnection", async (...args: unknown[]) => {
132
+ const connectionId = args[0] as number;
133
+ if (!connectionId) return;
134
+ try {
135
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}/test`, { method: "POST" });
136
+ const json = await res.json() as { ok: boolean; data?: { ok: boolean; error?: string } };
137
+ if (json.ok && json.data?.ok) {
138
+ await vscode.window.showInformationMessage("Connection successful");
139
+ } else {
140
+ await vscode.window.showErrorMessage(`Connection failed: ${json.data?.error ?? "Unknown error"}`);
141
+ }
142
+ } catch (e) {
143
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
144
+ }
145
+ }),
146
+ );
147
+
148
+ context.subscriptions.push(
149
+ vscode.commands.registerCommand("ppm-db.exportConnections", async () => {
150
+ await exportConnections(vscode);
151
+ }),
152
+ );
153
+
154
+ context.subscriptions.push(
155
+ vscode.commands.registerCommand("ppm-db.importConnections", async () => {
156
+ await importConnections(vscode, treeProvider);
157
+ }),
158
+ );
159
+
99
160
  // --- Status Bar ---
100
161
  const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 10);
101
162
  statusItem.text = "DB";
@@ -297,6 +358,27 @@ function openQueryPanel(
297
358
  // Add Connection — collect info via QuickPick + InputBox, then POST to API
298
359
  // ---------------------------------------------------------------------------
299
360
 
361
+ /** Collect optional group/color/readonly via InputBox + QuickPick */
362
+ async function collectConnectionExtras(vscode: VscodeApi): Promise<{ groupName?: string; color?: string; readonly?: number } | null> {
363
+ const groupName = await vscode.window.showInputBox({ prompt: "Group name (optional)", placeHolder: "e.g. Production" });
364
+ if (groupName === undefined) return null; // cancelled
365
+
366
+ const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
367
+ const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: "Pick a color (optional)" });
368
+ if (color === undefined) return null;
369
+
370
+ const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
371
+ placeHolder: "Readonly mode? (blocks write queries)",
372
+ });
373
+ if (readonlyChoice === undefined) return null;
374
+
375
+ return {
376
+ groupName: groupName || undefined,
377
+ color: color === "None" ? undefined : color,
378
+ readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
379
+ };
380
+ }
381
+
300
382
  async function addConnection(
301
383
  vscode: VscodeApi,
302
384
  treeProvider: ConnectionTreeProvider,
@@ -326,12 +408,16 @@ async function addConnection(
326
408
  connectionConfig = { type: "postgres", connectionString: connStr };
327
409
  }
328
410
 
329
- // 4. Create via API
411
+ // 4. Group, color, readonly
412
+ const extras = await collectConnectionExtras(vscode);
413
+ if (extras === null) return;
414
+
415
+ // 5. Create via API
330
416
  try {
331
417
  const res = await fetch(`${baseUrl}/api/db/connections`, {
332
418
  method: "POST",
333
419
  headers: { "Content-Type": "application/json" },
334
- body: JSON.stringify({ type, name, connectionConfig }),
420
+ body: JSON.stringify({ type, name, connectionConfig, ...extras }),
335
421
  });
336
422
  const json = await res.json() as { ok: boolean; error?: string };
337
423
  if (json.ok) {
@@ -344,3 +430,149 @@ async function addConnection(
344
430
  await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
345
431
  }
346
432
  }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Edit Connection — update name, group, color, readonly via QuickPick/InputBox
436
+ // ---------------------------------------------------------------------------
437
+
438
+ async function editConnection(
439
+ vscode: VscodeApi,
440
+ treeProvider: ConnectionTreeProvider,
441
+ connectionId: number,
442
+ ): Promise<void> {
443
+ // Fetch current connection data
444
+ let conn: { id: number; name: string; type: string; group_name?: string; color?: string; readonly?: number };
445
+ try {
446
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`);
447
+ const json = await res.json() as { ok: boolean; data?: typeof conn };
448
+ if (!json.ok || !json.data) {
449
+ await vscode.window.showErrorMessage("Failed to load connection");
450
+ return;
451
+ }
452
+ conn = json.data;
453
+ } catch (e) {
454
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
455
+ return;
456
+ }
457
+
458
+ // Name
459
+ const name = await vscode.window.showInputBox({ prompt: "Connection name", value: conn.name });
460
+ if (name === undefined) return;
461
+
462
+ // Connection config (optional update)
463
+ let connectionConfig: Record<string, string> | undefined;
464
+ const updateConfig = await vscode.window.showQuickPick(["Keep current", "Update connection config"], {
465
+ placeHolder: "Connection config",
466
+ });
467
+ if (updateConfig === undefined) return;
468
+ if (updateConfig === "Update connection config") {
469
+ if (conn.type === "sqlite") {
470
+ const path = await vscode.window.showInputBox({ prompt: "SQLite file path", placeHolder: "/path/to/database.db" });
471
+ if (path === undefined) return;
472
+ if (path) connectionConfig = { type: "sqlite", path };
473
+ } else {
474
+ const connStr = await vscode.window.showInputBox({ prompt: "PostgreSQL connection string", placeHolder: "postgres://user:pass@host:5432/dbname" });
475
+ if (connStr === undefined) return;
476
+ if (connStr) connectionConfig = { type: "postgres", connectionString: connStr };
477
+ }
478
+ }
479
+
480
+ // Group, color, readonly
481
+ const groupName = await vscode.window.showInputBox({ prompt: "Group name", value: conn.group_name ?? "" });
482
+ if (groupName === undefined) return;
483
+
484
+ const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
485
+ const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: `Current color: ${conn.color ?? "None"}` });
486
+ if (color === undefined) return;
487
+
488
+ const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
489
+ placeHolder: `Readonly? (current: ${conn.readonly === 1 ? "Yes" : "No"})`,
490
+ });
491
+ if (readonlyChoice === undefined) return;
492
+
493
+ // Update via API
494
+ try {
495
+ const body: Record<string, unknown> = {
496
+ name: name || conn.name,
497
+ groupName: groupName || null,
498
+ color: color === "None" ? null : color,
499
+ readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
500
+ };
501
+ if (connectionConfig) body.connectionConfig = connectionConfig;
502
+
503
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, {
504
+ method: "PUT",
505
+ headers: { "Content-Type": "application/json" },
506
+ body: JSON.stringify(body),
507
+ });
508
+ const json = await res.json() as { ok: boolean; error?: string };
509
+ if (json.ok) {
510
+ await vscode.window.showInformationMessage(`Connection "${name || conn.name}" updated`);
511
+ treeProvider.refresh();
512
+ } else {
513
+ await vscode.window.showErrorMessage(json.error ?? "Failed to update connection");
514
+ }
515
+ } catch (e) {
516
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
517
+ }
518
+ }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // Export Connections — copy JSON to clipboard via showInformationMessage
522
+ // ---------------------------------------------------------------------------
523
+
524
+ async function exportConnections(vscode: VscodeApi): Promise<void> {
525
+ try {
526
+ const res = await fetch(`${baseUrl}/api/db/connections/export`);
527
+ const json = await res.json() as { ok: boolean; data?: unknown };
528
+ if (!json.ok || !json.data) {
529
+ await vscode.window.showErrorMessage("Failed to export connections");
530
+ return;
531
+ }
532
+ // In extension context, we can't write to clipboard directly — show data as info
533
+ const data = json.data as { connections: unknown[] };
534
+ await vscode.window.showInformationMessage(`Exported ${data.connections.length} connection(s). Use built-in UI for file export.`);
535
+ } catch (e) {
536
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
537
+ }
538
+ }
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // Import Connections — paste JSON via InputBox
542
+ // ---------------------------------------------------------------------------
543
+
544
+ async function importConnections(
545
+ vscode: VscodeApi,
546
+ treeProvider: ConnectionTreeProvider,
547
+ ): Promise<void> {
548
+ const jsonStr = await vscode.window.showInputBox({
549
+ prompt: "Paste connections JSON (from export)",
550
+ placeHolder: '{"connections": [...]}',
551
+ });
552
+ if (!jsonStr) return;
553
+
554
+ try {
555
+ const data = JSON.parse(jsonStr);
556
+ const conns = data.connections ?? data;
557
+ if (!Array.isArray(conns)) {
558
+ await vscode.window.showErrorMessage("Invalid format: expected connections array");
559
+ return;
560
+ }
561
+ const res = await fetch(`${baseUrl}/api/db/connections/import`, {
562
+ method: "POST",
563
+ headers: { "Content-Type": "application/json" },
564
+ body: JSON.stringify({ connections: conns }),
565
+ });
566
+ const json = await res.json() as { ok: boolean; data?: { imported: number; skipped: number; errors: string[] } };
567
+ if (json.ok && json.data) {
568
+ let msg = `Imported ${json.data.imported} connection(s)`;
569
+ if (json.data.skipped > 0) msg += `, ${json.data.skipped} skipped`;
570
+ await vscode.window.showInformationMessage(msg);
571
+ treeProvider.refresh();
572
+ } else {
573
+ await vscode.window.showErrorMessage("Import failed");
574
+ }
575
+ } catch (e) {
576
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
577
+ }
578
+ }
@@ -15,25 +15,38 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
15
15
  <head>
16
16
  <meta charset="utf-8">
17
17
  <style>
18
- * { box-sizing: border-box; margin: 0; padding: 0; }
19
- body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #1e1e2e; color: #cdd6f4; padding: 12px; font-size: 13px; }
18
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
19
+ :root {
20
+ --bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
21
+ --border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
22
+ --surface-hover: #f4f4f5;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
27
+ --border: #27272a; --border2: #3f3f46; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
28
+ --surface-hover: #27272a;
29
+ }
30
+ }
31
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); padding: 12px; font-size: 13px; }
20
32
  .header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
21
33
  .header h3 { font-size: 14px; font-weight: 600; }
22
- .header .badge { background: #313244; padding: 2px 8px; border-radius: 4px; font-size: 11px; color: #a6adc8; }
23
- textarea { width: 100%; height: 80px; background: #313244; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; padding: 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; resize: vertical; }
24
- textarea:focus { outline: none; border-color: #89b4fa; }
34
+ .header .badge { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--subtext); }
35
+ textarea { width: 100%; height: 80px; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; color: var(--text); padding: 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; resize: vertical; }
36
+ textarea:focus { outline: none; border-color: var(--blue); }
25
37
  .actions { display: flex; gap: 8px; margin: 8px 0; }
26
- button { background: #89b4fa; color: #1e1e2e; border: none; padding: 6px 16px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
27
- button:hover { background: #74c7ec; }
28
- button.secondary { background: #313244; color: #cdd6f4; }
29
- .status { font-size: 11px; color: #a6adc8; margin: 4px 0; }
30
- .error { color: #f38ba8; background: #45475a; padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 12px; }
38
+ button { background: var(--blue); color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
39
+ button:hover { opacity: 0.9; }
40
+ button.secondary { background: var(--surface); color: var(--text); }
41
+ .status { font-size: 11px; color: var(--subtext); margin: 4px 0; }
42
+ .error { color: var(--red); background: var(--surface); padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 12px; }
31
43
  table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; }
32
- th { background: #313244; text-align: left; padding: 6px 8px; border-bottom: 1px solid #45475a; font-weight: 600; position: sticky; top: 0; }
33
- td { padding: 4px 8px; border-bottom: 1px solid #313244; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
- tr:hover td { background: #313244; }
35
- .results { max-height: 60vh; overflow: auto; border: 1px solid #45475a; border-radius: 6px; }
36
- .empty { text-align: center; padding: 24px; color: #6c7086; }
44
+ th { background: var(--surface); text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border2); font-weight: 600; position: sticky; top: 0; color: var(--subtext); }
45
+ td { padding: 4px 8px; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
46
+ tr:hover td { background: var(--surface-hover); }
47
+ .results { max-height: 60vh; overflow: auto; border: 1px solid var(--border2); border-radius: 6px; }
48
+ .empty { text-align: center; padding: 24px; color: var(--subtle); }
49
+ .null-val { color: var(--subtle); font-style: italic; }
37
50
  </style>
38
51
  </head>
39
52
  <body>
@@ -101,7 +114,7 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
101
114
  html += '<tr>';
102
115
  cols.forEach(c => {
103
116
  const v = row[c];
104
- html += '<td>' + (v === null ? '<span style="color:#6c7086">NULL</span>' : esc(v)) + '</td>';
117
+ html += v === null ? '<td class="null-val">NULL</td>' : '<td>' + esc(v) + '</td>';
105
118
  });
106
119
  html += '</tr>';
107
120
  });
@@ -27,12 +27,22 @@ export function getTableViewerHtml(opts: TableViewerOptions): string {
27
27
  <style>
28
28
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
29
29
  :root {
30
- --bg: #1e1e2e; --surface: #181825; --mantle: #11111b;
31
- --overlay0: #6c7086; --overlay1: #7f849c;
32
- --text: #cdd6f4; --subtext: #a6adc8; --subtle: #585b70;
33
- --border: #313244; --border2: #45475a;
34
- --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8; --yellow: #f9e2af;
35
- --surface-hover: #313244;
30
+ --bg: #ffffff; --surface: #f4f4f5; --mantle: #fafafa;
31
+ --overlay0: #71717a; --overlay1: #52525b;
32
+ --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
33
+ --border: #e4e4e7; --border2: #d4d4d8;
34
+ --blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
35
+ --surface-hover: #f4f4f5;
36
+ }
37
+ @media (prefers-color-scheme: dark) {
38
+ :root {
39
+ --bg: #09090b; --surface: #18181b; --mantle: #09090b;
40
+ --overlay0: #71717a; --overlay1: #a1a1aa;
41
+ --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
42
+ --border: #27272a; --border2: #3f3f46;
43
+ --blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
44
+ --surface-hover: #27272a;
45
+ }
36
46
  }
37
47
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
38
48