@hienlh/ppm 0.6.4 → 0.6.6

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 (57) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/web/assets/{chat-tab-CDVCDw_H.js → chat-tab-dwpaSkQD.js} +3 -3
  3. package/dist/web/assets/{code-editor-wmS73ejX.js → code-editor-ZFl5kZ4-.js} +1 -1
  4. package/dist/web/assets/database-viewer-DPpOsMqa.js +1 -0
  5. package/dist/web/assets/{diff-viewer-BsYccTx1.js → diff-viewer-CX74l6lV.js} +1 -1
  6. package/dist/web/assets/dist-Jb3Tnkpc.js +16 -0
  7. package/dist/web/assets/{git-graph-BbWb6_Jq.js → git-graph-Dju1rygf.js} +1 -1
  8. package/dist/web/assets/index-DSg2VjxL.css +2 -0
  9. package/dist/web/assets/{index-DhuAmTQ1.js → index-DXOEmhRm.js} +6 -6
  10. package/dist/web/assets/{input-CCCPR1s4.js → input-nI4xe1Y9.js} +1 -1
  11. package/dist/web/assets/keybindings-store-VhiJwp77.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-aPdw9BhU.js → markdown-renderer-Bke6DHFh.js} +1 -1
  13. package/dist/web/assets/postgres-viewer-DaNYnInA.js +1 -0
  14. package/dist/web/assets/{settings-store-DgOSmeGL.js → settings-store-CfB0vCtQ.js} +1 -1
  15. package/dist/web/assets/settings-tab-DD05d8rM.js +1 -0
  16. package/dist/web/assets/sqlite-viewer-Cx7tLyT-.js +1 -0
  17. package/dist/web/assets/{tab-store-DhXold0e.js → tab-store-DIyJSjtr.js} +1 -1
  18. package/dist/web/assets/table-DCVKGOr2.js +1 -0
  19. package/dist/web/assets/{terminal-tab-3tDV4RCn.js → terminal-tab-_farMLMO.js} +1 -1
  20. package/dist/web/assets/{use-monaco-theme-Ccqh1RD4.js → use-monaco-theme-Dexl3s3E.js} +1 -1
  21. package/dist/web/index.html +8 -8
  22. package/dist/web/sw.js +1 -1
  23. package/package.json +1 -1
  24. package/src/server/routes/chat.ts +2 -2
  25. package/src/server/routes/database.ts +5 -3
  26. package/src/services/database/sqlite-adapter.ts +1 -2
  27. package/src/services/sqlite.service.ts +2 -1
  28. package/src/types/config.ts +10 -2
  29. package/src/web/components/chat/tool-cards.tsx +2 -2
  30. package/src/web/components/database/connection-list.tsx +82 -33
  31. package/src/web/components/database/database-sidebar.tsx +2 -13
  32. package/src/web/components/database/database-viewer.tsx +232 -0
  33. package/src/web/components/database/use-connections.ts +14 -21
  34. package/src/web/components/database/use-database.ts +120 -0
  35. package/src/web/components/layout/command-palette.tsx +4 -5
  36. package/src/web/components/layout/editor-panel.tsx +1 -0
  37. package/src/web/components/layout/mobile-nav.tsx +1 -1
  38. package/src/web/components/layout/sidebar.tsx +1 -2
  39. package/src/web/components/layout/tab-bar.tsx +1 -0
  40. package/src/web/components/layout/tab-content.tsx +5 -0
  41. package/src/web/components/postgres/postgres-viewer.tsx +38 -30
  42. package/src/web/components/postgres/use-postgres.ts +2 -5
  43. package/src/web/components/settings/ai-settings-section.tsx +0 -1
  44. package/src/web/components/sqlite/sqlite-viewer.tsx +24 -18
  45. package/src/web/components/sqlite/use-sqlite.ts +4 -4
  46. package/src/web/hooks/use-chat.ts +1 -1
  47. package/src/web/hooks/use-usage.ts +1 -1
  48. package/src/web/stores/tab-store.ts +1 -0
  49. package/dist/web/assets/dist-PpKqMvyx.js +0 -16
  50. package/dist/web/assets/index-aIGuIMQ8.css +0 -2
  51. package/dist/web/assets/keybindings-store-BqgrTQAC.js +0 -1
  52. package/dist/web/assets/postgres-viewer-V4hKmmzV.js +0 -1
  53. package/dist/web/assets/settings-tab-DwsKpk9T.js +0 -1
  54. package/dist/web/assets/sqlite-viewer-BRsj8GXc.js +0 -1
  55. /package/dist/web/assets/{api-client-BHpHp5Lz.js → api-client-4Ni0i4Hl.js} +0 -0
  56. /package/dist/web/assets/{react-l9v2XLcs.js → react-DHSo28we.js} +0 -0
  57. /package/dist/web/assets/{utils-CAPYyGV3.js → utils-siJJ3uG0.js} +0 -0
@@ -0,0 +1,120 @@
1
+ import { useState, useCallback } from "react";
2
+ import { api } from "@/lib/api-client";
3
+
4
+ export interface DbTableInfo { name: string; schema: string; rowCount: number }
5
+ export interface DbColumnInfo { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null }
6
+ export interface DbQueryResult { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: "select" | "modify" }
7
+ interface DbTableData { columns: string[]; rows: Record<string, unknown>[]; total: number; page: number; limit: number }
8
+
9
+ /** SessionStorage cache key for table data */
10
+ function cacheKey(connectionId: number, table: string, schema: string, page: number) {
11
+ return `ppm-db-${connectionId}-${schema}.${table}-p${page}`;
12
+ }
13
+
14
+ function readCache(connectionId: number, table: string, schema: string, page: number): { data: DbTableData; cols: DbColumnInfo[] } | null {
15
+ try {
16
+ const raw = sessionStorage.getItem(cacheKey(connectionId, table, schema, page));
17
+ return raw ? JSON.parse(raw) : null;
18
+ } catch { return null; }
19
+ }
20
+
21
+ function writeCache(connectionId: number, table: string, schema: string, page: number, data: DbTableData, cols: DbColumnInfo[]) {
22
+ try { sessionStorage.setItem(cacheKey(connectionId, table, schema, page), JSON.stringify({ data, cols })); } catch { /* quota */ }
23
+ }
24
+
25
+ /**
26
+ * Generic database hook for unified API (/api/db/connections/:id/...).
27
+ * Works for any DB type (postgres, sqlite, mysql, etc.) via adapter pattern.
28
+ * No auto-fetch on mount — viewer calls selectTable() to start loading.
29
+ */
30
+ export function useDatabase(connectionId: number) {
31
+ const base = `/api/db/connections/${connectionId}`;
32
+ const [selectedTable, setSelectedTable] = useState<string | null>(null);
33
+ const [selectedSchema, setSelectedSchema] = useState("public");
34
+ const [tableData, setTableData] = useState<DbTableData | null>(null);
35
+ const [schema, setSchema] = useState<DbColumnInfo[]>([]);
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState<string | null>(null);
38
+ const [page, setPageState] = useState(1);
39
+ const [queryResult, setQueryResult] = useState<DbQueryResult | null>(null);
40
+ const [queryError, setQueryError] = useState<string | null>(null);
41
+ const [queryLoading, setQueryLoading] = useState(false);
42
+
43
+ // Fetch table data + schema for current selection
44
+ const fetchTableData = useCallback(async (table?: string, tableSchema?: string, p?: number) => {
45
+ const t = table ?? selectedTable;
46
+ const s = tableSchema ?? selectedSchema;
47
+ if (!t) return;
48
+ setLoading(true);
49
+ try {
50
+ const [data, cols] = await Promise.all([
51
+ api.get<DbTableData>(`${base}/data?table=${encodeURIComponent(t)}&schema=${s}&page=${p ?? page}&limit=100`),
52
+ api.get<DbColumnInfo[]>(`${base}/schema?table=${encodeURIComponent(t)}&schema=${s}`),
53
+ ]);
54
+ setTableData(data);
55
+ setSchema(cols);
56
+ writeCache(connectionId, t, s, p ?? page, data, cols);
57
+ } catch (e) {
58
+ setError((e as Error).message);
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ }, [base, connectionId, selectedTable, selectedSchema, page]);
63
+
64
+ const selectTable = useCallback((name: string, tableSchema = "public") => {
65
+ setSelectedTable(name);
66
+ setSelectedSchema(tableSchema);
67
+ setPageState(1);
68
+ setQueryResult(null);
69
+ // Show cached data instantly, then refresh in background
70
+ const cached = readCache(connectionId, name, tableSchema, 1);
71
+ if (cached) {
72
+ setTableData(cached.data);
73
+ setSchema(cached.cols);
74
+ setLoading(false);
75
+ // Still fetch fresh data in background
76
+ fetchTableData(name, tableSchema, 1);
77
+ } else {
78
+ fetchTableData(name, tableSchema, 1);
79
+ }
80
+ }, [connectionId, fetchTableData]);
81
+
82
+ const changePage = useCallback((p: number) => {
83
+ setPageState(p);
84
+ fetchTableData(undefined, undefined, p);
85
+ }, [fetchTableData]);
86
+
87
+ const executeQuery = useCallback(async (sqlText: string) => {
88
+ setQueryLoading(true);
89
+ setQueryError(null);
90
+ try {
91
+ const result = await api.post<DbQueryResult>(`${base}/query`, { sql: sqlText });
92
+ setQueryResult(result);
93
+ if (result.changeType === "modify") fetchTableData(selectedTable ?? undefined, selectedSchema);
94
+ } catch (e) {
95
+ setQueryError((e as Error).message);
96
+ } finally {
97
+ setQueryLoading(false);
98
+ }
99
+ }, [base, selectedTable, selectedSchema, fetchTableData]);
100
+
101
+ const updateCell = useCallback(async (pkColumn: string, pkValue: unknown, column: string, value: unknown) => {
102
+ if (!selectedTable) return;
103
+ const t = selectedTable;
104
+ const s = selectedSchema;
105
+ try {
106
+ await api.put(`${base}/cell`, { table: t, schema: s, pkColumn, pkValue, column, value });
107
+ // Re-fetch with explicit args to avoid stale closure
108
+ fetchTableData(t, s);
109
+ } catch (e) {
110
+ setError((e as Error).message);
111
+ }
112
+ }, [base, selectedTable, selectedSchema, fetchTableData]);
113
+
114
+ return {
115
+ selectedTable, selectTable, tableData, schema,
116
+ loading, error, page, setPage: changePage,
117
+ queryResult, queryError, queryLoading, executeQuery,
118
+ updateCell, refreshData: fetchTableData,
119
+ };
120
+ }
@@ -119,9 +119,8 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
119
119
  if (isPathQuery(query) || query.trim().length < 2) { setDbResults([]); return; }
120
120
  const timer = setTimeout(async () => {
121
121
  try {
122
- const res = await fetch(`/api/db/search?q=${encodeURIComponent(query.trim())}`);
123
- const json = await res.json() as { ok: boolean; data?: DbSearchResult[] };
124
- setDbResults(json.ok ? (json.data ?? []) : []);
122
+ const data = await api.get<DbSearchResult[]>(`/api/db/search?q=${encodeURIComponent(query.trim())}`);
123
+ setDbResults(data ?? []);
125
124
  } catch { setDbResults([]); }
126
125
  }, 300);
127
126
  return () => clearTimeout(timer);
@@ -219,11 +218,11 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
219
218
  connectionColor: r.connectionColor,
220
219
  action: () => {
221
220
  openTab({
222
- type: r.connectionType === "postgres" ? "postgres" : "sqlite",
221
+ type: "database",
223
222
  title: `${r.connectionName} · ${r.tableName}`,
224
223
  projectId: null,
225
224
  closable: true,
226
- metadata: { connectionId: r.connectionId, tableName: r.tableName, schemaName: r.schemaName, connectionColor: r.connectionColor },
225
+ metadata: { connectionId: r.connectionId, connectionName: r.connectionName, dbType: r.connectionType, tableName: r.tableName, schemaName: r.schemaName, connectionColor: r.connectionColor },
227
226
  });
228
227
  onClose();
229
228
  },
@@ -17,6 +17,7 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
17
17
  terminal: lazy(() => import("@/components/terminal/terminal-tab").then((m) => ({ default: m.TerminalTab }))),
18
18
  chat: lazy(() => import("@/components/chat/chat-tab").then((m) => ({ default: m.ChatTab }))),
19
19
  editor: lazy(() => import("@/components/editor/code-editor").then((m) => ({ default: m.CodeEditor }))),
20
+ database: lazy(() => import("@/components/database/database-viewer").then((m) => ({ default: m.DatabaseViewer }))),
20
21
  sqlite: lazy(() => import("@/components/sqlite/sqlite-viewer").then((m) => ({ default: m.SqliteViewer }))),
21
22
  postgres: lazy(() => import("@/components/postgres/postgres-viewer").then((m) => ({ default: m.PostgresViewer }))),
22
23
  "git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
@@ -21,7 +21,7 @@ const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
21
21
  const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_TAB_OPTIONS.map((o) => [o.type, o.label]));
22
22
 
23
23
  const TAB_ICONS: Record<TabType, React.ElementType> = {
24
- terminal: Terminal, chat: MessageSquare, editor: FileCode, sqlite: Database, postgres: Database,
24
+ terminal: Terminal, chat: MessageSquare, editor: FileCode, database: Database, sqlite: Database, postgres: Database,
25
25
  "git-graph": GitBranch, "git-diff": FileDiff, settings: Settings,
26
26
  };
27
27
 
@@ -97,8 +97,7 @@ export function Sidebar() {
97
97
  : "border-transparent text-text-secondary hover:text-foreground",
98
98
  )}
99
99
  >
100
- <Icon className="size-3.5" />
101
- {sidebarWidth >= 240 && <span>{tab.label}</span>}
100
+ <Icon className="size-3.5" title={tab.label} />
102
101
  </button>
103
102
  );
104
103
  })}
@@ -22,6 +22,7 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
22
22
  terminal: Terminal,
23
23
  chat: MessageSquare,
24
24
  editor: FileCode,
25
+ database: Database,
25
26
  sqlite: Database,
26
27
  postgres: Database,
27
28
  "git-graph": GitBranch,
@@ -18,6 +18,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
18
18
  default: m.CodeEditor,
19
19
  })),
20
20
  ),
21
+ database: lazy(() =>
22
+ import("@/components/database/database-viewer").then((m) => ({
23
+ default: m.DatabaseViewer,
24
+ })),
25
+ ),
21
26
  sqlite: lazy(() =>
22
27
  import("@/components/sqlite/sqlite-viewer").then((m) => ({
23
28
  default: m.SqliteViewer,
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useMemo } from "react";
1
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
2
  import { Database, Loader2, AlertCircle, Play, ChevronLeft, ChevronRight, Table, RefreshCw } from "lucide-react";
3
3
  import { useReactTable, getCoreRowModel, flexRender, type ColumnDef } from "@tanstack/react-table";
4
4
  import CodeMirror from "@uiw/react-codemirror";
@@ -15,7 +15,7 @@ export function PostgresViewer({ metadata }: Props) {
15
15
  // When connectionId present, the hook auto-connects — skip connection form
16
16
  if (!pg.connected) return <ConnectionForm initialValue={initialConn} onConnect={pg.connect} loading={pg.loading} error={pg.error} />;
17
17
 
18
- return <ConnectedView pg={pg} initialTable={metadata?.tableName as string | undefined} />;
18
+ return <ConnectedView pg={pg} initialTable={metadata?.tableName as string | undefined} hideTableList={!!connectionId} connectionName={metadata?.connectionName as string | undefined} />;
19
19
  }
20
20
 
21
21
  /* ---------- Connection Form ---------- */
@@ -44,45 +44,53 @@ function ConnectionForm({ initialValue, onConnect, loading, error }: {
44
44
  }
45
45
 
46
46
  /* ---------- Connected View ---------- */
47
- function ConnectedView({ pg, initialTable }: { pg: ReturnType<typeof usePostgres>; initialTable?: string }) {
47
+ function ConnectedView({ pg, initialTable, hideTableList, connectionName }: { pg: ReturnType<typeof usePostgres>; initialTable?: string; hideTableList?: boolean; connectionName?: string }) {
48
48
  const [queryPanelOpen, setQueryPanelOpen] = useState(false);
49
49
 
50
- // Jump to initial table from sidebar click
51
- const [didInit, setDidInit] = useState(false);
52
- if (initialTable && !didInit && pg.tables.length > 0 && pg.selectedTable !== initialTable) {
53
- const t = pg.tables.find((t) => t.name === initialTable);
54
- if (t) { setDidInit(true); pg.selectTable(t.name, t.schema); }
55
- }
50
+ // Jump to initial table when hideTableList, go direct (skip table list fetch)
51
+ const didInit = useRef(false);
52
+ useEffect(() => {
53
+ if (!initialTable || didInit.current) return;
54
+ if (hideTableList && pg.connected) {
55
+ didInit.current = true;
56
+ pg.selectTable(initialTable);
57
+ } else if (pg.tables.length > 0) {
58
+ const t = pg.tables.find((t) => t.name === initialTable);
59
+ if (t) { didInit.current = true; pg.selectTable(t.name, t.schema); }
60
+ }
61
+ }, [initialTable, pg.connected, pg.tables]); // eslint-disable-line react-hooks/exhaustive-deps
56
62
 
57
63
  return (
58
64
  <div className="flex h-full w-full overflow-hidden">
59
- {/* Table sidebar */}
60
- <div className="w-48 shrink-0 flex flex-col bg-background overflow-hidden">
61
- <div className="flex items-center justify-between px-3 py-2 border-b border-border">
62
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Tables</span>
63
- <button type="button" onClick={pg.refreshTables} className="text-muted-foreground hover:text-foreground transition-colors" title="Refresh">
64
- <RefreshCw className="size-3" />
65
- </button>
66
- </div>
67
- <div className="flex-1 overflow-y-auto">
68
- {pg.tables.map((t) => (
69
- <button key={`${t.schema}.${t.name}`} type="button" onClick={() => pg.selectTable(t.name, t.schema)}
70
- className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors ${
71
- pg.selectedTable === t.name ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"}`}>
72
- <Table className="size-3 shrink-0" />
73
- <span className="truncate flex-1">{t.schema !== "public" ? `${t.schema}.` : ""}{t.name}</span>
74
- <span className="text-[10px] opacity-60">{t.rowCount}</span>
65
+ {/* Table sidebar — hidden when opened from database sidebar */}
66
+ {!hideTableList && (
67
+ <div className="w-48 shrink-0 flex flex-col bg-background overflow-hidden">
68
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border">
69
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Tables</span>
70
+ <button type="button" onClick={pg.refreshTables} className="text-muted-foreground hover:text-foreground transition-colors" title="Refresh">
71
+ <RefreshCw className="size-3" />
75
72
  </button>
76
- ))}
77
- {pg.tables.length === 0 && <p className="px-3 py-4 text-xs text-muted-foreground text-center">No tables found</p>}
73
+ </div>
74
+ <div className="flex-1 overflow-y-auto">
75
+ {pg.tables.map((t) => (
76
+ <button key={`${t.schema}.${t.name}`} type="button" onClick={() => pg.selectTable(t.name, t.schema)}
77
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors ${
78
+ pg.selectedTable === t.name ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"}`}>
79
+ <Table className="size-3 shrink-0" />
80
+ <span className="truncate flex-1">{t.schema !== "public" ? `${t.schema}.` : ""}{t.name}</span>
81
+ <span className="text-[10px] opacity-60">{t.rowCount}</span>
82
+ </button>
83
+ ))}
84
+ {pg.tables.length === 0 && <p className="px-3 py-4 text-xs text-muted-foreground text-center">No tables found</p>}
85
+ </div>
78
86
  </div>
79
- </div>
87
+ )}
80
88
 
81
89
  {/* Main area */}
82
- <div className="flex-1 flex flex-col overflow-hidden border-l border-border">
90
+ <div className={`flex-1 flex flex-col overflow-hidden ${!hideTableList ? "border-l border-border" : ""}`}>
83
91
  <div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-background shrink-0">
84
92
  <Database className="size-3.5 text-muted-foreground" />
85
- <span className="text-xs text-muted-foreground truncate">PostgreSQL</span>
93
+ <span className="text-xs text-muted-foreground truncate">{connectionName ?? "PostgreSQL"}</span>
86
94
  {pg.selectedTable && <span className="text-xs text-muted-foreground">/ {pg.selectedTable}</span>}
87
95
  <div className="ml-auto">
88
96
  <button type="button" onClick={() => setQueryPanelOpen((v) => !v)}
@@ -50,12 +50,9 @@ export function usePostgres(connectionId?: number) {
50
50
  if (unifiedBase) {
51
51
  setLoading(true);
52
52
  try {
53
- const data = await api.get<PgTableInfo[]>(`${unifiedBase}/tables`);
53
+ // Use cached tables (no live DB query) — sidebar handles sync
54
+ const data = await api.get<PgTableInfo[]>(`${unifiedBase}/tables?cached=1`);
54
55
  setTables(data);
55
- if (data.length > 0 && !selectedTable) {
56
- setSelectedTable(data[0]!.name);
57
- setSelectedSchema(data[0]!.schema ?? "public");
58
- }
59
56
  } catch (e) { setError((e as Error).message); }
60
57
  finally { setLoading(false); }
61
58
  return;
@@ -20,7 +20,6 @@ const EFFORT_OPTIONS = [
20
20
  { value: "low", label: "Low" },
21
21
  { value: "medium", label: "Medium" },
22
22
  { value: "high", label: "High" },
23
- { value: "max", label: "Max" },
24
23
  ];
25
24
 
26
25
  export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useState, useEffect, useRef } from "react";
2
2
  import { Database, Loader2, AlertCircle } from "lucide-react";
3
3
  import { useSqlite } from "./use-sqlite";
4
4
  import { SqliteTableList } from "./sqlite-table-list";
@@ -24,9 +24,11 @@ export function SqliteViewer({ metadata }: SqliteViewerProps) {
24
24
  projectName=""
25
25
  dbPath=""
26
26
  connectionId={connectionId}
27
+ connectionName={metadata?.connectionName as string | undefined}
27
28
  initialTable={initialTable}
28
29
  queryPanelOpen={queryPanelOpen}
29
30
  onToggleQueryPanel={() => setQueryPanelOpen((v) => !v)}
31
+ hideTableList
30
32
  />
31
33
  );
32
34
  }
@@ -50,19 +52,21 @@ export function SqliteViewer({ metadata }: SqliteViewerProps) {
50
52
  }
51
53
 
52
54
  function SqliteViewerInner({
53
- projectName, dbPath, connectionId, initialTable, queryPanelOpen, onToggleQueryPanel,
55
+ projectName, dbPath, connectionId, connectionName, initialTable, queryPanelOpen, onToggleQueryPanel, hideTableList,
54
56
  }: {
55
- projectName: string; dbPath: string; connectionId?: number; initialTable?: string;
56
- queryPanelOpen: boolean; onToggleQueryPanel: () => void;
57
+ projectName: string; dbPath: string; connectionId?: number; connectionName?: string; initialTable?: string;
58
+ queryPanelOpen: boolean; onToggleQueryPanel: () => void; hideTableList?: boolean;
57
59
  }) {
58
60
  const sqlite = useSqlite(projectName, dbPath, connectionId);
59
61
 
60
62
  // Jump to initial table from sidebar click
61
- const [didInit, setDidInit] = useState(false);
62
- if (initialTable && !didInit && sqlite.tables.length > 0 && sqlite.selectedTable !== initialTable) {
63
- setDidInit(true);
64
- sqlite.selectTable(initialTable);
65
- }
63
+ const didInit = useRef(false);
64
+ useEffect(() => {
65
+ if (initialTable && !didInit.current && sqlite.tables.length > 0) {
66
+ didInit.current = true;
67
+ sqlite.selectTable(initialTable);
68
+ }
69
+ }, [initialTable, sqlite.tables]); // eslint-disable-line react-hooks/exhaustive-deps
66
70
 
67
71
  if (sqlite.error && sqlite.tables.length === 0) {
68
72
  return (
@@ -84,20 +88,22 @@ function SqliteViewerInner({
84
88
 
85
89
  return (
86
90
  <div className="flex h-full w-full overflow-hidden">
87
- {/* Left sidebar — table list */}
88
- <SqliteTableList
89
- tables={sqlite.tables}
90
- selectedTable={sqlite.selectedTable}
91
- onSelect={sqlite.selectTable}
92
- onRefresh={sqlite.refreshTables}
93
- />
91
+ {/* Left sidebar — table list (hidden when opened from database sidebar) */}
92
+ {!hideTableList && (
93
+ <SqliteTableList
94
+ tables={sqlite.tables}
95
+ selectedTable={sqlite.selectedTable}
96
+ onSelect={sqlite.selectTable}
97
+ onRefresh={sqlite.refreshTables}
98
+ />
99
+ )}
94
100
 
95
101
  {/* Main content area */}
96
- <div className="flex-1 flex flex-col overflow-hidden border-l border-border">
102
+ <div className={`flex-1 flex flex-col overflow-hidden ${!hideTableList ? "border-l border-border" : ""}`}>
97
103
  {/* Toolbar */}
98
104
  <div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-background shrink-0">
99
105
  <Database className="size-3.5 text-muted-foreground" />
100
- <span className="text-xs text-muted-foreground truncate">{dbPath}</span>
106
+ <span className="text-xs text-muted-foreground truncate">{connectionName ?? dbPath}</span>
101
107
  <span className="text-xs text-muted-foreground">
102
108
  {sqlite.selectedTable && `/ ${sqlite.selectedTable}`}
103
109
  </span>
@@ -23,21 +23,21 @@ export function useSqlite(projectName: string, dbPath: string, connectionId?: nu
23
23
  const base = unifiedBase ?? `${projectUrl(projectName)}/sqlite`;
24
24
  const qs = unifiedBase ? "" : `path=${encodeURIComponent(dbPath)}`;
25
25
 
26
- // Fetch tables on mount
26
+ // Fetch tables on mount — use cache when connectionId (sidebar handles live sync)
27
27
  const fetchTables = useCallback(async () => {
28
28
  setLoading(true);
29
29
  setError(null);
30
30
  try {
31
- const qsPart = qs ? `?${qs}` : "";
31
+ const qsPart = unifiedBase ? "?cached=1" : qs ? `?${qs}` : "";
32
32
  const data = await api.get<TableInfo[]>(`${base}/tables${qsPart}`);
33
33
  setTables(data);
34
- if (data.length > 0 && !selectedTable) setSelectedTable(data[0]!.name);
34
+ if (!unifiedBase && data.length > 0 && !selectedTable) setSelectedTable(data[0]!.name);
35
35
  } catch (e) {
36
36
  setError((e as Error).message);
37
37
  } finally {
38
38
  setLoading(false);
39
39
  }
40
- }, [base, qs]); // eslint-disable-line react-hooks/exhaustive-deps
40
+ }, [base, qs, unifiedBase]); // eslint-disable-line react-hooks/exhaustive-deps
41
41
 
42
42
  useEffect(() => { fetchTables(); }, [fetchTables]);
43
43
 
@@ -33,7 +33,7 @@ interface UseChatReturn {
33
33
  isConnected: boolean;
34
34
  }
35
35
 
36
- export function useChat(sessionId: string | null, providerId = "claude-sdk", projectName = ""): UseChatReturn {
36
+ export function useChat(sessionId: string | null, providerId = "claude", projectName = ""): UseChatReturn {
37
37
  const [messages, setMessages] = useState<ChatMessage[]>([]);
38
38
  const [messagesLoading, setMessagesLoading] = useState(false);
39
39
  const [isStreaming, setIsStreaming] = useState(false);
@@ -12,7 +12,7 @@ interface UseUsageReturn {
12
12
  refreshUsage: () => void;
13
13
  }
14
14
 
15
- export function useUsage(projectName: string, providerId = "claude-sdk"): UseUsageReturn {
15
+ export function useUsage(projectName: string, providerId = "claude"): UseUsageReturn {
16
16
  const [usageInfo, setUsageInfo] = useState<UsageInfo>({});
17
17
  const [usageLoading, setUsageLoading] = useState(false);
18
18
  const [lastFetchedAt, setLastFetchedAt] = useState<string | null>(null);
@@ -5,6 +5,7 @@ export type TabType =
5
5
  | "terminal"
6
6
  | "chat"
7
7
  | "editor"
8
+ | "database"
8
9
  | "sqlite"
9
10
  | "postgres"
10
11
  | "git-graph"