grain-tools 0.10.17 → 0.10.19

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.
@@ -0,0 +1,31 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Grain Results</title>
7
+ <style>
8
+ body { margin: 0; padding: 12px; color: var(--vscode-foreground, #222); font: 13px var(--vscode-font-family, sans-serif); }
9
+ button { font: inherit; }
10
+ .row { margin: 0 0 8px; }
11
+ #columns { overflow-wrap: anywhere; }
12
+ #error { color: var(--vscode-errorForeground, #b00); }
13
+ #grid { width: 100%; height: 480px; border: 0; }
14
+ label { display: inline-flex; gap: 6px; align-items: center; }
15
+ </style>
16
+ <script type="module" crossorigin>(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))s(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const u of r.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&s(u)}).observe(document,{childList:!0,subtree:!0});function n(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function s(t){if(t.ep)return;t.ep=!0;const r=n(t);fetch(t.href,r)}})();const l=new Map;let O=0,c,m=!1,i,f="",d=!1;function p(o,e={}){const n=++O;return new Promise(s=>{l.set(n,s),window.parent.postMessage({jsonrpc:"2.0",id:n,method:o,params:e},"*"),window.setTimeout(()=>{l.get(n)&&(l.delete(n),s({jsonrpc:"2.0",id:n,error:{message:"The host did not respond"}}))},1e4)})}function a(o){const e=document.querySelector("#error");e&&(e.textContent=o)}function y(o){const e=document.querySelector("#open");e&&(e.textContent=o)}function h(o){c?.contentWindow?.postMessage(o,"*")}function v(){!i||!m||h({jsonrpc:"2.0",method:"ui/notifications/tool-result",params:{structuredContent:i}})}window.addEventListener("message",o=>{const e=o.data;if(!(!e||e.jsonrpc!=="2.0")){if(o.source===c?.contentWindow){if(e.id!==void 0){const n=`frame-${String(e.id)}`;l.set(n,s=>{c?.contentWindow?.postMessage({...s,id:e.id},"*")}),window.parent.postMessage({...e,id:n},"*")}else window.parent.postMessage(e,"*"),e.method==="ui/notifications/initialized"&&(m=!0,v());return}if(e.id!==void 0&&l.has(e.id)){l.get(e.id)?.(e),l.delete(e.id);return}typeof e.method=="string"&&e.method.startsWith("ui/notifications/")&&(e.method==="ui/notifications/tool-result"?C(e.params):h(e))}});function C(o){if(!o||!o.structuredContent||typeof o.structuredContent!="object")return;i=o.structuredContent,v();const n=i.route==="editor",s=document.querySelector("#toggle-wrap");s&&(s.hidden=!n),y(n?"Open in Grain":"Open in workspace"),f=typeof i.cardPreferenceKey=="string"?i.cardPreferenceKey:"";const t=document.querySelector("#title"),r=document.querySelector("#count"),u=document.querySelector("#columns"),g=document.querySelector("#status");t&&(t.textContent=String(i.title||i.tabId||"Query result")),r&&(r.textContent=`${Number(i.rowCount??0).toLocaleString()} rows`),u&&(u.textContent=`Columns: ${(i.columns||[]).map(S=>S.name||"").join(", ")||"none"}`),g&&(g.textContent=`Status: ${i.status||"unknown"}`);try{d=f?window.localStorage.getItem(f)==="on":!1}catch{d=!1}const w=document.querySelector("#toggle");w&&(w.checked=d),(!n||d)&&b()}async function b(){if(c||!i)return;a("");const o=await p("resources/read",{uri:"ui://sql-preview/results-grid-full"}),n=o.result?.contents?.find(s=>typeof s.text=="string")?.text;if(o.error||!n){const s=i.route==="editor";a(s?"The full results view is unavailable. Open in Grain to see the result.":"The full results view is unavailable. Open in workspace to see the result."),y(s?"Open in Grain":"Open in workspace");return}c=document.createElement("iframe"),m=!1,c.id="grid",c.title="Full query results",c.srcdoc=n,document.body.append(c),document.querySelector("#card")?.classList.add("expanded")}document.querySelector("#open")?.addEventListener("click",async()=>{if(!i?.sessionId||!i.tabId){a("This result cannot be opened yet.");return}const o=i.route==="editor"?"open_in_grain":"open_in_workspace",e=await p("tools/call",{name:"open_result_view",arguments:{sessionId:i.sessionId,tabId:i.tabId,button:o}}),n=e.result,t=(n?.content||[]).map(u=>u.text).filter(Boolean).join(`
17
+ `),r=n?.structuredContent?.url||t.match(/https?:\/\/[^\s)]+/)?.[0];if(r){if(!(await p("ui/open-link",{url:r})).error){a(t&&t!==r?t:"Opened the result workspace.");return}p("tools/call",{name:"record_ui_event",arguments:{event:"mcp_result_view_opened",button:o}}).catch(()=>{}),a(`${t||"Open this result in your browser:"}
18
+ ${r}`);return}t&&a(t),e.error&&a(`${o==="open_in_grain"?"Open in Grain":"Open in workspace"} is not available yet.`)});document.querySelector("#toggle")?.addEventListener("change",o=>{d=o.currentTarget.checked;try{f&&window.localStorage.setItem(f,d?"on":"off")}catch{d=o.currentTarget.checked}d?b():(c?.remove(),c=void 0,document.querySelector("#card")?.classList.remove("expanded"))});p("ui/initialize",{appInfo:{name:"Grain Results Card",version:"1.0.0"},appCapabilities:{},protocolVersion:"2025-11-25"}).then(()=>window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/initialized"},"*"));</script>
19
+ </head>
20
+ <body>
21
+ <main id="card">
22
+ <h2 id="title" class="row">Query result</h2>
23
+ <div id="count" class="row"></div>
24
+ <div id="columns" class="row"></div>
25
+ <div id="status" class="row"></div>
26
+ <div class="row"><button id="open">Open in Grain</button></div>
27
+ <label id="toggle-wrap"><input id="toggle" type="checkbox">Show in chat</label>
28
+ <div id="error" role="status" aria-live="polite"></div>
29
+ </main>
30
+ </body>
31
+ </html>
package/dist/mcp-app.html CHANGED
@@ -12521,6 +12521,72 @@ function BaseHostProvider({
12521
12521
  }) {
12522
12522
  return /* @__PURE__ */ jsxRuntimeExports.jsx(HostContext.Provider, { value, children });
12523
12523
  }
12524
+ function useWorkspaceResultTarget() {
12525
+ const [targets, setTargets] = reactExports.useState({});
12526
+ const [resetToken, setResetToken] = reactExports.useState(0);
12527
+ const remember = reactExports.useCallback((tabId, structured) => {
12528
+ setResetToken((current) => current + 1);
12529
+ const sessionId = structured?.["sessionId"];
12530
+ const resultTabId = structured?.["tabId"];
12531
+ if (structured?.["route"] === "chat" && typeof sessionId === "string" && typeof resultTabId === "string") {
12532
+ setTargets((current) => ({ ...current, [tabId]: { sessionId, tabId: resultTabId } }));
12533
+ }
12534
+ }, []);
12535
+ return { targetForTab: (tabId) => targets[tabId], remember, resetToken };
12536
+ }
12537
+ function WorkspaceResultAction({
12538
+ target,
12539
+ resetToken,
12540
+ callServerTool,
12541
+ sendMessage
12542
+ }) {
12543
+ const [linkError, setLinkError] = reactExports.useState("");
12544
+ reactExports.useEffect(() => setLinkError(""), [resetToken]);
12545
+ const openWorkspace = async () => {
12546
+ if (!target) {
12547
+ return;
12548
+ }
12549
+ setLinkError("");
12550
+ try {
12551
+ const response = await callServerTool("open_result_view", {
12552
+ ...target,
12553
+ button: "open_in_workspace"
12554
+ });
12555
+ const structured = response.structuredContent;
12556
+ if (typeof structured?.url === "string") {
12557
+ try {
12558
+ await sendMessage("openLink", { url: structured.url });
12559
+ } catch {
12560
+ void callServerTool("record_ui_event", {
12561
+ event: "mcp_result_view_opened",
12562
+ button: "open_in_workspace"
12563
+ }).catch(() => void 0);
12564
+ setLinkError(structured.url);
12565
+ }
12566
+ } else if (structured?.openedInEditor === true) {
12567
+ setLinkError("Opened the result in Grain.");
12568
+ } else {
12569
+ throw new Error("The workspace link is unavailable");
12570
+ }
12571
+ } catch {
12572
+ setLinkError("Could not open this result in the workspace.");
12573
+ }
12574
+ };
12575
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
12576
+ target && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "sp-btn", onClick: () => void openWorkspace(), children: "Open in workspace" }),
12577
+ linkError && /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "sp-error-bar", role: "status", "aria-live": "polite", children: linkError })
12578
+ ] });
12579
+ }
12580
+ function copyResultToClipboard(result, copyText) {
12581
+ if (!result || result.rows.length === 0) {
12582
+ return;
12583
+ }
12584
+ const header = result.columns.map((column) => column.name).join(" ");
12585
+ const rows = result.rows.map(
12586
+ (row) => result.columns.map((_, index) => String(row[index] ?? "")).join(" ")
12587
+ );
12588
+ copyText([header, ...rows].join("\n"));
12589
+ }
12524
12590
  const createStoreImpl = (createState) => {
12525
12591
  let state;
12526
12592
  const listeners = /* @__PURE__ */ new Set();
@@ -147870,6 +147936,7 @@ function App() {
147870
147936
  updateTab
147871
147937
  } = useAppStore();
147872
147938
  const [isAppMode, setIsAppMode] = reactExports.useState(true);
147939
+ const { targetForTab, remember, resetToken } = useWorkspaceResultTarget();
147873
147940
  const hasReceivedToolCall = reactExports.useRef(false);
147874
147941
  const hasFallbackFired = reactExports.useRef(false);
147875
147942
  const gridRef = reactExports.useRef(null);
@@ -147984,6 +148051,7 @@ function App() {
147984
148051
  const structured = params?.structuredContent && typeof params.structuredContent === "object" ? params.structuredContent : void 0;
147985
148052
  const resultSql = entry?.sql ?? (typeof rawCallKey === "string" ? rawCallKey : typeof structured?.["query"] === "string" ? structured["query"] : "");
147986
148053
  const tabId = entry?.tabId ?? createTab(resultSql, false);
148054
+ remember(tabId, structured);
147987
148055
  setIsAppMode(true);
147988
148056
  if (params?.isError) {
147989
148057
  const text = params.content?.find((c) => c.type === "text")?.text;
@@ -148209,14 +148277,8 @@ function App() {
148209
148277
  copyTextSafe(csvString);
148210
148278
  }
148211
148279
  };
148212
- const handleCopy = () => {
148213
- if (!result || result.rows.length === 0) return;
148214
- const header = result.columns.map((c) => c.name).join(" ");
148215
- const tsvRows = result.rows.map(
148216
- (row) => result.columns.map((_, i2) => String(row[i2] ?? "")).join(" ")
148217
- );
148218
- copyTextSafe([header, ...tsvRows].join("\n"));
148219
- };
148280
+ const workspaceTarget = targetForTab(activeTab?.id ?? "");
148281
+ const handleCopy = () => copyResultToClipboard(result, copyTextSafe);
148220
148282
  if (!isReady || !activeTab)
148221
148283
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { display: "flex", justifyContent: "center", padding: "40px" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "spinner" }) });
148222
148284
  const showTabs = tabs.length > 1 || !isAppMode;
@@ -148276,7 +148338,8 @@ function App() {
148276
148338
  result && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "sp-row-count", children: [
148277
148339
  /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: result.rowCount ?? result.rows.length }),
148278
148340
  " rows"
148279
- ] })
148341
+ ] }),
148342
+ result && /* @__PURE__ */ jsxRuntimeExports.jsx(WorkspaceResultAction, { target: workspaceTarget, resetToken, callServerTool, sendMessage })
148280
148343
  ] }),
148281
148344
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "sp-result-header-right", children: result && result.rows.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
148282
148345
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "sp-btn", onClick: handleCopy, "aria-label": "Copy results", children: "Copy" }),
@@ -161939,11 +162002,15 @@ function MCPHostProvider({ children }) {
161939
162002
  };
161940
162003
  }
161941
162004
  });
161942
- const sendMessage = reactExports.useCallback((command, payload) => {
162005
+ const sendMessage = reactExports.useCallback(async (command, payload) => {
161943
162006
  if (!appRef.current) return;
161944
162007
  switch (command) {
161945
162008
  case "openLink":
161946
- if (appRef.current.openLink) appRef.current.openLink(payload);
162009
+ if (appRef.current.openLink) {
162010
+ await appRef.current.openLink(payload);
162011
+ } else {
162012
+ throw new Error("The host cannot open links");
162013
+ }
161947
162014
  break;
161948
162015
  default:
161949
162016
  console.warn("[MCPHost] Unknown command sent:", command);
package/dist/web-app.html CHANGED
@@ -12553,13 +12553,16 @@ async function bootstrapLocalSession(location, history) {
12553
12553
  const params = new URLSearchParams(location.hash.replace(/^#/, ""));
12554
12554
  const capability = params.get("launch");
12555
12555
  const rawView = params.get("view");
12556
+ const sessionId = params.get("sessionId");
12557
+ const tabId = params.get("tabId");
12558
+ const result = sessionId && tabId ? { sessionId, tabId } : void 0;
12556
12559
  let view = void 0;
12557
12560
  if (rawView === "connections" || rawView === "connections-add") {
12558
12561
  view = rawView;
12559
12562
  }
12560
12563
  history.replaceState({}, "", `${location.pathname}${location.search}`);
12561
12564
  if (!capability) {
12562
- return { session: "no-capability", view };
12565
+ return { session: "no-capability", view, result };
12563
12566
  }
12564
12567
  try {
12565
12568
  const response = await fetch(LOCAL_EXCHANGE_PATH, {
@@ -12571,11 +12574,11 @@ async function bootstrapLocalSession(location, history) {
12571
12574
  body: JSON.stringify({ capability })
12572
12575
  });
12573
12576
  if (response.status === 204) {
12574
- return { session: "ok", view };
12577
+ return { session: "ok", view, result };
12575
12578
  }
12576
- return { session: "failed", view };
12579
+ return { session: "failed", view, result };
12577
12580
  } catch {
12578
- return { session: "failed", view };
12581
+ return { session: "failed", view, result };
12579
12582
  }
12580
12583
  }
12581
12584
  async function authorizedFetch(input, init2) {
@@ -13065,6 +13068,10 @@ function useWebBootstrap() {
13065
13068
  return unsubscribe;
13066
13069
  }, []);
13067
13070
  }
13071
+ const RESULT_ROUTING_SCHEMA = {
13072
+ events: ["mcp_result_routed", "mcp_result_view_opened"],
13073
+ flagKeys: ["results-card-routing"]
13074
+ };
13068
13075
  const ALL_TOOLS = [
13069
13076
  "list_connectors",
13070
13077
  "run_query",
@@ -13087,7 +13094,8 @@ const ALL_TOOLS = [
13087
13094
  "list_charts",
13088
13095
  "ledger_stats",
13089
13096
  "verify_ledger",
13090
- "record_ui_event"
13097
+ "record_ui_event",
13098
+ "open_result_view"
13091
13099
  ];
13092
13100
  const WEB_PROXIED_TOOLS = [
13093
13101
  "list_connectors",
@@ -13110,6 +13118,7 @@ const TELEMETRY_EVENTS = [
13110
13118
  "query_confirmation_shown",
13111
13119
  "mcp_tool_invoked",
13112
13120
  "mcp_resource_read",
13121
+ ...RESULT_ROUTING_SCHEMA.events,
13113
13122
  "daemon_crashed",
13114
13123
  "service_lifecycle",
13115
13124
  "mcp_http_error",
@@ -13584,6 +13593,7 @@ const TELEMETRY_FLAG_KEYS = [
13584
13593
  "sql_preview_credential_required_signal",
13585
13594
  "sql_preview_crash_diagnostics",
13586
13595
  "sql_preview_mcp_surface",
13596
+ ...RESULT_ROUTING_SCHEMA.flagKeys,
13587
13597
  "unknown"
13588
13598
  ];
13589
13599
  const TELEMETRY_ACQUISITION_CHANNELS = ["marketplace", "open_vsx", "cli"];
@@ -14376,7 +14386,13 @@ function ReauthorizeNotice({
14376
14386
  }
14377
14387
  )
14378
14388
  ] }),
14379
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "sp-web-reauth-detail", children: reason === "expired-link" ? "This link has expired or was already used." : reason === "unreachable" ? "Grain may have stopped, or restarted on a different port. If it stopped, start it again first." : "This tab needs a fresh workspace link." })
14389
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "sp-web-reauth-detail", children: [
14390
+ reason === "expired-link" ? "This link has expired or was already used." : reason === "unreachable" ? "Grain may have stopped, or restarted on a different port. If it stopped, start it again first." : "This tab needs a fresh workspace link.",
14391
+ " ",
14392
+ "Using a browser other than your default? Run ",
14393
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "grain ui --url" }),
14394
+ " and paste the link here."
14395
+ ] })
14380
14396
  ] });
14381
14397
  }
14382
14398
  function isUpdateMarkerStatus(value) {
@@ -150354,7 +150370,8 @@ const TOOL_ENTRIES = {
150354
150370
  ledger_stats: ["ledger", "Show ledger health and counts."],
150355
150371
  verify_ledger: ["ledger", "Verify the ledger hash chain."],
150356
150372
  list_sessions: ["sessions", "Show active MCP sessions and their tabs."],
150357
- record_ui_event: null
150373
+ record_ui_event: null,
150374
+ open_result_view: null
150358
150375
  };
150359
150376
  const MCP_TOOL_GROUPS = GROUPS.map((group) => ({
150360
150377
  label: group.label,
@@ -152731,6 +152748,33 @@ function AgentQueryHeading({
152731
152748
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "sp-web-query-provenance", children: [tab.sessionDisplayName || tab.clientType, connectionName, ranAt].filter(Boolean).join(" · ") })
152732
152749
  ] });
152733
152750
  }
152751
+ function useWebAppInitialTarget(isReady, viewHint, resultTarget, tabs, showQuery, openSettingsPanel) {
152752
+ const appliedHint = reactExports.useRef(false);
152753
+ const appliedTarget = reactExports.useRef(void 0);
152754
+ reactExports.useEffect(() => {
152755
+ if (appliedHint.current || !viewHint) {
152756
+ return;
152757
+ }
152758
+ appliedHint.current = true;
152759
+ openSettingsPanel("connections", viewHint === "connections-add");
152760
+ }, [viewHint, openSettingsPanel]);
152761
+ reactExports.useEffect(() => {
152762
+ if (!isReady || !resultTarget) {
152763
+ return;
152764
+ }
152765
+ const targetKey = `${resultTarget.sessionId}\0${resultTarget.tabId}`;
152766
+ if (appliedTarget.current === targetKey) {
152767
+ return;
152768
+ }
152769
+ const target = tabs.find(
152770
+ (tab) => tab.sessionId === resultTarget.sessionId && (tab.remoteId === resultTarget.tabId || tab.id === resultTarget.tabId)
152771
+ );
152772
+ if (target) {
152773
+ appliedTarget.current = targetKey;
152774
+ showQuery(target.id);
152775
+ }
152776
+ }, [isReady, resultTarget, tabs, showQuery]);
152777
+ }
152734
152778
  const THEME_KEY = "grain:web-theme";
152735
152779
  const SIDEBAR_DEFAULT_WIDTH = 220;
152736
152780
  const SIDEBAR_MIN_WIDTH = 160;
@@ -152738,7 +152782,7 @@ const SIDEBAR_MAX_WIDTH = 400;
152738
152782
  const SIDEBAR_AUTO_COLLAPSE_QUERY = "(max-width: 640px)";
152739
152783
  const SIDEBAR_WIDTH_STORAGE_KEY = "sql-preview:web-sidebar-width";
152740
152784
  const CONNECTIONS_COLLAPSED_KEY = "grain:web-connections-collapsed";
152741
- function WebApp({ viewHint }) {
152785
+ function WebApp({ viewHint, resultTarget }) {
152742
152786
  const { onMessage, isReady, sendMessage, callServerTool, track } = useHostContext();
152743
152787
  const session = useWebSession();
152744
152788
  const {
@@ -152930,14 +152974,7 @@ function WebApp({ viewHint }) {
152930
152974
  reactExports.useEffect(() => {
152931
152975
  if (isReady) void fetchConnections();
152932
152976
  }, [isReady, fetchConnections]);
152933
- const hasAppliedHint = reactExports.useRef(false);
152934
- reactExports.useEffect(() => {
152935
- if (hasAppliedHint.current || !viewHint) {
152936
- return;
152937
- }
152938
- hasAppliedHint.current = true;
152939
- openSettingsPanel("connections", viewHint === "connections-add");
152940
- }, [viewHint, openSettingsPanel]);
152977
+ useWebAppInitialTarget(isReady, viewHint, resultTarget, tabs, showQuery, openSettingsPanel);
152941
152978
  reactExports.useEffect(() => {
152942
152979
  const mediaQuery = window.matchMedia(SIDEBAR_AUTO_COLLAPSE_QUERY);
152943
152980
  const handleChange = (e2) => {
@@ -153527,7 +153564,13 @@ async function bootstrap() {
153527
153564
  return;
153528
153565
  }
153529
153566
  ReactDOM$1.createRoot(document.getElementById("root")).render(
153530
- /* @__PURE__ */ jsxRuntimeExports.jsx(React.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebHostProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebApp, { viewHint: result.view }) }) })
153567
+ /* @__PURE__ */ jsxRuntimeExports.jsx(React.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebHostProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
153568
+ WebApp,
153569
+ {
153570
+ viewHint: result.view,
153571
+ ...result.result ? { resultTarget: result.result } : {}
153572
+ }
153573
+ ) }) })
153531
153574
  );
153532
153575
  }
153533
153576
  bootstrap();</script>