@hienlh/ppm 0.13.7 → 0.13.9

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 (102) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/ai-settings-section-CLNBWLS4.js +1 -0
  5. package/dist/web/assets/{api-settings-t7Leca7J.js → api-settings-D0_eiIYv.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-WMbLpD5Y.js +1 -0
  7. package/dist/web/assets/{audio-preview-BR7DoH0l.js → audio-preview-C1p-Q5XZ.js} +1 -1
  8. package/dist/web/assets/{chat-tab-8Bvn7PHt.js → chat-tab-BSJUkgxB.js} +9 -9
  9. package/dist/web/assets/code-editor-rNw5_pXh.js +8 -0
  10. package/dist/web/assets/{conflict-editor-C-lUw4gv.js → conflict-editor-Dcn3HuLD.js} +1 -1
  11. package/dist/web/assets/{csv-preview-C9qGhDlb.js → csv-preview-D5lmgVEy.js} +1 -1
  12. package/dist/web/assets/data-grid-nZfSIop5.js +5 -0
  13. package/dist/web/assets/database-DOWH9-Vv.js +1 -0
  14. package/dist/web/assets/database-viewer-CNoq5Uxp.js +1 -0
  15. package/dist/web/assets/{diff-viewer-I8qs3Nb-.js → diff-viewer-NMLD4V8q.js} +1 -1
  16. package/dist/web/assets/{esm-B3je8j5P.js → esm-nXReYVnB.js} +1 -1
  17. package/dist/web/assets/{extension-webview-Dt9bKs0C.js → extension-webview-DW2dBswj.js} +1 -1
  18. package/dist/web/assets/{file-store-BgZggznw.js → file-store-BrbCNyLm.js} +1 -1
  19. package/dist/web/assets/gitGraph-HDMCJU4V-BdPTuzO3.js +1 -0
  20. package/dist/web/assets/{image-preview-C-w-CK5s.js → image-preview-Dqp1KSus.js} +1 -1
  21. package/dist/web/assets/index-CoMWx5VS.js +27 -0
  22. package/dist/web/assets/index-Dzb3OtrX.css +2 -0
  23. package/dist/web/assets/info-3K5VOQVL-MHX_1JfR.js +1 -0
  24. package/dist/web/assets/{input-bGJExpJZ.js → input-DYWhyaze.js} +1 -1
  25. package/dist/web/assets/keybindings-store-B7nlHmDh.js +1 -0
  26. package/dist/web/assets/{markdown-renderer-ySnJPmc1.js → markdown-renderer-DNIXdY0d.js} +3 -3
  27. package/dist/web/assets/packet-RMMSAZCW-CreFbf9A.js +1 -0
  28. package/dist/web/assets/{pdf-preview-BoQTv6B2.js → pdf-preview-ChC1gaaZ.js} +1 -1
  29. package/dist/web/assets/pie-UPGHQEXC-CnaHXUh8.js +1 -0
  30. package/dist/web/assets/port-forwarding-tab-dLhH_g2l.js +1 -0
  31. package/dist/web/assets/{postgres-viewer-CBntLqXY.js → postgres-viewer-De0pzd1C.js} +3 -3
  32. package/dist/web/assets/radar-KQ55EAFF-UxsdRHvt.js +1 -0
  33. package/dist/web/assets/{scroll-area-D0EQpAH2.js → scroll-area-BEllam7_.js} +1 -1
  34. package/dist/web/assets/{settings-store-CdcSAgEZ.js → settings-store-BHBb62gq.js} +2 -2
  35. package/dist/web/assets/settings-tab-Mrs9uzCZ.js +1 -0
  36. package/dist/web/assets/sparkles-B0mRBy_j.js +1 -0
  37. package/dist/web/assets/{sql-query-editor-vpD0I0KG.js → sql-query-editor-CMQpaOjA.js} +1 -1
  38. package/dist/web/assets/sqlite-viewer-BqtIjvil.js +1 -0
  39. package/dist/web/assets/tab-store-0rGchMXr.js +1 -0
  40. package/dist/web/assets/terminal-tab-CeHEtoE2.js +1 -0
  41. package/dist/web/assets/treemap-KZPCXAKY-CBVPi4NV.js +1 -0
  42. package/dist/web/assets/{use-blob-url-BgxxT-n_.js → use-blob-url-Hn6n1730.js} +1 -1
  43. package/dist/web/assets/{use-monaco-theme-dtPsv6sh.js → use-monaco-theme-CP-vyTF8.js} +1 -1
  44. package/dist/web/assets/{vendor-mermaid-DCxaaPi4.js → vendor-mermaid-CMiurk2b.js} +2 -2
  45. package/dist/web/assets/{video-preview-DxTnfuSQ.js → video-preview-CHPVrMtx.js} +1 -1
  46. package/dist/web/assets/x-OGGXhtlb.js +1 -0
  47. package/dist/web/index.html +18 -18
  48. package/dist/web/sw.js +1 -1
  49. package/package.json +2 -1
  50. package/src/index.ts +0 -0
  51. package/src/server/ws/terminal.ts +4 -0
  52. package/src/services/terminal.service.ts +4 -1
  53. package/src/web/components/database/data-grid.tsx +133 -17
  54. package/src/web/components/database/glide-grid-theme.ts +82 -0
  55. package/src/web/components/database/glide-grid-types.ts +79 -0
  56. package/src/web/components/database/use-glide-cell-content.ts +124 -0
  57. package/src/web/components/database/use-glide-columns.ts +61 -0
  58. package/src/web/components/database/use-glide-selection.ts +48 -0
  59. package/src/web/components/editor/code-editor.tsx +126 -9
  60. package/src/web/components/layout/draggable-tab.tsx +1 -0
  61. package/src/web/components/terminal/terminal-tab.tsx +19 -8
  62. package/src/web/hooks/use-terminal.ts +22 -2
  63. package/src/web/index.html +1 -0
  64. package/src/web/stores/panel-utils.ts +1 -0
  65. package/test.sql +1 -0
  66. package/bun.lock +0 -2062
  67. package/bunfig.toml +0 -2
  68. package/dist/web/assets/ai-settings-section-DeW4WN43.js +0 -1
  69. package/dist/web/assets/architecture-PBZL5I3N-Dy3PgD6O.js +0 -1
  70. package/dist/web/assets/code-editor-B-F5VdzM.js +0 -8
  71. package/dist/web/assets/database-DCT0OjgQ.js +0 -1
  72. package/dist/web/assets/database-viewer-mLY7oMC_.js +0 -2
  73. package/dist/web/assets/gitGraph-HDMCJU4V-Bu1SIFFq.js +0 -1
  74. package/dist/web/assets/index-CEI0tfaL.css +0 -2
  75. package/dist/web/assets/index-CFz4k7zO.js +0 -27
  76. package/dist/web/assets/info-3K5VOQVL-DzfAxmVd.js +0 -1
  77. package/dist/web/assets/keybindings-store-CWX97luK.js +0 -1
  78. package/dist/web/assets/packet-RMMSAZCW-DpzHf4xp.js +0 -1
  79. package/dist/web/assets/pie-UPGHQEXC-BpzFCKJ8.js +0 -1
  80. package/dist/web/assets/plus-51UQ45rf.js +0 -1
  81. package/dist/web/assets/port-forwarding-tab-CjlX-0D2.js +0 -1
  82. package/dist/web/assets/radar-KQ55EAFF-DAxWKxM4.js +0 -1
  83. package/dist/web/assets/settings-tab-Djw-bqxG.js +0 -1
  84. package/dist/web/assets/sqlite-viewer-CjtNrQ7C.js +0 -1
  85. package/dist/web/assets/tab-store-Jvy1eZGM.js +0 -1
  86. package/dist/web/assets/terminal-tab-bKLoy06f.js +0 -1
  87. package/dist/web/assets/treemap-KZPCXAKY-D6dgXbAe.js +0 -1
  88. package/dist/web/assets/x-BtqbfkR7.js +0 -1
  89. /package/dist/web/assets/{api-client-r4nyVy7H.js → api-client-Dvzcc_EO.js} +0 -0
  90. /package/dist/web/assets/{chevron-right-BzAdxJRG.js → chevron-right-DnHIvvcy.js} +0 -0
  91. /package/dist/web/assets/{code-CuravVys.js → code-DGBecc50.js} +0 -0
  92. /package/dist/web/assets/{csv-parser-DxVplKKB.js → csv-parser-DO0dz4x_.js} +0 -0
  93. /package/dist/web/assets/{dist-BqoEabX7.js → dist-CGvx1c8C.js} +0 -0
  94. /package/dist/web/assets/{katex-bpagxk3Z.js → katex-BFE6i_OH.js} +0 -0
  95. /package/dist/web/assets/{lib-BqkcKGFq.js → lib-D_kRA9p6.js} +0 -0
  96. /package/dist/web/assets/{react-BkWDCPD7.js → react-GqWghJ-L.js} +0 -0
  97. /package/dist/web/assets/{sql-completion-provider-EzHOQLfo.js → sql-completion-provider-tCzZfqWs.js} +0 -0
  98. /package/dist/web/assets/{table-DbSviOmw.js → table-Dq575bPF.js} +0 -0
  99. /package/dist/web/assets/{text-wrap-DzvCTq_i.js → text-wrap-Cn6BNQfq.js} +0 -0
  100. /package/dist/web/assets/{trash-2-BgDIBl6f.js → trash-2-CJYoLw7Q.js} +0 -0
  101. /package/dist/web/assets/{utils-ChWX7pZv.js → utils-CTg5uAYR.js} +0 -0
  102. /package/dist/web/assets/{vendor-xterm-D7SePDJp.js → vendor-xterm-u3AZMvTx.js} +0 -0
@@ -0,0 +1,124 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { GridCellKind, type GridCell, type EditableGridCell, type Item } from "@glideapps/glide-data-grid";
3
+ import type { GridColumnSchema } from "./glide-grid-types";
4
+ import { formatCellValue } from "./glide-grid-types";
5
+
6
+ /** Map DB type string to Glide cell kind */
7
+ function dbTypeToKind(type: string): GridCellKind {
8
+ const t = type.toLowerCase();
9
+ if (/^(int|serial|bigint|smallint|float|double|decimal|numeric|real|money)/.test(t)) {
10
+ return GridCellKind.Number;
11
+ }
12
+ if (/^bool/.test(t)) return GridCellKind.Boolean;
13
+ return GridCellKind.Text;
14
+ }
15
+
16
+ /** Truncate display string for canvas rendering performance */
17
+ function truncateDisplay(val: string, max = 200): string {
18
+ return val.length > max ? val.slice(0, max) + "…" : val;
19
+ }
20
+
21
+ interface UseGlideCellContentResult {
22
+ getCellContent: (cell: Item) => GridCell;
23
+ onCellEdited: (cell: Item, newValue: EditableGridCell) => void;
24
+ }
25
+
26
+ /**
27
+ * Provides getCellContent and onCellEdited callbacks for Glide Data Grid.
28
+ * Uses refs for rows/columnOrder to avoid stale closures in canvas render loop.
29
+ */
30
+ export function useGlideCellContent(
31
+ rows: Record<string, unknown>[],
32
+ columnOrder: string[],
33
+ schema: GridColumnSchema[],
34
+ pkCol: string | null,
35
+ onCellUpdate: (pkCol: string, pkVal: unknown, col: string, val: unknown) => void,
36
+ ): UseGlideCellContentResult {
37
+ // Use refs to avoid stale data in canvas callbacks
38
+ const rowsRef = useRef(rows);
39
+ rowsRef.current = rows;
40
+ const colOrderRef = useRef(columnOrder);
41
+ colOrderRef.current = columnOrder;
42
+
43
+ const schemaMap = useRef(new Map<string, GridColumnSchema>());
44
+ schemaMap.current = new Map(schema.map((s) => [s.name, s]));
45
+
46
+ const getCellContent = useCallback(([colIdx, rowIdx]: Item): GridCell => {
47
+ const colName = colOrderRef.current[colIdx];
48
+ const row = rowsRef.current[rowIdx];
49
+ if (!colName || !row) {
50
+ return { kind: GridCellKind.Text, data: "", displayData: "", allowOverlay: false };
51
+ }
52
+
53
+ const val = row[colName];
54
+ const colSchema = schemaMap.current.get(colName);
55
+ const kind = colSchema ? dbTypeToKind(colSchema.type) : GridCellKind.Text;
56
+ const isPk = colSchema?.pk ?? false;
57
+
58
+ // NULL values — styled as italic gray text
59
+ if (val == null) {
60
+ return {
61
+ kind: GridCellKind.Text,
62
+ data: "",
63
+ displayData: "NULL",
64
+ allowOverlay: !isPk,
65
+ readonly: isPk,
66
+ themeOverride: { textDark: "#6b7280" },
67
+ };
68
+ }
69
+
70
+ // Number cells
71
+ if (kind === GridCellKind.Number && typeof val === "number") {
72
+ return {
73
+ kind: GridCellKind.Number,
74
+ data: val,
75
+ displayData: String(val),
76
+ allowOverlay: !isPk,
77
+ readonly: isPk,
78
+ };
79
+ }
80
+
81
+ // Boolean cells — display only, edit via text overlay
82
+ if (kind === GridCellKind.Boolean && typeof val === "boolean") {
83
+ return {
84
+ kind: GridCellKind.Boolean,
85
+ data: val,
86
+ readonly: isPk,
87
+ allowOverlay: false,
88
+ };
89
+ }
90
+
91
+ // Default: Text cell
92
+ const strVal = formatCellValue(val);
93
+ return {
94
+ kind: GridCellKind.Text,
95
+ data: strVal,
96
+ displayData: truncateDisplay(strVal),
97
+ allowOverlay: !isPk,
98
+ readonly: isPk,
99
+ };
100
+ }, []); // stable — reads from refs
101
+
102
+ const onCellEdited = useCallback(([colIdx, rowIdx]: Item, newValue: EditableGridCell) => {
103
+ if (!pkCol) return;
104
+ const colName = colOrderRef.current[colIdx];
105
+ const row = rowsRef.current[rowIdx];
106
+ if (!colName || !row) return;
107
+
108
+ const pkVal = row[pkCol];
109
+ let parsed: unknown;
110
+ if (newValue.kind === GridCellKind.Text) {
111
+ parsed = newValue.data === "" ? null : newValue.data;
112
+ } else if (newValue.kind === GridCellKind.Number) {
113
+ parsed = newValue.data;
114
+ } else if (newValue.kind === GridCellKind.Boolean) {
115
+ parsed = newValue.data;
116
+ } else {
117
+ return;
118
+ }
119
+
120
+ onCellUpdate(pkCol, pkVal, colName, parsed);
121
+ }, [pkCol, onCellUpdate]);
122
+
123
+ return { getCellContent, onCellEdited };
124
+ }
@@ -0,0 +1,61 @@
1
+ import { useMemo } from "react";
2
+ import type { GridColumn } from "@glideapps/glide-data-grid";
3
+ import type { GridColumnSchema } from "./glide-grid-types";
4
+
5
+ interface UseGlideColumnsResult {
6
+ /** Ordered GridColumn definitions (pinned first) */
7
+ columns: GridColumn[];
8
+ /** Number of frozen columns from left */
9
+ freezeColumns: number;
10
+ /** Column name order matching GridColumn indices */
11
+ columnOrder: string[];
12
+ }
13
+
14
+ /**
15
+ * Build Glide Data Grid column definitions from schema.
16
+ * Reorders columns: pinned first, then unpinned.
17
+ */
18
+ export function useGlideColumns(
19
+ schema: GridColumnSchema[],
20
+ columnNames: string[],
21
+ pinnedCols: Set<string>,
22
+ colWidths: Map<string, number>,
23
+ orderBy?: string | null,
24
+ orderDir?: "ASC" | "DESC",
25
+ ): UseGlideColumnsResult {
26
+ return useMemo(() => {
27
+ // Reorder: pinned first, then rest (preserve original order within each group)
28
+ const pinned = columnNames.filter((c) => pinnedCols.has(c));
29
+ const unpinned = columnNames.filter((c) => !pinnedCols.has(c));
30
+ const ordered = [...pinned, ...unpinned];
31
+
32
+ const schemaMap = new Map(schema.map((s) => [s.name, s]));
33
+
34
+ const columns: GridColumn[] = ordered.map((name) => {
35
+ const col = schemaMap.get(name);
36
+ const isPk = col?.pk ?? false;
37
+
38
+ // Determine sort icon
39
+ let icon: string | undefined;
40
+ if (orderBy === name) {
41
+ icon = orderDir === "ASC" ? "sortAsc" : "sortDesc";
42
+ } else if (isPk) {
43
+ icon = "headerRowID";
44
+ }
45
+
46
+ return {
47
+ title: name,
48
+ id: name,
49
+ width: colWidths.get(name) ?? 150,
50
+ hasMenu: true,
51
+ icon,
52
+ };
53
+ });
54
+
55
+ return {
56
+ columns,
57
+ freezeColumns: pinned.length,
58
+ columnOrder: ordered,
59
+ };
60
+ }, [schema, columnNames, pinnedCols, colWidths, orderBy, orderDir]);
61
+ }
@@ -0,0 +1,48 @@
1
+ import { useState, useCallback, useMemo } from "react";
2
+ import { CompactSelection, type GridSelection } from "@glideapps/glide-data-grid";
3
+
4
+ const EMPTY_SELECTION: GridSelection = {
5
+ columns: CompactSelection.empty(),
6
+ rows: CompactSelection.empty(),
7
+ };
8
+
9
+ interface UseGlideSelectionResult {
10
+ gridSelection: GridSelection;
11
+ onGridSelectionChange: (newSel: GridSelection) => void;
12
+ /** Array of selected row indices (derived from CompactSelection) */
13
+ selectedRowIndices: number[];
14
+ clearSelection: () => void;
15
+ }
16
+
17
+ /**
18
+ * Manages controlled selection state for Glide Data Grid.
19
+ * Provides row indices array for bulk operations (delete, export).
20
+ */
21
+ export function useGlideSelection(): UseGlideSelectionResult {
22
+ const [gridSelection, setGridSelection] = useState<GridSelection>(EMPTY_SELECTION);
23
+
24
+ const onGridSelectionChange = useCallback((newSel: GridSelection) => {
25
+ setGridSelection(newSel);
26
+ }, []);
27
+
28
+ const selectedRowIndices = useMemo(() => {
29
+ const indices: number[] = [];
30
+ if (gridSelection.rows) {
31
+ for (const range of gridSelection.rows) {
32
+ // CompactSelection stores [start, end) ranges
33
+ if (Array.isArray(range)) {
34
+ for (let i = range[0]; i < range[1]; i++) indices.push(i);
35
+ } else {
36
+ indices.push(range);
37
+ }
38
+ }
39
+ }
40
+ return indices;
41
+ }, [gridSelection.rows]);
42
+
43
+ const clearSelection = useCallback(() => {
44
+ setGridSelection(EMPTY_SELECTION);
45
+ }, []);
46
+
47
+ return { gridSelection, onGridSelectionChange, selectedRowIndices, clearSelection };
48
+ }
@@ -8,13 +8,15 @@ import { usePanelStore } from "@/stores/panel-store";
8
8
  import { useSettingsStore } from "@/stores/settings-store";
9
9
  import { basename } from "@/lib/utils";
10
10
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
11
- import { Loader2, FileWarning, Play, Database } from "lucide-react";
11
+ import { Loader2, FileWarning, Play, Database, ExternalLink, X, GripHorizontal } from "lucide-react";
12
12
  import { EditorBreadcrumb } from "./editor-breadcrumb";
13
13
  import { EditorToolbar } from "./editor-toolbar";
14
14
  import { SaveAsDialog } from "./save-as-dialog";
15
15
  import { EditorMobileToolbar } from "./editor-mobile-toolbar";
16
16
  import { createSqlCompletionProvider, clearCompletionCache, type SchemaInfo } from "../database/sql-completion-provider";
17
17
  import { useConnections, type Connection } from "../database/use-connections";
18
+ import { DataGrid } from "../database/data-grid";
19
+ import type { DbQueryResult, DbColumnInfo } from "../database/use-database";
18
20
 
19
21
  const MarkdownRenderer = lazy(() =>
20
22
  import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
@@ -170,18 +172,37 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
170
172
  return () => { completionDisposable.current?.dispose(); };
171
173
  }, [sqlSchemaInfo]);
172
174
 
173
- // Run in DB Viewer
175
+ // Run SQL inline — execute query and show results in bottom panel
174
176
  const openTab = useTabStore((s) => s.openTab);
175
- const runSqlInViewer = useCallback((sqlText: string) => {
177
+ const [sqlResult, setSqlResult] = useState<DbQueryResult | null>(null);
178
+ const [sqlError, setSqlError] = useState<string | null>(null);
179
+ const [sqlLoading, setSqlLoading] = useState(false);
180
+ const [sqlResultSql, setSqlResultSql] = useState<string>("");
181
+ const runSqlInViewer = useCallback(async (sqlText: string) => {
176
182
  if (!selectedSqlConn) return;
183
+ setSqlLoading(true);
184
+ setSqlError(null);
185
+ setSqlResultSql(sqlText);
186
+ try {
187
+ const result = await api.post<DbQueryResult>(`/api/db/connections/${selectedSqlConn.id}/query`, { sql: sqlText });
188
+ setSqlResult(result);
189
+ } catch (e) {
190
+ setSqlError((e as Error).message);
191
+ setSqlResult(null);
192
+ } finally {
193
+ setSqlLoading(false);
194
+ }
195
+ }, [selectedSqlConn]);
196
+ const openSqlResultInTab = useCallback(() => {
197
+ if (!selectedSqlConn || !sqlResultSql) return;
177
198
  openTab({
178
199
  type: "database",
179
200
  title: `${selectedSqlConn.name} · Query`,
180
201
  projectId: null,
181
202
  closable: true,
182
- metadata: { connectionId: selectedSqlConn.id, connectionName: selectedSqlConn.name, dbType: selectedSqlConn.type, initialSql: sqlText },
203
+ metadata: { connectionId: selectedSqlConn.id, connectionName: selectedSqlConn.name, dbType: selectedSqlConn.type, initialSql: sqlResultSql },
183
204
  });
184
- }, [selectedSqlConn, openTab]);
205
+ }, [selectedSqlConn, openTab, sqlResultSql]);
185
206
 
186
207
  const handleRunInDbViewer = useCallback(() => {
187
208
  if (!editorRef.current || !selectedSqlConn) return;
@@ -299,9 +320,9 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
299
320
  return () => window.removeEventListener("file:changed", handler);
300
321
  }, [filePath, projectName, isExternalFile, inlineContent, isUntitled]);
301
322
 
302
- // Update tab title unsaved indicator
323
+ // Update tab title unsaved indicator (skip for inline content — title set by caller)
303
324
  useEffect(() => {
304
- if (!ownTab) return;
325
+ if (!ownTab || inlineContent != null) return;
305
326
  const baseName = isUntitled
306
327
  ? `Untitled-${metadata?.untitledNumber ?? 1}`
307
328
  : (filePath ? basename(filePath) : "Untitled");
@@ -520,7 +541,7 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
520
541
  onClick={handleRunInDbViewer}
521
542
  disabled={!selectedSqlConn}
522
543
  className="p-0.5 rounded text-muted-foreground hover:text-primary disabled:opacity-30 transition-colors"
523
- title="Run all in DB Viewer"
544
+ title="Run SQL"
524
545
  >
525
546
  <Play className="size-3.5" />
526
547
  </button>
@@ -583,7 +604,7 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
583
604
  ) : isMarkdown && mdMode === "preview" ? (
584
605
  <MarkdownPreview content={content ?? ""} />
585
606
  ) : (
586
- <div className="flex-1 overflow-hidden">
607
+ <div className="flex-1 overflow-hidden min-h-0">
587
608
  <Editor
588
609
  height="100%"
589
610
  language={inlineLanguage ?? getMonacoLanguage(filePath ?? "")}
@@ -608,6 +629,16 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
608
629
  </div>
609
630
  )}
610
631
 
632
+ {/* Inline SQL result panel */}
633
+ {isSql && (sqlResult || sqlError || sqlLoading) && (
634
+ <SqlResultPanel
635
+ result={sqlResult} error={sqlError} loading={sqlLoading}
636
+ connName={selectedSqlConn?.name}
637
+ onClose={() => { setSqlResult(null); setSqlError(null); setSqlLoading(false); }}
638
+ onOpenInTab={openSqlResultInTab}
639
+ />
640
+ )}
641
+
611
642
  {/* Mobile toolbar — bottom, like terminal */}
612
643
  {isMobile && <EditorMobileToolbar editorRef={editorRef} readOnly={inlineContent != null} />}
613
644
 
@@ -625,6 +656,92 @@ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEdit
625
656
  );
626
657
  });
627
658
 
659
+ const NOOP = () => {};
660
+
661
+ /** Inline SQL result panel — shows query results below the editor */
662
+ function SqlResultPanel({ result, error, loading, connName, onClose, onOpenInTab }: {
663
+ result: DbQueryResult | null;
664
+ error: string | null;
665
+ loading: boolean;
666
+ connName?: string;
667
+ onClose: () => void;
668
+ onOpenInTab: () => void;
669
+ }) {
670
+ const tableData = useMemo(() => (
671
+ result?.changeType === "select" && result.rows.length > 0
672
+ ? { columns: result.columns, rows: result.rows, total: result.rows.length, limit: result.rows.length }
673
+ : null
674
+ ), [result]);
675
+
676
+ const querySchema = useMemo<DbColumnInfo[]>(() => (
677
+ (result?.columns ?? []).map((c) => ({ name: c, type: "text", nullable: true, pk: false, defaultValue: null }))
678
+ ), [result?.columns]);
679
+
680
+ const [panelHeight, setPanelHeight] = useState(250);
681
+ const handleDrag = useCallback((e: React.MouseEvent) => {
682
+ e.preventDefault();
683
+ const startY = e.clientY;
684
+ const startH = panelHeight;
685
+ const onMove = (ev: MouseEvent) => setPanelHeight(Math.max(80, startH + (startY - ev.clientY)));
686
+ const onUp = () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); };
687
+ document.addEventListener("mousemove", onMove);
688
+ document.addEventListener("mouseup", onUp);
689
+ }, [panelHeight]);
690
+
691
+ return (
692
+ <div className="shrink-0 border-t border-border flex flex-col" style={{ height: panelHeight }}>
693
+ {/* Resize handle */}
694
+ <div onMouseDown={handleDrag}
695
+ className="shrink-0 h-1.5 cursor-row-resize bg-border/50 hover:bg-primary/30 flex items-center justify-center transition-colors">
696
+ <GripHorizontal className="size-3 text-muted-foreground/50" />
697
+ </div>
698
+ {/* Title bar */}
699
+ <div className="flex items-center gap-2 px-2 py-1 bg-muted/50 border-b border-border shrink-0">
700
+ <Database className="size-3 text-muted-foreground" />
701
+ <span className="text-xs font-medium text-foreground truncate flex-1">
702
+ {connName ? `${connName} · Results` : "Query Results"}
703
+ {result?.executionTimeMs != null && <span className="text-muted-foreground ml-1.5 font-normal">{result.executionTimeMs}ms</span>}
704
+ </span>
705
+ <button type="button" onClick={onOpenInTab} title="Open in DB Viewer tab"
706
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
707
+ <ExternalLink className="size-3" />
708
+ <span className="hidden sm:inline">Open in Tab</span>
709
+ </button>
710
+ <button type="button" onClick={onClose} title="Close results"
711
+ className="p-0.5 rounded text-muted-foreground hover:text-foreground transition-colors">
712
+ <X className="size-3" />
713
+ </button>
714
+ </div>
715
+
716
+ {/* Content */}
717
+ <div className="flex-1 overflow-hidden min-h-0">
718
+ {loading && (
719
+ <div className="flex items-center justify-center h-full">
720
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
721
+ </div>
722
+ )}
723
+ {error && <div className="px-3 py-2 text-xs text-destructive bg-destructive/5">{error}</div>}
724
+ {result?.changeType === "modify" && (
725
+ <div className="px-3 py-2 text-xs text-green-500">
726
+ {result.rowsAffected} row(s) affected
727
+ </div>
728
+ )}
729
+ {tableData && (
730
+ <DataGrid
731
+ tableData={tableData} schema={querySchema} loading={false}
732
+ page={1} onPageChange={NOOP} onCellUpdate={NOOP}
733
+ orderBy={null} orderDir="ASC" onToggleSort={NOOP}
734
+ connectionName={connName}
735
+ />
736
+ )}
737
+ {result?.changeType === "select" && result.rows.length === 0 && (
738
+ <div className="px-3 py-2 text-xs text-muted-foreground">No results</div>
739
+ )}
740
+ </div>
741
+ </div>
742
+ );
743
+ }
744
+
628
745
  function LoadingSpinner() {
629
746
  return <div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>;
630
747
  }
@@ -136,6 +136,7 @@ export function DraggableTab({
136
136
  ) : (
137
137
  <span
138
138
  className="max-w-[120px] truncate"
139
+ title={tab.title}
139
140
  onDoubleClick={(e) => {
140
141
  if (onRename) { e.stopPropagation(); setEditing(true); }
141
142
  }}
@@ -1,7 +1,7 @@
1
1
  import { useRef, useEffect, useState, useCallback, memo } from "react";
2
2
  import { useTerminal } from "@/hooks/use-terminal";
3
3
  import { cn } from "@/lib/utils";
4
- import { Copy, ClipboardPaste } from "lucide-react";
4
+ import { Copy, ClipboardPaste, RotateCcw } from "lucide-react";
5
5
  import "@xterm/xterm/css/xterm.css";
6
6
 
7
7
  interface TerminalTabProps {
@@ -22,7 +22,7 @@ export const TerminalTab = memo(function TerminalTab({ metadata }: TerminalTabPr
22
22
  const sessionId = (metadata?.sessionId as string) ?? "new";
23
23
  const projectName = metadata?.projectName as string | undefined;
24
24
  const containerRef = useRef<HTMLDivElement>(null);
25
- const { connected, reconnecting, sendData, getSelection } = useTerminal({ sessionId, projectName, containerRef });
25
+ const { connected, reconnecting, exited, sendData, getSelection, restart } = useTerminal({ sessionId, projectName, containerRef });
26
26
  const [ctrlMode, setCtrlMode] = useState(false);
27
27
  const [viewportHeight, setViewportHeight] = useState<number | null>(null);
28
28
 
@@ -97,16 +97,27 @@ export const TerminalTab = memo(function TerminalTab({ metadata }: TerminalTabPr
97
97
  <span
98
98
  className={cn(
99
99
  "size-2 rounded-full",
100
- connected ? "bg-success" : reconnecting ? "bg-warning" : "bg-error",
100
+ exited ? "bg-error" : connected ? "bg-success" : reconnecting ? "bg-warning" : "bg-error",
101
101
  )}
102
102
  />
103
103
  <span className="text-text-secondary">
104
- {connected
105
- ? "Connected"
106
- : reconnecting
107
- ? "Reconnecting..."
108
- : "Disconnected"}
104
+ {exited
105
+ ? "Process exited"
106
+ : connected
107
+ ? "Connected"
108
+ : reconnecting
109
+ ? "Reconnecting..."
110
+ : "Disconnected"}
109
111
  </span>
112
+ {exited && (
113
+ <button
114
+ onClick={restart}
115
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-surface-elevated text-text-primary hover:bg-primary hover:text-primary-foreground active:bg-primary active:text-primary-foreground transition-colors"
116
+ >
117
+ <RotateCcw size={10} />
118
+ Restart
119
+ </button>
120
+ )}
110
121
  <span className="text-text-subtle ml-auto font-mono">{sessionId}</span>
111
122
  </div>
112
123
 
@@ -66,8 +66,10 @@ interface UseTerminalOptions {
66
66
  interface UseTerminalReturn {
67
67
  connected: boolean;
68
68
  reconnecting: boolean;
69
+ exited: boolean;
69
70
  sendData: (data: string) => void;
70
71
  getSelection: () => string;
72
+ restart: () => void;
71
73
  }
72
74
 
73
75
  const RESIZE_PREFIX = "\x01RESIZE:";
@@ -83,6 +85,7 @@ export function useTerminal(
83
85
  const reconnectAttempts = useRef(0);
84
86
  const [connected, setConnected] = useState(false);
85
87
  const [reconnecting, setReconnecting] = useState(false);
88
+ const [exited, setExited] = useState(false);
86
89
  const actualSessionId = useRef(sessionId); // Track server-assigned session ID
87
90
 
88
91
  const sendData = useCallback((data: string) => {
@@ -104,6 +107,20 @@ export function useTerminal(
104
107
  }
105
108
  }, []);
106
109
 
110
+ const restart = useCallback(() => {
111
+ // Close existing WS, reset to "new" session, reconnect
112
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
113
+ wsRef.current?.close();
114
+ wsRef.current = null;
115
+ actualSessionId.current = "new";
116
+ reconnectAttempts.current = 0;
117
+ setExited(false);
118
+ setConnected(false);
119
+ setReconnecting(false);
120
+ // connectWs will be called after this via setTimeout to allow state to settle
121
+ setTimeout(() => connectWs(), 0);
122
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
123
+
107
124
  const connectWs = useCallback(() => {
108
125
  const term = termRef.current;
109
126
  if (!term) return;
@@ -130,13 +147,16 @@ export function useTerminal(
130
147
  if (event.data.startsWith("{")) {
131
148
  try {
132
149
  const msg = JSON.parse(event.data);
133
- if (msg.type === "session" || msg.type === "error") {
150
+ if (msg.type === "session" || msg.type === "error" || msg.type === "exited") {
134
151
  if (msg.type === "session" && msg.id) {
135
152
  actualSessionId.current = msg.id; // Save for reconnect
136
153
  }
137
154
  if (msg.type === "error") {
138
155
  term.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
139
156
  }
157
+ if (msg.type === "exited") {
158
+ setExited(true);
159
+ }
140
160
  return; // Don't write raw JSON to terminal
141
161
  }
142
162
  } catch {
@@ -234,5 +254,5 @@ export function useTerminal(
234
254
  };
235
255
  }, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
236
256
 
237
- return { connected, reconnecting, sendData, getSelection };
257
+ return { connected, reconnecting, exited, sendData, getSelection, restart };
238
258
  }
@@ -11,6 +11,7 @@
11
11
  <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600;700&family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet" />
12
12
  </head>
13
13
  <body class="bg-[#0f1419] text-[#e5e7eb] font-sans antialiased">
14
+ <div id="portal" style="position: fixed; left: 0; top: 0; z-index: 9999;" /></div>
14
15
  <div id="root"></div>
15
16
  <script type="module" src="./main.tsx"></script>
16
17
  </body>
@@ -89,6 +89,7 @@ export function getNextUntitledNumber(panels: Record<string, Panel>): number {
89
89
  export function deriveTabId(type: TabType, metadata?: Record<string, unknown>): string {
90
90
  switch (type) {
91
91
  case "editor":
92
+ if (metadata?.viewerKey) return `editor:viewer:${metadata.viewerKey}`;
92
93
  if (metadata?.isUntitled) return `editor:untitled-${metadata.untitledNumber ?? 1}`;
93
94
  return `editor:${metadata?.filePath ?? "untitled"}`;
94
95
  case "chat": {
package/test.sql ADDED
@@ -0,0 +1 @@
1
+ select * from config;