diff --git a/AGENTS.md b/AGENTS.md index f1bb613..da8217e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,18 +123,7 @@ Working on between operator in kql-lezer. ## HIGH: MCP Servers -Available MCP (Model Context Protocol) servers for enhanced functionality: - -- **vibe-check-mcp**: Use `vibe_check` to analyze code quality, style, or overall "vibe" of code snippets. Useful for code reviews or improvements. -- **taskmanager**: Use `add_tasks_to_request` to break down complex tasks, `approve_request_completion` to track progress. Helps manage multi-step development tasks. -- **sequential-thinking**: Use `sequentialthinking` for step-by-step reasoning and problem-solving. Ideal for debugging or complex logic analysis. -- **basic-memory**: Use `basic-memory_fetch` to retrieve stored information, `create_memory_project` for organizing project-related memories. Provides persistent context across sessions. -- **ESLint**: Use `lint-files` to run ESLint on specific files or directories. Integrates linting directly into workflows. -- **mcp-server-github**: Use for GitHub API interactions, such as fetching issues, PRs, or repository data. Enhances GitHub workflow integration. -- **bun-docs-mcp**: Use `SearchBun` to query Bun runtime documentation. Essential for Bun-specific questions or API lookups. -- **mcp-server-context7**: Use `get-library-docs` to retrieve documentation and code examples for libraries, `resolve-library-id` to find compatible library identifiers. Critical for researching external libraries and APIs. - -Always prefer MCP servers over manual searches when available, especially for documentation (context7), linting (ESLint), and task management (taskmanager). +Always prefer MCP servers over manual searches when available. ## MEDIUM: Tool Usage diff --git a/packages/kql-lezer/src/grammar/plugins/rules/query.ts b/packages/kql-lezer/src/grammar/plugins/rules/query.ts index 0552c2b..dffc6a9 100644 --- a/packages/kql-lezer/src/grammar/plugins/rules/query.ts +++ b/packages/kql-lezer/src/grammar/plugins/rules/query.ts @@ -1,46 +1,82 @@ -import { seq, many, ref, choice, optional, type RuleDef, kw } from "@fossiq/lezer-grammar-generator"; +import { + seq, + many, + ref, + choice, + optional, + type RuleDef, + kw, +} from "@fossiq/lezer-grammar-generator"; /** * Top-level query rules. */ export const queryRules: Record = { - Query: { - expression: seq( - many(choice(ref("LetStatement"), ref("SetStatement"), ref("DeclareQueryParametersStatement"))), - ref("QueryExpression") - ), - }, - - QueryExpression: { - expression: choice( - ref("PipelineExpression"), - ref("UnionExpression"), - ref("SearchExpression"), - ref("FindExpression") + Query: { + expression: seq( + many( + choice( + ref("LetStatement"), + ref("SetStatement"), + ref("DeclareQueryParametersStatement") ) - }, + ), + optional(ref("QueryExpression")) + ), + }, - UnionExpression: { - expression: seq( - kw("union"), - optional(ref("UnionParameters")), - ref("TableList") - ) - }, + QueryExpression: { + expression: choice( + ref("PipelineExpression"), + ref("UnionExpression"), + ref("SearchExpression"), + ref("FindExpression") + ), + }, + + UnionExpression: { + expression: seq( + kw("union"), + optional(ref("UnionParameters")), + ref("TableList") + ), + }, - SearchExpression: { - expression: seq( - kw("search"), - many(choice(ref("Identifier"), ref("String"), ref("Pipe"), ref("OpenParen"), ref("CloseParen"), kw("in"), kw("kind"), ref("Equals"))) - // TODO: Improve search grammar + SearchExpression: { + expression: seq( + kw("search"), + many( + choice( + ref("Identifier"), + ref("String"), + ref("Pipe"), + ref("OpenParen"), + ref("CloseParen"), + kw("in"), + kw("kind"), + ref("Equals") ) - }, + ) + // TODO: Improve search grammar + ), + }, - FindExpression: { - expression: seq( - kw("find"), - many(choice(ref("Identifier"), ref("String"), ref("Pipe"), ref("OpenParen"), ref("CloseParen"), kw("in"), kw("kind"), ref("Equals"))) - // TODO: Improve find grammar + FindExpression: { + expression: seq( + kw("find"), + many( + choice( + ref("Identifier"), + ref("String"), + ref("Pipe"), + ref("OpenParen"), + ref("CloseParen"), + kw("in"), + kw("kind"), + ref("Equals") ) - } -}; \ No newline at end of file + ) + // TODO: Improve find grammar + ), + }, +}; diff --git a/packages/ui/index.html b/packages/ui/index.html index 6d38c32..88f05ad 100644 --- a/packages/ui/index.html +++ b/packages/ui/index.html @@ -21,11 +21,11 @@ Fossiq - KQL Query Explorer - - - - - - - Fossiq - KQL Query Explorer - - - -
- - - diff --git a/packages/ui/public/manifest.json b/packages/ui/public/manifest.json index ab37ed4..b7979ff 100644 --- a/packages/ui/public/manifest.json +++ b/packages/ui/public/manifest.json @@ -12,13 +12,13 @@ "categories": ["productivity", "utilities"], "icons": [ { - "src": "data:image/svg+xml,F", + "src": "data:image/svg+xml,", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any" }, { - "src": "data:image/svg+xml,F", + "src": "data:image/svg+xml,", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any maskable" diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index b81591b..24e2ff2 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -12,8 +12,7 @@ const STORAGE_KEY_RESULTS = "fossiq-results"; const AppContent: Component = () => { // Load persisted query and results from localStorage - const savedQuery = - localStorage.getItem(STORAGE_KEY_QUERY) || ""; + const savedQuery = localStorage.getItem(STORAGE_KEY_QUERY) || ""; const savedResults = (() => { try { const stored = localStorage.getItem(STORAGE_KEY_RESULTS); @@ -47,7 +46,6 @@ const AppContent: Component = () => { } }); - const handleRun = async () => { const connection = conn(); if (!connection) { @@ -116,8 +114,7 @@ const AppContent: Component = () => { } - > -
+ editorPane={
{ />
+ } + resultsPane={
-

Results {results().length > 0 && `(${results().length})`}

+
+

Results {results().length > 0 && `(${results().length})`}

+
{
-
- + } + /> ); }; diff --git a/packages/ui/src/components/Layout.tsx b/packages/ui/src/components/Layout.tsx index 0385073..cdab85d 100644 --- a/packages/ui/src/components/Layout.tsx +++ b/packages/ui/src/components/Layout.tsx @@ -1,29 +1,85 @@ -import { Component } from "solid-js"; +import { Component, createSignal, JSX, createEffect } from "solid-js"; import Header from "./Header"; import Sidebar from "./Sidebar"; import { useTheme } from "../hooks/useTheme"; +const STORAGE_KEY_SIDEBAR = "fossiq-sidebar-collapsed"; + interface LayoutProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SolidJS children can be any renderable type - children?: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any headerContent?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editorPane?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resultsPane?: any; } const Layout: Component = (props) => { const { theme, toggleTheme } = useTheme(); + // Initialize from localStorage + const [sidebarCollapsed, setSidebarCollapsed] = createSignal( + localStorage.getItem(STORAGE_KEY_SIDEBAR) === "true" + ); + + const [resultsHeight, setResultsHeight] = createSignal(300); + const [isResizing, setIsResizing] = createSignal(false); + + // Persist sidebar state + createEffect(() => { + localStorage.setItem(STORAGE_KEY_SIDEBAR, String(sidebarCollapsed())); + }); + const handleAddSource = () => { console.log("Add source clicked"); // TODO: Implement add source dialog }; + const handleMouseDown: JSX.EventHandler = (e) => { + e.preventDefault(); + setIsResizing(true); + + const startY = e.clientY; + const startHeight = resultsHeight(); + + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaY = startY - moveEvent.clientY; + const newHeight = Math.min(Math.max(startHeight + deltaY, 100), 600); + setResultsHeight(newHeight); + }; + + const handleMouseUp = () => { + setIsResizing(false); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }; + return (
{props.headerContent}
- {props.children} - +
+
+ setSidebarCollapsed(!sidebarCollapsed())} + /> + {props.editorPane} +
+
+
+
+ {props.resultsPane} +
); diff --git a/packages/ui/src/components/ResultsTable.tsx b/packages/ui/src/components/ResultsTable.tsx index 8b41a65..a7fd2e2 100644 --- a/packages/ui/src/components/ResultsTable.tsx +++ b/packages/ui/src/components/ResultsTable.tsx @@ -25,21 +25,45 @@ const ResultsTable: Component = (props) => { const [tooltip, setTooltip] = createSignal(null); let parentRef: HTMLDivElement | undefined; - const MAX_COLUMN_WIDTH = 250; - // Dynamically generate columns based on the first item in data - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Column definitions are generic for dynamic query results - const columns = createMemo[]>(() => { + const columns = createMemo[]>(() => { if (!props.data || props.data.length === 0) return []; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowNumberColumn: ColumnDef = { + id: "rowNumber", + header: "#", + cell: (info) => info.row.index + 1, + enableSorting: false, + size: 50, + }; + const firstItem = props.data[0]; - return Object.keys(firstItem).map((key) => ({ - accessorKey: key, - header: key, - cell: (info) => { - const value = info.getValue(); - return typeof value === "bigint" ? String(value) : value; - }, - })); + const dataColumns = Object.keys(firstItem).map((key) => { + // Simple heuristic for column width stability + const headerWidth = key.length * 10 + 20; // 10px per char + padding + const value = firstItem[key]; + const valueString = + value === null || value === undefined ? "" : String(value); + const valueWidth = valueString.length * 8 + 20; // Slightly narrower char width assumption for values + + const estimatedWidth = Math.min( + Math.max(headerWidth, valueWidth, 100), + 300 + ); + + return { + accessorKey: key, + header: key, + cell: (info) => { + const val = info.getValue(); + return typeof val === "bigint" ? String(val) : val; + }, + size: estimatedWidth, + } as ColumnDef; + }); + + return [rowNumberColumn, ...dataColumns]; }); const table = createSolidTable({ @@ -69,7 +93,7 @@ const ResultsTable: Component = (props) => { }, getScrollElement: () => parentRef ?? null, estimateSize: () => 35, - overscan: 10, + overscan: 20, }); const virtualItems = () => rowVirtualizer.getVirtualItems(); @@ -104,7 +128,7 @@ const ResultsTable: Component = (props) => { return (
= (props) => { } }} > - +
@@ -132,20 +163,34 @@ const ResultsTable: Component = (props) => { {(header) => ( {/* Spacer row for virtual scroll offset */} - {virtualItems().length > 0 && ( + {virtualItems().length > 0 && (virtualItems()[0]?.start ?? 0) > 0 && ( @@ -189,17 +238,35 @@ const ResultsTable: Component = (props) => { {(cell) => { const value = cell.getValue(); + const isRowNumber = cell.column.id === "rowNumber"; return (
{flexRender( @@ -165,10 +210,14 @@ const ResultsTable: Component = (props) => {
handleCellClick(e, value)} + style={ + isRowNumber + ? { + padding: "0.5rem 0.5rem 0.5rem 1rem", + "white-space": "nowrap", + "text-align": "right", + color: "var(--text-secondary)", + "font-variant-numeric": "tabular-nums", + width: `${cell.column.getSize()}px`, + "min-width": `${cell.column.getSize()}px`, + "background-color": "var(--bg-secondary)", + "font-weight": "500", + } + : { + padding: "0.5rem 1rem", + "white-space": "nowrap", + overflow: "hidden", + "text-overflow": "ellipsis", + width: `${cell.column.getSize()}px`, + "min-width": `${cell.column.getSize()}px`, + cursor: "pointer", + } + } + onClick={(e) => + !isRowNumber && handleCellClick(e, value) + } title="" > {flexRender( @@ -223,6 +290,8 @@ const ResultsTable: Component = (props) => { totalSize() - (virtualItems()[virtualItems().length - 1]?.end ?? 0) }px`, + padding: 0, + border: "none", }} colspan={headerGroups()[0]?.headers.length || 1} /> diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx index 6358532..4ad4a1d 100644 --- a/packages/ui/src/components/Sidebar.tsx +++ b/packages/ui/src/components/Sidebar.tsx @@ -4,9 +4,11 @@ import { useSchema } from "../contexts/SchemaContext"; interface SidebarProps { onAddSource?: () => void; + collapsed?: boolean; + onToggleCollapse?: () => void; } -const Sidebar: Component = (_props) => { +const Sidebar: Component = (props) => { const { tables, addTable, @@ -54,77 +56,111 @@ const Sidebar: Component = (_props) => { }; return ( -