@hienlh/ppm 0.9.67 → 0.9.69

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 (33) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/web/assets/{chat-tab-onkz52iv.js → chat-tab-rHbqenEa.js} +1 -1
  3. package/dist/web/assets/code-editor-C34pPIaA.js +8 -0
  4. package/dist/web/assets/database-viewer-8Eifo5Ak.js +2 -0
  5. package/dist/web/assets/{diff-viewer-eFO08m_L.js → diff-viewer-CrUaMMwc.js} +1 -1
  6. package/dist/web/assets/{extension-webview-4CL9kCKR.js → extension-webview-EJvlbzMw.js} +1 -1
  7. package/dist/web/assets/{git-graph-BqeE_o17.js → git-graph-boq7GWXZ.js} +1 -1
  8. package/dist/web/assets/index-C4vNxaKi.css +2 -0
  9. package/dist/web/assets/{index-DwrCg0TN.js → index-DHr-SBBY.js} +4 -4
  10. package/dist/web/assets/keybindings-store-CWfygy_t.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-BUqab2os.js → markdown-renderer-BrW4qEBv.js} +1 -1
  12. package/dist/web/assets/{port-forwarding-tab-CfO-UJ84.js → port-forwarding-tab-UB_l73FZ.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-BVJZ44eU.js → postgres-viewer-BZE43hDP.js} +1 -1
  14. package/dist/web/assets/{settings-tab-C6hdJujW.js → settings-tab-DPx50bZm.js} +1 -1
  15. package/dist/web/assets/{sql-query-editor-OhZa4Z9F.js → sql-query-editor-CPBBvi1U.js} +1 -1
  16. package/dist/web/assets/{sqlite-viewer-C8p1_jz4.js → sqlite-viewer-DbHvVTSs.js} +1 -1
  17. package/dist/web/assets/{terminal-tab-CaO0WnIo.js → terminal-tab-Br_Zlw2Z.js} +1 -1
  18. package/dist/web/assets/{use-monaco-theme-U9ZhfvHB.js → use-monaco-theme-DDZwQjEv.js} +1 -1
  19. package/dist/web/index.html +2 -2
  20. package/dist/web/sw.js +1 -1
  21. package/package.json +1 -1
  22. package/src/providers/claude-agent-sdk.ts +33 -25
  23. package/src/services/account.service.ts +6 -3
  24. package/src/services/cloud-ws.service.ts +6 -1
  25. package/src/web/components/database/connection-list.tsx +16 -10
  26. package/src/web/components/database/data-grid.tsx +28 -37
  27. package/src/web/components/database/database-viewer.tsx +27 -5
  28. package/src/web/components/database/use-database.ts +16 -1
  29. package/src/web/components/editor/code-editor.tsx +30 -30
  30. package/dist/web/assets/code-editor-BixOXePn.js +0 -8
  31. package/dist/web/assets/database-viewer-DfLe8ewt.js +0 -2
  32. package/dist/web/assets/index-BWLy2h18.css +0 -2
  33. package/dist/web/assets/keybindings-store-BNBONtSd.js +0 -1
@@ -710,16 +710,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
710
710
  crashRetryLoop: for (;;) {
711
711
  try {
712
712
  // Streaming input: create message channel and persistent query
713
- const { generator: streamGen, controller: streamCtrl } = createMessageChannel();
714
713
  const firstMsg = {
715
714
  type: 'user' as const,
716
715
  message: buildMessageParam(message),
717
716
  parent_tool_use_id: null,
718
717
  session_id: sessionId,
719
718
  };
720
- streamCtrl.push(firstMsg);
721
719
 
722
- const q = query({
720
+ const { generator: streamGen, controller: initialCtrl } = createMessageChannel();
721
+ initialCtrl.push(firstMsg);
722
+
723
+ const initialQuery = query({
723
724
  prompt: streamGen,
724
725
  options: {
725
726
  ...queryOptions,
@@ -727,9 +728,19 @@ export class ClaudeAgentSdkProvider implements AIProvider {
727
728
  canUseTool,
728
729
  } as any,
729
730
  });
730
- this.streamingSessions.set(sessionId, { meta, query: q, controller: streamCtrl });
731
- this.activeQueries.set(sessionId, q);
732
- let eventSource: AsyncIterable<any> = q;
731
+ this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl });
732
+ this.activeQueries.set(sessionId, initialQuery);
733
+ let eventSource: AsyncIterable<any> = initialQuery;
734
+
735
+ // Helper: close the CURRENT streaming session (not stale closure refs).
736
+ // All retry paths must use this instead of closing captured variables directly.
737
+ const closeCurrentStream = () => {
738
+ const ss = this.streamingSessions.get(sessionId);
739
+ if (ss) {
740
+ ss.controller.done();
741
+ ss.query.close();
742
+ }
743
+ };
733
744
 
734
745
  let lastPartialText = "";
735
746
  /** Number of tool_use blocks pending results (top-level tools only, not subagent children) */
@@ -759,9 +770,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
759
770
  if ((msg as any).type === "result" && (msg as any).subtype === "error_during_execution" && ((msg as any).num_turns ?? 0) === 0 && retryCount < MAX_RETRIES) {
760
771
  retryCount++;
761
772
  console.warn(`[sdk] transient error on first event — retrying (attempt ${retryCount}/${MAX_RETRIES})`);
762
- // Close failed query and old channel, create new channel + query for retry
763
- streamCtrl.done();
764
- q.close();
773
+ // Close current streaming session (uses streamingSessions, not stale closure refs)
774
+ closeCurrentStream();
765
775
  const { generator: retryGen, controller: retryCtrl } = createMessageChannel();
766
776
  retryCtrl.push(firstMsg);
767
777
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: undefined };
@@ -809,9 +819,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
809
819
  } else {
810
820
  console.log(`[sdk] session=${sessionId} ignoring retry init sdk_id=${initMsg.session_id} (mapping already set)`);
811
821
  }
812
- const oldMeta = this.activeSessions.get(sessionId);
813
- if (oldMeta) {
814
- this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
822
+ // Only create activeSessions alias for first-time SDK id mapping.
823
+ // Retry init events create phantom entries that pollute the map.
824
+ if (isFirstMessage) {
825
+ const oldMeta = this.activeSessions.get(sessionId);
826
+ if (oldMeta) {
827
+ this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
828
+ }
815
829
  }
816
830
  }
817
831
  }
@@ -849,8 +863,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
849
863
  console.log(`[sdk] session=${sessionId} intercepted api_retry 401 — refreshing token for ${account.id} (${label})`);
850
864
  yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
851
865
  const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
852
- streamCtrl.done();
853
- q.close();
866
+ closeCurrentStream();
854
867
  const { generator: earlyAuthGen, controller: earlyAuthCtrl } = createMessageChannel();
855
868
  const currentSdkId = getSessionMapping(sessionId);
856
869
  const canResume = !!currentSdkId;
@@ -877,8 +890,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
877
890
  console.log(`[sdk] session=${sessionId} refresh failed — switching to ${nextAcc.id} (${label})`);
878
891
  yield { type: "account_retry" as const, reason: "Switching account", accountId: nextAcc.id, accountLabel: label };
879
892
  const switchEnv = this.buildQueryEnv(meta.projectPath, nextAcc);
880
- streamCtrl.done();
881
- q.close();
893
+ closeCurrentStream();
882
894
  const { generator: switchGen, controller: switchCtrl } = createMessageChannel();
883
895
  const currentSdkId = getSessionMapping(sessionId);
884
896
  const canResume = !!currentSdkId;
@@ -1030,8 +1042,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1030
1042
  // Re-resolve sdkId: the init event may have mapped ppmId → real SDK session_id
1031
1043
  // after sdkId was originally resolved. Using the stale value would try to
1032
1044
  // resume a non-existent session, causing the SDK to hang forever.
1033
- streamCtrl.done();
1034
- q.close();
1045
+ closeCurrentStream();
1035
1046
  const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
1036
1047
  const currentSdkId = getSessionMapping(sessionId);
1037
1048
  const canResume = !!currentSdkId;
@@ -1082,10 +1093,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1082
1093
  }
1083
1094
  yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
1084
1095
  await new Promise((r) => setTimeout(r, backoff));
1085
- // Close failed query and recreate with (potentially new) account env.
1096
+ // Close current streaming session and recreate with (potentially new) account env.
1086
1097
  // Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
1087
- streamCtrl.done();
1088
- q.close();
1098
+ closeCurrentStream();
1089
1099
  const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
1090
1100
  const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
1091
1101
  const rlCurrentSdkId = getSessionMapping(sessionId);
@@ -1183,8 +1193,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1183
1193
  yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
1184
1194
  await new Promise((r) => setTimeout(r, backoff));
1185
1195
  // Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
1186
- streamCtrl.done();
1187
- q.close();
1196
+ closeCurrentStream();
1188
1197
  const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
1189
1198
  const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
1190
1199
  const rlCurrentSdkId2 = getSessionMapping(sessionId);
@@ -1216,8 +1225,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1216
1225
  console.log(`[sdk] 401 in result on account ${account.id} (${label}) — token refreshed, retrying`);
1217
1226
  yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
1218
1227
  // Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
1219
- streamCtrl.done();
1220
- q.close();
1228
+ closeCurrentStream();
1221
1229
  const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
1222
1230
  const { generator: authRetryGen2, controller: authRetryCtrl2 } = createMessageChannel();
1223
1231
  const authCurrentSdkId2 = getSessionMapping(sessionId);
@@ -125,7 +125,9 @@ class AccountService {
125
125
 
126
126
  /**
127
127
  * Ensure the access token for an OAuth account is still fresh.
128
- * If it's expired or about to expire (within 60s), refresh it proactively.
128
+ * If it's expired or about to expire (within 1 hour), refresh it proactively.
129
+ * The generous buffer prevents 401 errors mid-conversation — the SDK subprocess
130
+ * may run for a long time before the token is actually sent to the API.
129
131
  * Returns the refreshed account with fresh tokens, or null if refresh failed.
130
132
  */
131
133
  async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
@@ -135,9 +137,10 @@ class AccountService {
135
137
  if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
136
138
  if (!acc.expiresAt) return acc;
137
139
  const nowS = Math.floor(Date.now() / 1000);
138
- if (acc.expiresAt - nowS > 60) return acc; // still fresh
140
+ const REFRESH_BUFFER_S = 3600; // 1 hour refresh proactively before expiry
141
+ if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return acc; // still fresh
139
142
  try {
140
- console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s)`);
143
+ console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${REFRESH_BUFFER_S}s)`);
141
144
  await this.refreshAccessToken(id, false);
142
145
  return this.getWithTokens(id);
143
146
  } catch (e) {
@@ -99,6 +99,7 @@ export function connect(opts: {
99
99
 
100
100
  export function disconnect(): void {
101
101
  shouldConnect = false;
102
+ reconnecting = false; // prevent stale flag from blocking future doConnect()
102
103
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
103
104
  if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
104
105
  if (ws) {
@@ -122,8 +123,12 @@ export function onCommand(handler: CommandHandler): void {
122
123
  commandHandler = handler;
123
124
  }
124
125
 
126
+ /** Returns true if WS is authenticated and ready, or still in auth handshake */
125
127
  export function isConnected(): boolean {
126
- return connected;
128
+ // Also treat "connecting/open but auth pending" as connected to prevent
129
+ // external monitors from killing a valid WS during the 500ms auth delay.
130
+ if (connected) return true;
131
+ return ws !== null && ws.readyState <= WebSocket.OPEN;
127
132
  }
128
133
 
129
134
  // ─── Internal ───────────────────────────────────────
@@ -88,6 +88,21 @@ export function ConnectionList({
88
88
  return a.localeCompare(b);
89
89
  });
90
90
 
91
+ // Pre-compute schemas for all connections (hooks can't be inside .map())
92
+ const schemasPerConn = useMemo(() => {
93
+ const result = new Map<number, Map<string, CachedTable[]>>();
94
+ for (const conn of connections) {
95
+ const tables = cachedTables.get(conn.id) ?? [];
96
+ const map = new Map<string, CachedTable[]>();
97
+ for (const t of tables) {
98
+ const key = t.schemaName;
99
+ (map.get(key) ?? map.set(key, []).get(key)!).push(t);
100
+ }
101
+ result.set(conn.id, map);
102
+ }
103
+ return result;
104
+ }, [connections, cachedTables]);
105
+
91
106
  if (connections.length === 0) {
92
107
  return (
93
108
  <p className="px-4 py-6 text-xs text-text-subtle text-center">
@@ -123,16 +138,7 @@ export function ConnectionList({
123
138
  const tables = cachedTables.get(conn.id) ?? [];
124
139
  const isRefreshing = refreshingIds.has(conn.id);
125
140
 
126
- // Group tables by schema for postgres
127
- const schemas = useMemo(() => {
128
- const map = new Map<string, CachedTable[]>();
129
- for (const t of tables) {
130
- const key = t.schemaName;
131
- (map.get(key) ?? map.set(key, []).get(key)!).push(t);
132
- }
133
- return map;
134
- }, [tables]);
135
-
141
+ const schemas = schemasPerConn.get(conn.id) ?? new Map();
136
142
  const isSingleSchema = schemas.size <= 1;
137
143
  const filter = tableFilter.get(conn.id) ?? "";
138
144
 
@@ -21,6 +21,8 @@ interface DataGridProps {
21
21
  selectedTable?: string | null;
22
22
  selectedSchema?: string;
23
23
  connectionName?: string;
24
+ /** Controlled column ILIKE filters — parent owns state */
25
+ columnFilters?: Record<string, string>;
24
26
  /** Called when column ILIKE filters change — parent builds WHERE clause */
25
27
  onColumnFilter?: (filters: Record<string, string>) => void;
26
28
  }
@@ -29,7 +31,7 @@ export function DataGrid({
29
31
  tableData, schema, loading, page, onPageChange, onCellUpdate, onRowDelete,
30
32
  orderBy, orderDir, onToggleSort,
31
33
  onBulkDelete, onInsertRow,
32
- connectionId, selectedTable, selectedSchema, connectionName, onColumnFilter,
34
+ connectionId, selectedTable, selectedSchema, connectionName, columnFilters: colFilters = {}, onColumnFilter,
33
35
  }: DataGridProps) {
34
36
  const [editingCell, setEditingCell] = useState<{ rowIdx: number; col: string } | null>(null);
35
37
  const [editValue, setEditValue] = useState("");
@@ -52,7 +54,6 @@ export function DataGrid({
52
54
  }, [openTab]);
53
55
  const [pinnedCols, setPinnedCols] = useState<Set<string>>(new Set());
54
56
  const [pinnedRows, setPinnedRows] = useState<Set<number>>(new Set());
55
- const [colFilters, setColFilters] = useState<Record<string, string>>({});
56
57
  const [filterOpenCol, setFilterOpenCol] = useState<string | null>(null);
57
58
 
58
59
  const pkCol = useMemo(() => {
@@ -118,20 +119,9 @@ export function DataGrid({
118
119
  }, []);
119
120
 
120
121
  const updateColFilter = useCallback((col: string, val: string) => {
121
- setColFilters((prev) => {
122
- const next = { ...prev };
123
- if (val) next[col] = val; else delete next[col];
124
- return next;
125
- });
126
- }, []);
127
-
128
- // Notify parent when column filters change
129
- const colFiltersRef = useRef(colFilters);
130
- useEffect(() => {
131
- if (colFiltersRef.current !== colFilters) {
132
- colFiltersRef.current = colFilters;
133
- onColumnFilter?.(colFilters);
134
- }
122
+ const next = { ...colFilters };
123
+ if (val) next[col] = val; else delete next[col];
124
+ onColumnFilter?.(next);
135
125
  }, [colFilters, onColumnFilter]);
136
126
 
137
127
  const toggleRowSelection = useCallback((idx: number) => {
@@ -219,24 +209,13 @@ export function DataGrid({
219
209
  return () => el.removeEventListener("keydown", handler);
220
210
  }, [tableData, selectedRows]);
221
211
 
222
- if (!tableData) {
223
- return (
224
- <div className="flex items-center justify-center h-full text-xs text-muted-foreground">
225
- {loading ? <Loader2 className="size-4 animate-spin" /> : "Select a table"}
226
- </div>
227
- );
228
- }
229
-
230
- const totalPages = Math.ceil(tableData.total / tableData.limit) || 1;
231
- const hasSelection = selectedRows.size > 0;
232
- const allSelected = selectedRows.size === tableData.rows.length && tableData.rows.length > 0;
233
-
234
212
  // Build ordered column list: pinned first, then unpinned
235
213
  const orderedCols = useMemo(() => {
214
+ if (!tableData) return [];
236
215
  const pinned = tableData.columns.filter((c) => pinnedCols.has(c));
237
216
  const unpinned = tableData.columns.filter((c) => !pinnedCols.has(c));
238
217
  return [...pinned, ...unpinned];
239
- }, [tableData.columns, pinnedCols]);
218
+ }, [tableData?.columns, pinnedCols]);
240
219
 
241
220
  // Measure actual column widths and header height from DOM
242
221
  const theadRef = useRef<HTMLTableSectionElement>(null);
@@ -248,12 +227,10 @@ export function DataGrid({
248
227
  if (!thead) return;
249
228
  const measure = () => {
250
229
  setHeaderHeight(thead.offsetHeight);
251
- // Measure each <th> by data-col attribute
252
230
  const widths = new Map<string, number>();
253
231
  thead.querySelectorAll<HTMLElement>("th[data-col]").forEach((th) => {
254
232
  widths.set(th.dataset.col!, th.offsetWidth);
255
233
  });
256
- // Also measure checkbox th
257
234
  const cbTh = thead.querySelector<HTMLElement>("th[data-col='_cb']");
258
235
  if (cbTh) widths.set("_cb", cbTh.offsetWidth);
259
236
  setColWidths(widths);
@@ -262,7 +239,7 @@ export function DataGrid({
262
239
  const obs = new ResizeObserver(measure);
263
240
  obs.observe(thead);
264
241
  return () => obs.disconnect();
265
- }, [tableData?.columns, pinnedCols]); // re-measure when columns or pins change
242
+ }, [tableData?.columns, pinnedCols]);
266
243
 
267
244
  // Compute sticky left offsets from measured widths
268
245
  const pinnedColOffsets = useMemo(() => {
@@ -290,13 +267,11 @@ export function DataGrid({
290
267
  else pinnedRowRefs.current.delete(idx);
291
268
  }, []);
292
269
 
293
- // Measure pinned rows after render
294
270
  useEffect(() => {
295
271
  if (pinnedRowData.length === 0) {
296
272
  if (pinnedRowHeights.size > 0) setPinnedRowHeights(new Map());
297
273
  return;
298
274
  }
299
- // Use rAF to measure after browser layout
300
275
  const id = requestAnimationFrame(() => {
301
276
  const heights = new Map<number, number>();
302
277
  for (const { idx } of pinnedRowData) {
@@ -308,17 +283,28 @@ export function DataGrid({
308
283
  return () => cancelAnimationFrame(id);
309
284
  }, [pinnedRowData, tableData]); // eslint-disable-line react-hooks/exhaustive-deps
310
285
 
311
- // Compute cumulative sticky top for each pinned row
312
286
  const pinnedRowTops = useMemo(() => {
313
287
  const tops = new Map<number, number>();
314
288
  let cumTop = headerHeight;
315
289
  for (const { idx } of pinnedRowData) {
316
290
  tops.set(idx, cumTop);
317
- cumTop += pinnedRowHeights.get(idx) ?? 28; // fallback 28 for first render
291
+ cumTop += pinnedRowHeights.get(idx) ?? 28;
318
292
  }
319
293
  return tops;
320
294
  }, [headerHeight, pinnedRowData, pinnedRowHeights]);
321
295
 
296
+ if (!tableData) {
297
+ return (
298
+ <div className="flex items-center justify-center h-full text-xs text-muted-foreground">
299
+ {loading ? <Loader2 className="size-4 animate-spin" /> : "Select a table"}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ const totalPages = Math.ceil(tableData.total / tableData.limit) || 1;
305
+ const hasSelection = selectedRows.size > 0;
306
+ const allSelected = selectedRows.size === tableData.rows.length && tableData.rows.length > 0;
307
+
322
308
  return (
323
309
  <div ref={containerRef} tabIndex={0} className="flex flex-col h-full overflow-hidden outline-none">
324
310
  {/* Search + bulk actions toolbar */}
@@ -391,7 +377,12 @@ export function DataGrid({
391
377
  )}
392
378
 
393
379
  {/* Table */}
394
- <div className="flex-1 overflow-auto">
380
+ <div className="flex-1 overflow-auto relative">
381
+ {loading && (
382
+ <div className="absolute inset-0 z-30 flex items-center justify-center bg-background/60">
383
+ <Loader2 className="size-5 animate-spin text-primary" />
384
+ </div>
385
+ )}
395
386
  <table className="w-full text-xs" style={{ borderCollapse: "separate", borderSpacing: 0 }}>
396
387
  <thead ref={theadRef} className="sticky top-0 z-20 bg-muted">
397
388
  <tr>
@@ -7,6 +7,17 @@ import { ExportButton } from "./export-button";
7
7
  import { DataGrid } from "./data-grid";
8
8
  import type { SchemaInfo } from "./sql-completion-provider";
9
9
 
10
+ /** Parse WHERE "col" ILIKE '%val%' clauses from SQL */
11
+ function parseSqlFilters(sql: string): Record<string, string> {
12
+ const filters: Record<string, string> = {};
13
+ const re = /"(\w+)"\s+ILIKE\s+'%([^']*?)%'/gi;
14
+ let m: RegExpExecArray | null;
15
+ while ((m = re.exec(sql)) !== null) {
16
+ filters[m[1]!] = m[2]!.replace(/''/g, "'");
17
+ }
18
+ return filters;
19
+ }
20
+
10
21
  interface Props { metadata?: Record<string, unknown>; tabId?: string }
11
22
 
12
23
  /** Generic database viewer — works for any DB type via unified API */
@@ -52,9 +63,8 @@ export function DatabaseViewer({ metadata }: Props) {
52
63
  if (!db.selectedTable || Object.keys(columnFilters).length === 0) return;
53
64
  clearTimeout(filterTimerRef.current);
54
65
  filterTimerRef.current = setTimeout(() => {
55
- // Execute the current defaultQuery which already includes filters
56
- db.executeQuery(defaultQuery);
57
- setShowingQueryResult(true);
66
+ // Execute filter query into tableData stays in table grid mode
67
+ db.queryAsTable(defaultQuery);
58
68
  }, 500);
59
69
  return () => clearTimeout(filterTimerRef.current);
60
70
  }, [columnFilters]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -110,9 +120,21 @@ export function DatabaseViewer({ metadata }: Props) {
110
120
  // Track whether user ran a custom query (show results instead of table grid)
111
121
  const [showingQueryResult, setShowingQueryResult] = useState(!!initialSql);
112
122
  const handleExecuteQuery = useCallback((sql: string) => {
123
+ const trimmed = sql.trim();
124
+ // Check if query is a simple SELECT on the current table — stay in table grid mode
125
+ if (db.selectedTable) {
126
+ const tablePattern = new RegExp(`^SELECT\\s+\\*\\s+FROM\\s+"?${db.selectedTable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"?\\b`, "i");
127
+ if (tablePattern.test(trimmed)) {
128
+ // Parse ILIKE filters from SQL and sync to columnFilters
129
+ const parsed = parseSqlFilters(trimmed);
130
+ setColumnFilters(parsed);
131
+ db.queryAsTable(trimmed);
132
+ return;
133
+ }
134
+ }
113
135
  setShowingQueryResult(true);
114
136
  db.executeQuery(sql);
115
- }, [db.executeQuery]);
137
+ }, [db.executeQuery, db.queryAsTable, db.selectedTable]);
116
138
 
117
139
  // When user interacts with DataGrid (sort/page), switch back to table view
118
140
  const handleToggleSort = useCallback((col: string) => {
@@ -173,7 +195,7 @@ export function DatabaseViewer({ metadata }: Props) {
173
195
  orderBy={db.orderBy} orderDir={db.orderDir} onToggleSort={handleToggleSort}
174
196
  onBulkDelete={db.bulkDelete} onInsertRow={db.insertRow}
175
197
  connectionId={connectionId} selectedTable={db.selectedTable} selectedSchema={db.selectedSchema}
176
- connectionName={connectionName} onColumnFilter={handleColumnFilter} />
198
+ connectionName={connectionName} columnFilters={columnFilters} onColumnFilter={handleColumnFilter} />
177
199
  )}
178
200
 
179
201
  {showQueryResults && (
@@ -173,12 +173,27 @@ export function useDatabase(connectionId: number) {
173
173
  }
174
174
  }, [base, selectedTable, selectedSchema, fetchTableData]);
175
175
 
176
+ /** Execute SQL but put results into tableData (for column filters) */
177
+ const queryAsTable = useCallback(async (sqlText: string) => {
178
+ setLoading(true);
179
+ try {
180
+ const result = await api.post<DbQueryResult>(`${base}/query`, { sql: sqlText });
181
+ if (result.changeType === "select") {
182
+ setTableData({ columns: result.columns, rows: result.rows, total: result.rows.length, page: 1, limit: result.rows.length });
183
+ }
184
+ } catch (e) {
185
+ setError((e as Error).message);
186
+ } finally {
187
+ setLoading(false);
188
+ }
189
+ }, [base]);
190
+
176
191
  return {
177
192
  selectedTable, selectedSchema, selectTable, tableData, schema,
178
193
  loading, error, page, setPage: changePage,
179
194
  orderBy, orderDir, toggleSort,
180
195
  queryResult, queryError, queryLoading, executeQuery,
181
196
  updateCell, deleteRow, bulkDelete, insertRow,
182
- refreshData: fetchTableData,
197
+ refreshData: fetchTableData, queryAsTable,
183
198
  };
184
199
  }
@@ -86,6 +86,36 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
86
86
 
87
87
  const selectedSqlConn = useMemo(() => connections.find((c) => c.id === sqlConnId) ?? null, [connections, sqlConnId]);
88
88
 
89
+ // Beautify for inline content (must be before early returns to maintain hook order)
90
+ const canBeautifyInline = inlineContent != null && (inlineLanguage === "json" || inlineLanguage === "xml");
91
+ const [isBeautified, setIsBeautified] = useState(false);
92
+ const handleBeautifyInline = useCallback(() => {
93
+ if (!inlineContent) return;
94
+ if (isBeautified) {
95
+ setContent(inlineContent);
96
+ setIsBeautified(false);
97
+ } else {
98
+ const trimmed = inlineContent.trimStart();
99
+ if (inlineLanguage === "json") {
100
+ try { setContent(JSON.stringify(JSON.parse(trimmed), null, 2)); setIsBeautified(true); } catch { /* not valid */ }
101
+ } else if (inlineLanguage === "xml") {
102
+ let indent = 0;
103
+ const formatted = trimmed.replace(/(>)(<)(\/*)/g, "$1\n$2$3")
104
+ .split("\n")
105
+ .map((line) => {
106
+ const l = line.trim();
107
+ if (l.startsWith("</")) indent = Math.max(0, indent - 1);
108
+ const padded = " ".repeat(indent) + l;
109
+ if (l.startsWith("<") && !l.startsWith("</") && !l.endsWith("/>") && !l.includes("</")) indent++;
110
+ return padded;
111
+ })
112
+ .join("\n");
113
+ setContent(formatted);
114
+ setIsBeautified(true);
115
+ }
116
+ }
117
+ }, [inlineContent, inlineLanguage, isBeautified]);
118
+
89
119
  // Persist selected connection per file
90
120
  const handleSqlConnChange = useCallback((connId: number) => {
91
121
  setSqlConnId(connId);
@@ -362,36 +392,6 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
362
392
  </div>
363
393
  ) : null;
364
394
 
365
- // Beautify for inline content
366
- const canBeautifyInline = inlineContent != null && (inlineLanguage === "json" || inlineLanguage === "xml");
367
- const [isBeautified, setIsBeautified] = useState(false);
368
- const handleBeautifyInline = useCallback(() => {
369
- if (!inlineContent) return;
370
- if (isBeautified) {
371
- setContent(inlineContent);
372
- setIsBeautified(false);
373
- } else {
374
- const trimmed = inlineContent.trimStart();
375
- if (inlineLanguage === "json") {
376
- try { setContent(JSON.stringify(JSON.parse(trimmed), null, 2)); setIsBeautified(true); } catch { /* not valid */ }
377
- } else if (inlineLanguage === "xml") {
378
- let indent = 0;
379
- const formatted = trimmed.replace(/(>)(<)(\/*)/g, "$1\n$2$3")
380
- .split("\n")
381
- .map((line) => {
382
- const l = line.trim();
383
- if (l.startsWith("</")) indent = Math.max(0, indent - 1);
384
- const padded = " ".repeat(indent) + l;
385
- if (l.startsWith("<") && !l.startsWith("</") && !l.endsWith("/>") && !l.includes("</")) indent++;
386
- return padded;
387
- })
388
- .join("\n");
389
- setContent(formatted);
390
- setIsBeautified(true);
391
- }
392
- }
393
- }, [inlineContent, inlineLanguage, isBeautified]);
394
-
395
395
  return (
396
396
  <div className="flex flex-col h-full w-full overflow-hidden">
397
397
  {/* Inline content toolbar (cell viewer mode) */}
@@ -1,8 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/csv-preview-D2pJJj3K.js","assets/chunk-CFjPhJqf.js","assets/lib-DurwGtQO.js","assets/react-nm2Ru1Pt.js","assets/createLucideIcon-PuMiQgHl.js","assets/arrow-up-BYhx9ckd.js","assets/react-dom-Bpkvzu3U.js","assets/jsx-runtime-kMwlnEGE.js","assets/csv-parser-CNNw2RVA.js"])))=>i.map(i=>d[i]);
2
- import{o as e}from"./chunk-CFjPhJqf.js";import{t}from"./react-nm2Ru1Pt.js";import"./react-dom-Bpkvzu3U.js";import{t as n}from"./createLucideIcon-PuMiQgHl.js";import{t as r}from"./chevron-right-4zq1jPv6.js";import{n as i,t as a}from"./markdown-renderer-BUqab2os.js";import{t as o}from"./table-DFevCOMd.js";import{t as s}from"./text-wrap-BWNOVswA.js";import{t as c}from"./preload-helper-Bf_JiD2A.js";import{t as l}from"./jsx-runtime-kMwlnEGE.js";import"./dist-DIV6WgAG.js";import{t as u}from"./utils-BNytJOb1.js";import{i as d,r as f,t as p}from"./api-client-BfBM3I7n.js";import{Ct as m,Et as ee,K as h,R as g,S as _,St as v,Tt as y,Y as te,_ as b,_t as x,a as ne,at as re,b as S,bt as C,g as w,pt as T,v as E,vt as D,w as ie,wt as O,x as k,xt as A,y as j,yt as M}from"./index-DwrCg0TN.js";import"./chunk-GEFDOKGD-D-pKjlVd.js";import"./src-BqX54PbV.js";import"./chunk-7R4GIKGN-Dv-4cAYn.js";import"./chunk-HHEYEP7N-C7vxA5i9.js";import"./dist-CSJdAyA9.js";import"./chunk-PU5JKC2W-ek7k4QVB.js";import"./chunk-MX3YWQON-BpS_PtKp.js";import"./chunk-YBOYWFTD-rQG3QH5s.js";import"./chunk-PQ6SQG4A-TF58UVMU.js";import"./chunk-KYZI473N-Bb0MCaIO.js";import"./chunk-O4XLMI2P-nDhi_cVu.js";import"./chunk-GLR3WWYH-DKikpoJM.js";import"./chunk-XPW4576I-BPQQBakK.js";import{n as ae,t as oe}from"./use-monaco-theme-U9ZhfvHB.js";import{n as se,t as ce}from"./sql-completion-provider-DM9Qov6L.js";var N=n(`file-exclamation-point`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),P=e(t(),1),F=l(),le={ts:A,tsx:A,js:A,jsx:A,py:A,rs:A,go:A,html:A,css:A,scss:A,json:v,md:C,txt:C,yaml:M,yml:M};function ue(e,t){return t?x:le[e.split(`.`).pop()?.toLowerCase()??``]??D}function I(e,t){let n=[],r=e;for(let e=0;e<t.length;e++){let i=t[e],a=t.slice(0,e+1).join(`/`),o=r.find(e=>e.name===i);if(n.push({name:i,fullPath:a,node:o??null,siblings:r}),o?.children)r=o.children;else{for(let r=e+1;r<t.length;r++)n.push({name:t[r],fullPath:t.slice(0,r+1).join(`/`),node:null,siblings:[]});break}}return n}function L(e){return[...e].sort((e,t)=>e.type===t.type?e.name.localeCompare(t.name):e.type===`directory`?-1:1)}function de({filePath:e,projectName:t,tabId:n,className:i}){let a=g(e=>e.tree),{updateTab:o,openTab:s}=h(),c=(0,P.useRef)(null),l=(0,P.useMemo)(()=>I(a,e.split(`/`).filter(Boolean)),[a,e]);(0,P.useEffect)(()=>{c.current&&(c.current.scrollLeft=c.current.scrollWidth)},[l]);function d(e,r){let i=u(e);r.metaKey||r.ctrlKey?s({type:`editor`,title:i,metadata:{filePath:e,projectName:t},projectId:t,closable:!0}):o(n,{title:i,metadata:{filePath:e,projectName:t}})}return(0,F.jsx)(`div`,{ref:c,className:i,children:l.map((e,n)=>(0,F.jsxs)(`div`,{className:`flex items-center shrink-0`,children:[n>0&&(0,F.jsx)(r,{className:`size-3 text-muted-foreground shrink-0 mx-0.5`}),e.siblings.length>0?(0,F.jsx)(fe,{segment:e,isLast:n===l.length-1,projectName:t,onFileClick:d}):(0,F.jsx)(`span`,{className:`text-xs text-muted-foreground px-1 py-0.5`,children:e.name})]},e.fullPath))})}function fe({segment:e,isLast:t,projectName:n,onFileClick:r}){let i=(0,P.useMemo)(()=>L(e.siblings),[e.siblings]);return(0,F.jsxs)(w,{children:[(0,F.jsx)(_,{asChild:!0,children:(0,F.jsx)(`button`,{type:`button`,className:`text-xs px-1 py-0.5 rounded hover:bg-muted transition-colors truncate max-w-[120px] ${t?`text-foreground font-medium`:`text-muted-foreground`}`,children:e.name})}),(0,F.jsx)(b,{align:`start`,className:`max-h-[300px] p-1`,children:i.map(t=>(0,F.jsx)(R,{node:t,projectName:n,activePath:e.fullPath,onFileClick:r},t.path))})]})}function R({node:e,projectName:t,activePath:n,onFileClick:r}){let i=ue(e.name,e.type===`directory`),a=e.path===n;return e.type===`directory`&&e.children&&e.children.length>0?(0,F.jsxs)(j,{children:[(0,F.jsxs)(k,{className:`text-xs gap-1.5 ${a?`bg-muted`:``}`,children:[(0,F.jsx)(i,{className:`size-3.5 shrink-0 text-muted-foreground`}),(0,F.jsx)(`span`,{className:`truncate`,children:e.name})]}),(0,F.jsx)(S,{className:`max-h-[300px] overflow-y-auto p-1`,children:L(e.children).map(e=>(0,F.jsx)(R,{node:e,projectName:t,activePath:n,onFileClick:r},e.path))})]}):(0,F.jsxs)(E,{className:`text-xs gap-1.5 cursor-pointer ${a?`bg-muted`:``}`,onSelect:e=>{},onClick:t=>{e.type!==`directory`&&r(e.path,t)},children:[(0,F.jsx)(i,{className:`size-3.5 shrink-0 text-muted-foreground`}),(0,F.jsx)(`span`,{className:`truncate`,children:e.name})]})}function z({active:e,onClick:t,icon:n,label:r}){return(0,F.jsxs)(`button`,{type:`button`,onClick:t,className:`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${e?`bg-muted text-foreground`:`text-muted-foreground hover:text-foreground`}`,children:[(0,F.jsx)(n,{className:`size-3`}),(0,F.jsx)(`span`,{className:`hidden sm:inline`,children:r})]})}function pe({ext:e,mdMode:t,onMdModeChange:n,csvMode:r,onCsvModeChange:a,wordWrap:c,onToggleWordWrap:l,filePath:u,projectName:d,className:f}){return(0,F.jsxs)(`div`,{className:f,children:[(e===`md`||e===`mdx`)&&n&&(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(z,{active:t===`edit`,onClick:()=>n(`edit`),icon:i,label:`Edit`}),(0,F.jsx)(z,{active:t===`preview`,onClick:()=>n(`preview`),icon:m,label:`Preview`})]}),e===`csv`&&a&&(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(z,{active:r===`table`,onClick:()=>a(`table`),icon:o,label:`Table`}),(0,F.jsx)(z,{active:r===`raw`,onClick:()=>a(`raw`),icon:i,label:`Raw`})]}),(0,F.jsx)(z,{active:c,onClick:l,icon:s,label:`Wrap`}),u&&d&&(0,F.jsx)(z,{active:!1,onClick:()=>ie(d,u),icon:y,label:`Download`})]})}var me=(0,P.lazy)(()=>c(()=>import(`./csv-preview-D2pJJj3K.js`).then(e=>({default:e.CsvPreview})),__vite__mapDeps([0,1,2,3,4,5,6,7,8]))),he=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`svg`,`ico`]),ge=new Set([`db`,`sqlite`,`sqlite3`]);function B(e){return e.split(`.`).pop()?.toLowerCase()??``}function _e(e){return{js:`javascript`,jsx:`javascript`,ts:`typescript`,tsx:`typescript`,py:`python`,html:`html`,css:`css`,scss:`scss`,json:`json`,md:`markdown`,mdx:`markdown`,yaml:`yaml`,yml:`yaml`,sh:`shell`,bash:`shell`,sql:`sql`}[B(e)]??`plaintext`}function V({metadata:e,tabId:t}){let n=e?.filePath,r=e?.projectName,i=e?.inlineContent,a=e?.inlineLanguage,[o,s]=(0,P.useState)(i??null),[c,l]=(0,P.useState)(`utf-8`),[f,m]=(0,P.useState)(!0),[g,_]=(0,P.useState)(null),[v,y]=(0,P.useState)(!1),b=(0,P.useRef)(null),x=(0,P.useRef)(``),S=(0,P.useRef)(null),{tabs:C,updateTab:w}=h(),{wordWrap:E,toggleWordWrap:D}=te(),ie=oe(),O=C.find(e=>e.id===t),k=n?B(n):``,A=he.has(k),j=k===`pdf`,M=ge.has(k),le=k===`md`||k===`mdx`,ue=k===`csv`,I=k===`sql`,[L,fe]=(0,P.useState)(`preview`),[R,z]=(0,P.useState)(`table`),{connections:V,cachedTables:xe,refreshTables:Se}=ne(),[H,Ce]=(0,P.useState)(()=>{if(!I||!n)return null;let e=localStorage.getItem(`ppm:sql-conn:${n}`);return e?Number(e):null}),U=(0,P.useRef)(null),W=(0,P.useRef)(null),G=(0,P.useMemo)(()=>V.find(e=>e.id===H)??null,[V,H]),we=(0,P.useCallback)(e=>{Ce(e),n&&localStorage.setItem(`ppm:sql-conn:${n}`,String(e)),Se(e).catch(()=>{})},[n,Se]),K=(0,P.useMemo)(()=>{if(!I||!H)return;let e=(xe.get(H)??[]).map(e=>({name:e.tableName,schema:e.schemaName}));if(e.length!==0)return{tables:e,getColumns:async(e,t)=>p.get(`/api/db/connections/${H}/schema?table=${encodeURIComponent(e)}${t?`&schema=${encodeURIComponent(t)}`:``}`)}},[I,H,xe]);(0,P.useEffect)(()=>{if(!(!U.current||!K))return W.current?.dispose(),ce(),W.current=U.current.languages.registerCompletionItemProvider(`sql`,se(U.current,K)),()=>{W.current?.dispose()}},[K]);let Te=h(e=>e.openTab),q=(0,P.useCallback)(e=>{G&&Te({type:`database`,title:`${G.name} · Query`,projectId:null,closable:!0,metadata:{connectionId:G.id,connectionName:G.name,dbType:G.type,initialSql:e}})},[G,Te]),Ee=(0,P.useCallback)(()=>{if(!S.current||!G)return;let e=S.current,t=e.getSelection();q(t&&!t.isEmpty()?e.getModel()?.getValueInRange(t)??e.getValue():e.getValue())},[G,q]),J=(0,P.useRef)([]),Y=(0,P.useRef)(q);Y.current=q,(0,P.useEffect)(()=>{M&&t&&w(t,{type:`sqlite`})},[M,t,w]);let X=n?/^(\/|[A-Za-z]:[/\\])/.test(n):!1;(0,P.useEffect)(()=>{if(i!=null){m(!1);return}if(!n||!X&&!r)return;if(A||j){m(!1);return}m(!0),_(null);let e=X?`/api/fs/read?path=${encodeURIComponent(n)}`:`${d(r)}/files/read?path=${encodeURIComponent(n)}`;return p.get(e).then(e=>{s(e.content),e.encoding&&l(e.encoding),x.current=e.content,m(!1)}).catch(e=>{_(e instanceof Error?e.message:`Failed to load file`),m(!1)}),()=>{b.current&&clearTimeout(b.current)}},[n,r,A,j,X]),(0,P.useEffect)(()=>{if(!O)return;let e=n?u(n):`Untitled`,t=v?`${e} \u25CF`:e;O.title!==t&&w(O.id,{title:t})},[v]);let De=(0,P.useCallback)(async e=>{if(n&&!(!X&&!r))try{X?await p.put(`/api/fs/write`,{path:n,content:e}):await p.put(`${d(r)}/files/write`,{path:n,content:e}),y(!1)}catch{}},[n,r,X]);function Oe(e){let t=e??``;s(t),x.current=t,y(!0),b.current&&clearTimeout(b.current),b.current=setTimeout(()=>De(x.current),1e3)}let Z=e?.lineNumber,ke=(0,P.useCallback)((e,t)=>{if(S.current=e,U.current=t,Z&&Z>0&&setTimeout(()=>{e.revealLineInCenter(Z),e.setPosition({lineNumber:Z,column:1}),e.focus()},100),e.addCommand(t.KeyMod.Alt|t.KeyCode.KeyZ,()=>te.getState().toggleWordWrap()),t.languages.typescript.typescriptDefaults.setDiagnosticsOptions({noSemanticValidation:!0,noSyntaxValidation:!0,noSuggestionDiagnostics:!0}),t.languages.typescript.javascriptDefaults.setDiagnosticsOptions({noSemanticValidation:!0,noSyntaxValidation:!0,noSuggestionDiagnostics:!0}),K&&(W.current?.dispose(),W.current=t.languages.registerCompletionItemProvider(`sql`,se(t,K))),I){J.current.forEach(e=>e.dispose()),J.current=[];let n=e.addCommand(0,(e,t)=>{t&&Y.current(t)});if(n){let e=t.languages.registerCodeLensProvider(`sql`,{provideCodeLenses:e=>{let t=[],r=e.getValue().split(`
3
- `),i=-1,a=[],o=(e,r)=>{let i=r.trim();!i||i.startsWith(`--`)||t.push({range:{startLineNumber:e,startColumn:1,endLineNumber:e,endColumn:1},command:{id:n,title:`▷ Run`,arguments:[i]}})};for(let e=0;e<r.length;e++){let t=r[e].trim();if(i===-1){if(!t||t.startsWith(`--`))continue;i=e+1,a=[]}a.push(r[e]),t.endsWith(`;`)&&(o(i,a.join(`
4
- `)),i=-1,a=[])}return i>0&&a.join(``).trim()&&o(i,a.join(`
5
- `)),{lenses:t,dispose:()=>{}}}});J.current.push(e)}}},[K]);if(!i&&(!n||!X&&!r))return(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full text-text-secondary text-sm`,children:`No file selected.`});if(f)return(0,F.jsxs)(`div`,{className:`flex items-center justify-center h-full gap-2 text-text-secondary`,children:[(0,F.jsx)(T,{className:`size-5 animate-spin`}),(0,F.jsx)(`span`,{className:`text-sm`,children:`Loading file...`})]});if(g)return(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full text-error text-sm`,children:g});if(A)return(0,F.jsx)(ye,{filePath:n,projectName:r});if(j)return(0,F.jsx)(be,{filePath:n,projectName:r});if(c===`base64`)return(0,F.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary`,children:[(0,F.jsx)(N,{className:`size-10 text-text-subtle`}),(0,F.jsx)(`p`,{className:`text-sm`,children:`This file is a binary format and cannot be displayed.`}),(0,F.jsx)(`p`,{className:`text-xs text-text-subtle`,children:n})]});let Ae=I?(0,F.jsxs)(`div`,{className:`shrink-0 flex items-center gap-1 px-2 border-l border-border`,children:[(0,F.jsx)(ee,{className:`size-3 text-muted-foreground`}),(0,F.jsxs)(`select`,{value:H??``,onChange:e=>{let t=Number(e.target.value);t&&we(t)},className:`h-5 text-[10px] bg-transparent border border-border rounded px-1 text-foreground outline-none max-w-[140px]`,title:`Select connection for autocomplete`,children:[(0,F.jsx)(`option`,{value:``,children:`Connection…`}),V.map(e=>(0,F.jsx)(`option`,{value:e.id,children:e.name},e.id))]}),(0,F.jsx)(`button`,{type:`button`,onClick:Ee,disabled:!G,className:`p-0.5 rounded text-muted-foreground hover:text-primary disabled:opacity-30 transition-colors`,title:`Run all in DB Viewer`,children:(0,F.jsx)(re,{className:`size-3.5`})})]}):null,je=i!=null&&(a===`json`||a===`xml`),[Q,$]=(0,P.useState)(!1),Me=(0,P.useCallback)(()=>{if(i)if(Q)s(i),$(!1);else{let e=i.trimStart();if(a===`json`)try{s(JSON.stringify(JSON.parse(e),null,2)),$(!0)}catch{}else if(a===`xml`){let t=0;s(e.replace(/(>)(<)(\/*)/g,`$1
6
- $2$3`).split(`
7
- `).map(e=>{let n=e.trim();n.startsWith(`</`)&&(t=Math.max(0,t-1));let r=` `.repeat(t)+n;return n.startsWith(`<`)&&!n.startsWith(`</`)&&!n.endsWith(`/>`)&&!n.includes(`</`)&&t++,r}).join(`
8
- `)),$(!0)}}},[i,a,Q]);return(0,F.jsxs)(`div`,{className:`flex flex-col h-full w-full overflow-hidden`,children:[i!=null&&je&&(0,F.jsx)(`div`,{className:`flex items-center h-7 border-b border-border bg-background shrink-0 px-2 gap-2`,children:(0,F.jsx)(`button`,{type:`button`,onClick:Me,className:`text-[10px] px-2 py-0.5 rounded border border-border hover:bg-muted transition-colors text-foreground`,children:Q?`Raw`:`Beautify`})}),n&&r&&t&&(0,F.jsxs)(`div`,{className:`hidden md:flex items-center h-7 border-b border-border bg-background shrink-0`,children:[(0,F.jsx)(de,{filePath:n,projectName:r,tabId:t,className:`flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none px-2 gap-0.5`}),Ae,(0,F.jsx)(pe,{ext:k,mdMode:L,onMdModeChange:fe,csvMode:R,onCsvModeChange:z,wordWrap:E,onToggleWordWrap:D,filePath:n,projectName:r,className:`shrink-0 flex items-center gap-1 px-2`})]}),I&&(!r||!t)&&(0,F.jsxs)(`div`,{className:`hidden md:flex items-center h-7 border-b border-border bg-background shrink-0 px-2`,children:[(0,F.jsx)(`span`,{className:`text-xs text-muted-foreground truncate flex-1`,children:n?u(n):`SQL`}),Ae]}),ue&&R===`table`?(0,F.jsx)(P.Suspense,{fallback:(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,F.jsx)(T,{className:`size-5 animate-spin text-text-subtle`})}),children:(0,F.jsx)(me,{content:o??``,onContentChange:Oe,wordWrap:E})}):le&&L===`preview`?(0,F.jsx)(ve,{content:o??``}):(0,F.jsx)(`div`,{className:`flex-1 overflow-hidden`,children:(0,F.jsx)(ae,{height:`100%`,language:a??_e(n??``),value:o??``,onChange:i==null?Oe:void 0,onMount:ke,theme:ie,options:{fontSize:13,fontFamily:`Menlo, Monaco, Consolas, monospace`,wordWrap:E?`on`:`off`,minimap:{enabled:!1},scrollBeyondLastLine:!1,automaticLayout:!0,lineNumbers:`on`,folding:!0,bracketPairColorization:{enabled:!0},readOnly:i!=null},loading:(0,F.jsx)(T,{className:`size-5 animate-spin text-text-subtle`})})})]})}function ve({content:e}){return(0,F.jsx)(a,{content:e,className:`flex-1 overflow-auto p-4`})}function ye({filePath:e,projectName:t}){let[n,r]=(0,P.useState)(null),[i,a]=(0,P.useState)(!1);return(0,P.useEffect)(()=>{let n,i=`${d(t)}/files/raw?path=${encodeURIComponent(e)}`,o=f();return fetch(i,{headers:o?{Authorization:`Bearer ${o}`}:{}}).then(e=>{if(!e.ok)throw Error(`Failed`);return e.blob()}).then(e=>{let t=URL.createObjectURL(e);n=t,r(t)}).catch(()=>a(!0)),()=>{n&&URL.revokeObjectURL(n)}},[e,t]),i?(0,F.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary`,children:[(0,F.jsx)(N,{className:`size-10 text-text-subtle`}),(0,F.jsx)(`p`,{className:`text-sm`,children:`Failed to load image.`})]}):n?(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full p-4 bg-surface overflow-auto`,children:(0,F.jsx)(`img`,{src:n,alt:e,className:`max-w-full max-h-full object-contain`})}):(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,F.jsx)(T,{className:`size-5 animate-spin text-text-subtle`})})}function be({filePath:e,projectName:t}){let[n,r]=(0,P.useState)(null),[i,a]=(0,P.useState)(!1);(0,P.useEffect)(()=>{let n,i=`${d(t)}/files/raw?path=${encodeURIComponent(e)}`,o=f();return fetch(i,{headers:o?{Authorization:`Bearer ${o}`}:{}}).then(e=>{if(!e.ok)throw Error(`Failed`);return e.blob()}).then(e=>{let t=URL.createObjectURL(new Blob([e],{type:`application/pdf`}));n=t,r(t)}).catch(()=>a(!0)),()=>{n&&URL.revokeObjectURL(n)}},[e,t]);let o=(0,P.useCallback)(()=>{n&&window.open(n,`_blank`)},[n]);return i?(0,F.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary`,children:[(0,F.jsx)(N,{className:`size-10 text-text-subtle`}),(0,F.jsx)(`p`,{className:`text-sm`,children:`Failed to load PDF.`})]}):n?(0,F.jsxs)(`div`,{className:`flex flex-col h-full`,children:[(0,F.jsxs)(`div`,{className:`flex items-center justify-between px-3 py-1.5 border-b border-border bg-background shrink-0`,children:[(0,F.jsx)(`span`,{className:`text-xs text-text-secondary truncate`,children:e}),(0,F.jsxs)(`button`,{onClick:o,className:`flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors`,children:[(0,F.jsx)(O,{className:`size-3`}),` Open in new tab`]})]}),(0,F.jsx)(`iframe`,{src:n,title:e,className:`flex-1 w-full border-none`})]}):(0,F.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,F.jsx)(T,{className:`size-5 animate-spin text-text-subtle`})})}export{V as CodeEditor};