@marimo-team/frontend 0.23.10 → 0.23.11-dev2

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 (81) hide show
  1. package/dist/assets/{CellStatus-e0ex7Iei.js → CellStatus-Xx_7tl6i.js} +1 -1
  2. package/dist/assets/{JsonOutput-DRNPZOvX.js → JsonOutput-CbiJ_QRH.js} +5 -5
  3. package/dist/assets/{MarimoErrorOutput-BH6hs0Ir.js → MarimoErrorOutput-D9gYNYr6.js} +2 -2
  4. package/dist/assets/{RenderHTML-BQ1PO4Wd.js → RenderHTML-YHkbXfrr.js} +1 -1
  5. package/dist/assets/{RunButton-F8pLIvFp.js → RunButton-BSiMfpSC.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-Bd_3tPEt.js → add-cell-with-ai-CMER9awn.js} +20 -20
  7. package/dist/assets/{add-connection-dialog-AhwxOztN.js → add-connection-dialog-DKp7ZBWA.js} +1 -1
  8. package/dist/assets/{agent-panel-Dm0KI1sF.js → agent-panel-DN7RxKMN.js} +6 -6
  9. package/dist/assets/{ai-model-dropdown-CG4B4rqH.js → ai-model-dropdown-iEXaZ6YH.js} +3 -3
  10. package/dist/assets/{app-config-button-D_sFrSql.js → app-config-button-x5lccIL4.js} +1 -1
  11. package/dist/assets/{cell-editor-B3aYYyAI.js → cell-editor-G1jWTDlE.js} +15 -12
  12. package/dist/assets/{cell-link-Bj4-yOIx.js → cell-link-kkkjfd8C.js} +1 -1
  13. package/dist/assets/{cells-DOA0Gew8.js → cells-DGi-ohLl.js} +67 -67
  14. package/dist/assets/{chat-display-DI0jRLIv.js → chat-display-D3J4Wxxq.js} +1 -1
  15. package/dist/assets/{chat-panel-BU4HHdf5.js → chat-panel-Cswz1fGO.js} +2 -2
  16. package/dist/assets/{chat-ui-C7igY2w5.js → chat-ui-Cl5CdH_w.js} +4 -4
  17. package/dist/assets/{column-preview-nu3Qo2OW.js → column-preview-DypE7iks.js} +1 -1
  18. package/dist/assets/{command-palette-Bjv1Z7v8.js → command-palette-BtXkFFyo.js} +1 -1
  19. package/dist/assets/{common-fDFYY_sv.js → common-ByASZK7c.js} +1 -1
  20. package/dist/assets/{components--C6N-DXq.js → components-D8MC-pIV.js} +1 -1
  21. package/dist/assets/{datasource-CR6RRpTi.js → datasource-CpHnS4tt.js} +2 -2
  22. package/dist/assets/{dependency-graph-panel-BEgkvdX0.js → dependency-graph-panel-BpSpgk5-.js} +1 -1
  23. package/dist/assets/{documentation-panel-HvbKykbI.js → documentation-panel-iZ0hlSyN.js} +1 -1
  24. package/dist/assets/{download-DhxnAw14.js → download-YWP1Hdr2.js} +3 -3
  25. package/dist/assets/{edit-page-BeWwLeT8.js → edit-page-Bq0PlZWO.js} +6 -6
  26. package/dist/assets/{error-panel-WLBQ3q9p.js → error-panel-Cz8n3tJR.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-DtzGlnzT.js → file-explorer-panel-D9WR_f2x.js} +3 -3
  28. package/dist/assets/{file-icons-D2f3nfbq.js → file-icons-PPuQLn_Y.js} +1 -1
  29. package/dist/assets/{file-name-input-BNYf9WWM.js → file-name-input-BTXHtcG6.js} +1 -1
  30. package/dist/assets/{floating-outline-BvHFWRoz.js → floating-outline-C3q3X-Lc.js} +1 -1
  31. package/dist/assets/{focus-Ldqh99xE.js → focus-cLz6vpFk.js} +1 -1
  32. package/dist/assets/{form-CxBAInfg.js → form-CMwVIRZb.js} +1 -1
  33. package/dist/assets/{home-page-DwFpLpXK.js → home-page-DZO0YSAN.js} +2 -2
  34. package/dist/assets/{hooks-DyMacA-R.js → hooks-BZ1DWowv.js} +1 -1
  35. package/dist/assets/{html-to-image-CyAtzePO.js → html-to-image-Cso6uxBX.js} +2 -2
  36. package/dist/assets/index-BNWrEIlp.css +2 -0
  37. package/dist/assets/{index-fNBoXCyz.js → index-Cc7y-hhH.js} +14 -14
  38. package/dist/assets/{kiosk-mode-CTWHjzXs.js → kiosk-mode-U7OyKG2t.js} +1 -1
  39. package/dist/assets/{layout-dRNPwA-7.js → layout-Bx16aCt5.js} +5 -5
  40. package/dist/assets/{logs-panel-BVLYycQb.js → logs-panel-DvVodKOA.js} +1 -1
  41. package/dist/assets/{markdown-renderer-C9Ujvj0b.js → markdown-renderer-iZsApCjt.js} +1 -1
  42. package/dist/assets/{name-cell-input-DabvuruX.js → name-cell-input-BMWQdztR.js} +1 -1
  43. package/dist/assets/{outline-panel-B-ncbNWI.js → outline-panel-CLBPCXIO.js} +1 -1
  44. package/dist/assets/{packages-panel-CESeW_3T.js → packages-panel-JkGrPw8-.js} +1 -1
  45. package/dist/assets/{panels-BTEEcvYR.js → panels-BzShZ7de.js} +1 -1
  46. package/dist/assets/{process-output-pPgH0ANl.js → process-output-B90zX9Wb.js} +1 -1
  47. package/dist/assets/{radio-group-D7rh6Zek.js → radio-group-NLdY8xke.js} +1 -1
  48. package/dist/assets/{readonly-python-code-CvPx4CU_.js → readonly-python-code-7GLqujXI.js} +1 -1
  49. package/dist/assets/{reveal-component-DGQQBmed.js → reveal-component-Br0POw5W.js} +12 -12
  50. package/dist/assets/{run-page-yFxABzXq.js → run-page-Cl_FUfH2.js} +1 -1
  51. package/dist/assets/{scratchpad-panel-B_nxW99u.js → scratchpad-panel-BMEpUyr3.js} +1 -1
  52. package/dist/assets/session-panel-BbcFNEli.js +1 -0
  53. package/dist/assets/{snippets-panel-BSgcoo-M.js → snippets-panel-hdutxjmA.js} +1 -1
  54. package/dist/assets/{state-BWM3qRkR.js → state-BshcA8O2.js} +1 -1
  55. package/dist/assets/{state-C8yHPSLN.js → state-C-rzpByZ.js} +2 -2
  56. package/dist/assets/{textarea-bBuxbFm7.js → textarea-CTAL0lla.js} +1 -1
  57. package/dist/assets/{tracing-pt7eDHWc.js → tracing-BF5hbD3V.js} +1 -1
  58. package/dist/assets/{tracing-panel-BveZ5DZw.js → tracing-panel-5AsyMp7H.js} +2 -2
  59. package/dist/assets/{tree-actions-1KQDSu1H.js → tree-actions-DupCr05h.js} +1 -1
  60. package/dist/assets/{useCellActionButton-C4V7J6PS.js → useCellActionButton-BAo_hBug.js} +1 -1
  61. package/dist/assets/{useDeleteCell-BvnurAZ9.js → useDeleteCell-B6dMWmEN.js} +1 -1
  62. package/dist/assets/{useDependencyPanelTab-DNlXaxbt.js → useDependencyPanelTab-BZB0AnlV.js} +1 -1
  63. package/dist/assets/{useNotebookActions-Dw6tY2qa.js → useNotebookActions-DWcbGJ23.js} +1 -1
  64. package/dist/assets/{useRunCells-cSZpNqnR.js → useRunCells-mJYEegIM.js} +1 -1
  65. package/dist/assets/{useSplitCell-s7dSiBUa.js → useSplitCell-LSbcCg0_.js} +1 -1
  66. package/dist/index.html +23 -23
  67. package/package.json +1 -1
  68. package/src/components/datasources/__tests__/filter-empty.test.ts +81 -0
  69. package/src/components/datasources/__tests__/utils.test.ts +62 -1
  70. package/src/components/datasources/components.tsx +15 -7
  71. package/src/components/datasources/datasources.tsx +311 -178
  72. package/src/components/datasources/utils.ts +40 -1
  73. package/src/components/editor/ai/ai-completion-editor.tsx +6 -5
  74. package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +133 -0
  75. package/src/core/ai/stream-completion-text.ts +48 -0
  76. package/src/core/ai/strip-wrapping-backticks.ts +88 -0
  77. package/src/core/codemirror/ai/request.ts +2 -14
  78. package/src/core/datasets/__tests__/data-source.test.ts +226 -0
  79. package/src/core/datasets/data-source-connections.ts +88 -24
  80. package/dist/assets/index-BAYF7dcV.css +0 -2
  81. package/dist/assets/session-panel-CTJPYbAa.js +0 -1
@@ -4,15 +4,54 @@ import { BigQueryDialect } from "@marimo-team/codemirror-sql/dialects";
4
4
  import { isKnownDialect } from "@/core/codemirror/language/languages/sql/utils";
5
5
  import type { SQLTableContext } from "@/core/datasets/data-source-connections";
6
6
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
7
- import type { DataTable, DataType } from "@/core/kernel/messages";
7
+ import type {
8
+ Database,
9
+ DatabaseSchema,
10
+ DataTable,
11
+ DataType,
12
+ } from "@/core/kernel/messages";
8
13
  import { logNever } from "@/utils/assertNever";
9
14
  import type { ColumnHeaderStatsKey } from "../data-table/types";
10
15
 
16
+ /**
17
+ * Stable id for a table node in the datasources tree.
18
+ *
19
+ * schemaPath already includes the leaf schema for nested namespaces, so use it
20
+ * when present and fall back to the flat schema name otherwise (avoids
21
+ * duplicating the leaf, e.g. `top.nested.nested.table`).
22
+ */
23
+ export function tableUniqueId(
24
+ sqlTableContext: SQLTableContext | undefined,
25
+ tableName: string,
26
+ ): string {
27
+ if (!sqlTableContext) {
28
+ return tableName;
29
+ }
30
+ const segments = (
31
+ sqlTableContext.schemaPath?.length
32
+ ? sqlTableContext.schemaPath
33
+ : [sqlTableContext.schema]
34
+ ).filter(Boolean);
35
+ return [sqlTableContext.database, ...segments, tableName].join(".");
36
+ }
37
+
11
38
  // Some databases have no schemas, so we don't show it (eg. Clickhouse)
12
39
  export function isSchemaless(schemaName: string) {
13
40
  return schemaName === "";
14
41
  }
15
42
 
43
+ // Lazy discovery: the `*_resolved` flags default to `true` and are only `false`
44
+ // when enumeration was deferred. Helper functions to centralize logic
45
+ export function areSchemasResolved(database: Database): boolean {
46
+ return database.schemas_resolved !== false;
47
+ }
48
+ export function areTablesResolved(schema: DatabaseSchema): boolean {
49
+ return schema.tables_resolved !== false;
50
+ }
51
+ export function areChildSchemasResolved(schema: DatabaseSchema): boolean {
52
+ return schema.child_schemas_resolved !== false;
53
+ }
54
+
16
55
  interface SqlCodeFormatter {
17
56
  /**
18
57
  * Format the table path based on dialect-specific rules
@@ -27,7 +27,7 @@ import { Label } from "@/components/ui/label";
27
27
  import { Switch } from "@/components/ui/switch";
28
28
  import { Tooltip } from "@/components/ui/tooltip";
29
29
  import { toast } from "@/components/ui/use-toast";
30
- import { stagedAICellsAtom } from "@/core/ai/staged-cells";
30
+ import { stripWrappingBackticks } from "@/core/ai/strip-wrapping-backticks";
31
31
  import { type AiCompletionCell, includeOtherCellsAtom } from "@/core/ai/state";
32
32
  import type { CellId } from "@/core/cells/ids";
33
33
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
@@ -45,6 +45,7 @@ import {
45
45
  RejectCompletionButton,
46
46
  } from "./completion-handlers";
47
47
  import { addContextCompletion, getAICompletionBody } from "./completion-utils";
48
+ import { stagedAICellsAtom } from "@/core/ai/staged-cells";
48
49
 
49
50
  const Original = CodeMirrorMerge.Original;
50
51
  const Modified = CodeMirrorMerge.Modified;
@@ -123,7 +124,6 @@ export const AiCompletionEditor: React.FC<Props> = ({
123
124
  api: runtimeManager.getAiURL("completion").toString(),
124
125
  headers: runtimeManager.headers(),
125
126
  initialInput: initialPrompt,
126
- streamProtocol: "text",
127
127
  // Throttle the messages and data updates to 100ms
128
128
  experimental_throttle: 100,
129
129
  body: {
@@ -143,13 +143,14 @@ export const AiCompletionEditor: React.FC<Props> = ({
143
143
  });
144
144
  },
145
145
  onFinish: (_prompt, completion) => {
146
- // Remove trailing new lines
147
- setCompletion(completion.trimEnd());
146
+ setCompletion(stripWrappingBackticks(completion).trimEnd());
148
147
  },
149
148
  });
150
149
 
151
150
  const inputRef = React.useRef<ReactCodeMirrorRef>(null);
152
- const completion = untrimmedCompletion.trimEnd();
151
+ const completion = stripWrappingBackticks(untrimmedCompletion, {
152
+ streaming: isLoading,
153
+ }).trimEnd();
153
154
 
154
155
  const initialSubmit = useCallback(() => {
155
156
  if (triggerImmediately && !isLoading && initialPrompt) {
@@ -0,0 +1,133 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { stripWrappingBackticks } from "../strip-wrapping-backticks";
5
+
6
+ // Cases aligned with `without_wrapping_backticks` on a complete string (single chunk).
7
+ const CASES: [string[], string][] = [
8
+ [["Hello", " world", "!"], "Hello world!"],
9
+ [["```", "print('hello')", "print('world')"], "print('hello')print('world')"],
10
+ [
11
+ ["print('hello')", "print('world')", "```"],
12
+ "print('hello')print('world')```",
13
+ ],
14
+ [["```", "print('hello')", "```"], "print('hello')"],
15
+ [["Hello", " ``` ", "world"], "Hello ``` world"],
16
+ [["``", "`print('hello')", "``", "`"], "print('hello')"],
17
+ [["``", "`", "\n", "print('hello')", "\n", "``", "`"], "print('hello')"],
18
+ [
19
+ ["```\n", "print('hello')", "print('world')", "\n```"],
20
+ "print('hello')print('world')",
21
+ ],
22
+ [
23
+ ["```\nprint('hello')\n", "print('world')\n```"],
24
+ "print('hello')\nprint('world')",
25
+ ],
26
+ [
27
+ ["```\n", "def test():\n ", "return True\n```"],
28
+ "def test():\n return True",
29
+ ],
30
+ [[], ""],
31
+ [["```idk```"], "idk"],
32
+ [["Hello world"], "Hello world"],
33
+ [
34
+ ["```python\n", "def hello():\n ", "print('world')\n```"],
35
+ "def hello():\n print('world')",
36
+ ],
37
+ [
38
+ ["```python", "\ndef hello():\n ", "print('world')\n```"],
39
+ "def hello():\n print('world')",
40
+ ],
41
+ [
42
+ ["```sql", "SELECT * FROM table", " WHERE id = 1", "```"],
43
+ "SELECT * FROM table WHERE id = 1",
44
+ ],
45
+ [
46
+ ["```sql\n", "SELECT * FROM table\n", "WHERE id = 1\n```"],
47
+ "SELECT * FROM table\nWHERE id = 1",
48
+ ],
49
+ [["```", "print('hello')", "``` "], "print('hello') "],
50
+ [["```python\n", "print('hello')", "\n``` "], "print('hello') "],
51
+ [["```", "code", "```\t\n"], "code\t\n"],
52
+ [[" ```", "code", "```"], "code"],
53
+ [[" ```python\n", "code", "```"], "code"],
54
+ [["\t```", "code", "```"], "code"],
55
+ [["\n", "\n", "```\n", "code", "\n```\n"], "code\n"],
56
+ [["\n", "\n", "```python\n", "code", "\n```\n"], "code\n"],
57
+ [["\n``", "`python\n", "code", "\n```\n"], "code\n"],
58
+ [["\n`", "`", "`python\n", "code", "\n```\n"], "code\n"],
59
+ [["```python ", "code", "```"], " code"],
60
+ [["```python\t", "code", "```"], "\tcode"],
61
+ [["```\n", "```"], ""],
62
+ [["```python\n", "```"], ""],
63
+ [["```\n", "code"], "code"],
64
+ [["```python\n", "code"], "code"],
65
+ [["code", "\n```"], "code\n```"],
66
+ [
67
+ ["```\n", "x = 1\n", "```\n", "```\n", "y = 2\n", "```"],
68
+ "x = 1\n```\n```\ny = 2",
69
+ ],
70
+ [["```python\n", "s = 'use `backticks`'\n", "```"], "s = 'use `backticks`'"],
71
+ [["``", "`\n", "code\n", "``", "`"], "code"],
72
+ [["```", "python\n", "code\n", "```"], "code"],
73
+ [["```", "code", "```"], "code"],
74
+ [["prefix ", "```\n", "code\n", "```"], "prefix ```\ncode\n```"],
75
+ [["```\n", "code\n", "```", " suffix"], "code\n``` suffix"],
76
+ [["```\n\n", "code\n\n", "```"], "\ncode\n"],
77
+ [["```\n", " code \n", "```"], " code "],
78
+ [["```\n", "\tcode\t\n", "```"], "\tcode\t"],
79
+ [
80
+ ["```\n", "x\n", "```\n", "```python\n", "y\n", "```"],
81
+ "x\n```\n```python\ny",
82
+ ],
83
+ [["```javascript\n", "console.log()\n", "```"], "javascript\nconsole.log()"],
84
+ [["```markdown\n", "# Title\n", "```"], "# Title"],
85
+ ];
86
+
87
+ describe("stripWrappingBackticks", () => {
88
+ it.each(CASES)("strips fences for %j", (chunks, expected) => {
89
+ expect(stripWrappingBackticks(chunks.join(""))).toBe(expected);
90
+ });
91
+ });
92
+
93
+ // In streaming mode, the opening fence is stripped as soon as it is
94
+ // unambiguous, but the closing fence is left untouched (it may not have
95
+ // arrived yet, or a trailing "```" may be content).
96
+ const STREAMING_CASES: [string, string][] = [
97
+ // No fence: passthrough.
98
+ ["import pandas as pd", "import pandas as pd"],
99
+ // Plain opening fence stripped once the first line is terminated.
100
+ ["```\n", ""],
101
+ ["```\ncode", "code"],
102
+ ["```\ncode\nmore", "code\nmore"],
103
+ // Opening fence stripped, closing fence intentionally kept while streaming.
104
+ ["```\ncode\n```", "code\n```"],
105
+ ["```python\ncode\n```", "code\n```"],
106
+ // Language fences stripped as soon as the full identifier is present.
107
+ ["```python", ""],
108
+ ["```python\n", ""],
109
+ ["```python\ncode", "code"],
110
+ ["```sql\nSELECT 1", "SELECT 1"],
111
+ ["```markdown\n# Title", "# Title"],
112
+ // Leading whitespace before the fence.
113
+ [" ```python\ncode", "code"],
114
+ // Partial language identifiers are left intact until they resolve.
115
+ ["```", "```"],
116
+ ["```p", "```p"],
117
+ ["```py", "```py"],
118
+ ["```pyth", "```pyth"],
119
+ ["```s", "```s"],
120
+ ["```mark", "```mark"],
121
+ // First-line content that is not a known language prefix is stripped as a
122
+ // plain fence even before a newline arrives.
123
+ ["```x", "x"],
124
+ ["```x = 1", "x = 1"],
125
+ // Unsupported language identifiers are kept as content (matches final mode).
126
+ ["```json\ncode", "json\ncode"],
127
+ ];
128
+
129
+ describe("stripWrappingBackticks (streaming)", () => {
130
+ it.each(STREAMING_CASES)("strips opening fence for %j", (text, expected) => {
131
+ expect(stripWrappingBackticks(text, { streaming: true })).toBe(expected);
132
+ });
133
+ });
@@ -0,0 +1,48 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { consumeStream, parseJsonEventStream, uiMessageChunkSchema } from "ai";
4
+ import { stripWrappingBackticks } from "./strip-wrapping-backticks";
5
+
6
+ /**
7
+ * Read an AI SDK UI message stream response and return the assistant text.
8
+ */
9
+ export async function streamCompletionText(
10
+ response: Response,
11
+ ): Promise<string> {
12
+ if (!response.ok) {
13
+ throw new Error(await response.text());
14
+ }
15
+
16
+ if (!response.body) {
17
+ throw new Error("Failed to get response body");
18
+ }
19
+
20
+ let result = "";
21
+
22
+ await consumeStream({
23
+ stream: parseJsonEventStream({
24
+ stream: response.body,
25
+ schema: uiMessageChunkSchema,
26
+ }).pipeThrough(
27
+ new TransformStream({
28
+ transform(part) {
29
+ if (!part.success) {
30
+ throw part.error;
31
+ }
32
+
33
+ const streamPart = part.value;
34
+ if (streamPart.type === "text-delta") {
35
+ result += streamPart.delta;
36
+ } else if (streamPart.type === "error") {
37
+ throw new Error(streamPart.errorText);
38
+ }
39
+ },
40
+ }),
41
+ ),
42
+ onError: (error) => {
43
+ throw error;
44
+ },
45
+ });
46
+
47
+ return stripWrappingBackticks(result);
48
+ }
@@ -0,0 +1,88 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ const LANGS = ["python", "sql", "markdown"] as const;
4
+
5
+ interface StripOptions {
6
+ /**
7
+ * When true, the text is treated as a partial stream:
8
+ * - the opening fence is only stripped once we can be sure of its language
9
+ * identifier (e.g. "```py" is left untouched since it may still become
10
+ * "```python"), and
11
+ * - the closing fence is left in place, since it may not have arrived yet (or
12
+ * a trailing "```" may be part of the content).
13
+ */
14
+ streaming?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Removes wrapping markdown code fences from a completion string.
19
+ */
20
+ export function stripWrappingBackticks(
21
+ text: string,
22
+ opts: StripOptions = {},
23
+ ): string {
24
+ const { streaming = false } = opts;
25
+ const leadingWhitespace = text.match(/^\s*/)?.[0] ?? "";
26
+ const rest = text.slice(leadingWhitespace.length);
27
+
28
+ let body: string | null = null;
29
+ let strippedOpening = false;
30
+
31
+ for (const lang of LANGS) {
32
+ if (rest.startsWith(`\`\`\`${lang}`)) {
33
+ strippedOpening = true;
34
+ body = rest.slice(3 + lang.length);
35
+ if (body.startsWith("\n")) {
36
+ body = body.slice(1);
37
+ }
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (!strippedOpening && rest.startsWith("```")) {
43
+ const afterFence = rest.slice(3);
44
+ if (streaming && isPartialLanguageFence(afterFence)) {
45
+ return text;
46
+ }
47
+ strippedOpening = true;
48
+ body = afterFence;
49
+ if (body.startsWith("\n")) {
50
+ body = body.slice(1);
51
+ }
52
+ }
53
+
54
+ if (!strippedOpening || body === null) {
55
+ return text;
56
+ }
57
+
58
+ // While streaming, the closing fence may not have arrived yet, so leave the
59
+ // body as-is once the opening fence is removed.
60
+ if (streaming) {
61
+ return body;
62
+ }
63
+
64
+ const strippedEnd = body.trimEnd();
65
+ const trailingSpace = body.slice(strippedEnd.length);
66
+
67
+ if (strippedEnd.endsWith("\n```")) {
68
+ return strippedEnd.slice(0, -4) + trailingSpace;
69
+ }
70
+ if (strippedEnd.endsWith("```")) {
71
+ return strippedEnd.slice(0, -3) + trailingSpace;
72
+ }
73
+
74
+ return body;
75
+ }
76
+
77
+ /**
78
+ * Returns true if the text after an opening "```" has no terminating newline
79
+ * yet and could still become a known language identifier.
80
+ */
81
+ function isPartialLanguageFence(afterFence: string): boolean {
82
+ // Once the first line is terminated, we already know it is not a known
83
+ // language fence (those are handled above), so treat it as a plain fence.
84
+ if (afterFence.includes("\n")) {
85
+ return false;
86
+ }
87
+ return LANGS.some((lang) => lang.startsWith(afterFence));
88
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { waitForConnectionOpen } from "@/core/network/connection";
4
4
  import type { AiCompletionRequest } from "@/core/network/types";
5
+ import { streamCompletionText } from "@/core/ai/stream-completion-text";
5
6
  import { getRuntimeManager } from "@/core/runtime/config";
6
7
  import type { LanguageAdapterType } from "../language/types";
7
8
 
@@ -47,20 +48,7 @@ ${opts.codeAfter}
47
48
 
48
49
  const firstLineIndent = opts.selection.match(/^\s*/)?.[0] || "";
49
50
 
50
- const reader = response.body?.getReader();
51
- if (!reader) {
52
- throw new Error("Failed to get response reader");
53
- }
54
-
55
- let result = "";
56
- // oxlint-disable-next-line no-constant-condition
57
- while (true) {
58
- const { done, value } = await reader.read();
59
- if (done) {
60
- break;
61
- }
62
- result += new TextDecoder().decode(value);
63
- }
51
+ let result = await streamCompletionText(response);
64
52
 
65
53
  // Add back the indent if it was stripped, which can happen with
66
54
  // LLM responses
@@ -2,9 +2,12 @@
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
3
  import { variableName } from "@/__tests__/branded";
4
4
  import type { DatabaseSchema, DataTable } from "@/core/kernel/messages";
5
+ import { store } from "@/core/state/jotai";
5
6
  import type { VariableName } from "@/core/variables/types";
6
7
  import {
8
+ allTablesAtom,
7
9
  type DataSourceConnection,
10
+ dataSourceConnectionsAtom,
8
11
  type DataSourceState,
9
12
  exportedForTesting,
10
13
  type SQLTableContext,
@@ -575,3 +578,226 @@ describe("add table", () => {
575
578
  expect(db1?.schemas.length).toBe(1);
576
579
  });
577
580
  });
581
+
582
+ describe("nested namespaces", () => {
583
+ // Iceberg-style: database "top" with a schemaless schema and a nested
584
+ // namespace "nested" that has a deferred child namespace "deep".
585
+ const nestedConnections: DataSourceConnection[] = [
586
+ {
587
+ name: "ice" as ConnectionName,
588
+ source: "iceberg",
589
+ display_name: "Iceberg",
590
+ dialect: "iceberg",
591
+ databases: [
592
+ {
593
+ name: "top",
594
+ dialect: "iceberg",
595
+ schemas_resolved: true,
596
+ schemas: [
597
+ { name: "", tables: [], tables_resolved: true },
598
+ {
599
+ name: "nested",
600
+ tables: [],
601
+ tables_resolved: false,
602
+ child_schemas: [],
603
+ child_schemas_resolved: false,
604
+ },
605
+ ],
606
+ },
607
+ ],
608
+ },
609
+ ];
610
+
611
+ let baseState: DataSourceState;
612
+
613
+ beforeEach(() => {
614
+ baseState = addConnection(nestedConnections, initialState());
615
+ });
616
+
617
+ const findSchema = (
618
+ state: DataSourceState,
619
+ path: string[],
620
+ ): DatabaseSchema | undefined => {
621
+ const conn = state.connectionsMap.get("ice" as ConnectionName);
622
+ const db = conn?.databases.find((d) => d.name === "top");
623
+ let schemas = db?.schemas ?? [];
624
+ let found: DatabaseSchema | undefined;
625
+ for (const segment of path) {
626
+ found = schemas.find((s) => s.name === segment);
627
+ schemas = found?.child_schemas ?? [];
628
+ }
629
+ return found;
630
+ };
631
+
632
+ it("sets child namespaces at a nested path", () => {
633
+ const children: DatabaseSchema[] = [
634
+ { name: "deep", tables: [], tables_resolved: false },
635
+ ];
636
+ const newState = reducer(baseState, {
637
+ type: "addSchemaList",
638
+ payload: {
639
+ schemas: children,
640
+ sqlSchemaContext: {
641
+ engine: "ice",
642
+ database: "top",
643
+ schemaPath: ["nested"],
644
+ },
645
+ },
646
+ });
647
+
648
+ const nested = findSchema(newState, ["nested"]);
649
+ expect(nested?.child_schemas_resolved).toBe(true);
650
+ expect(nested?.child_schemas?.map((s) => s.name)).toEqual(["deep"]);
651
+ // The schemaless sibling is untouched.
652
+ expect(findSchema(newState, [""])?.name).toBe("");
653
+ });
654
+
655
+ it("sets tables at a nested path", () => {
656
+ const tables: DataTable[] = [
657
+ {
658
+ name: "table4",
659
+ columns: [],
660
+ num_columns: 0,
661
+ num_rows: 0,
662
+ variable_name: null,
663
+ source: "iceberg",
664
+ source_type: "catalog",
665
+ type: "table",
666
+ },
667
+ ];
668
+ const newState = reducer(baseState, {
669
+ type: "addTableList",
670
+ payload: {
671
+ tables,
672
+ sqlTableContext: {
673
+ engine: "ice",
674
+ database: "top",
675
+ schema: "nested",
676
+ dialect: "iceberg",
677
+ schemaPath: ["nested"],
678
+ },
679
+ },
680
+ });
681
+
682
+ const nested = findSchema(newState, ["nested"]);
683
+ expect(nested?.tables_resolved).toBe(true);
684
+ expect(nested?.tables.map((t) => t.name)).toEqual(["table4"]);
685
+ });
686
+
687
+ it("replaces a single table at a nested path", () => {
688
+ const makeTable = (numRows: number): DataTable => ({
689
+ name: "table4",
690
+ columns: [],
691
+ num_columns: 0,
692
+ num_rows: numRows,
693
+ variable_name: null,
694
+ source: "iceberg",
695
+ source_type: "catalog",
696
+ type: "table",
697
+ });
698
+ const context = {
699
+ engine: "ice",
700
+ database: "top",
701
+ schema: "nested",
702
+ dialect: "iceberg",
703
+ schemaPath: ["nested"],
704
+ };
705
+ let state = reducer(baseState, {
706
+ type: "addTableList",
707
+ payload: { tables: [makeTable(1)], sqlTableContext: context },
708
+ });
709
+ state = reducer(state, {
710
+ type: "addTable",
711
+ payload: { table: makeTable(42), sqlTableContext: context },
712
+ });
713
+
714
+ const nested = findSchema(state, ["nested"]);
715
+ expect(nested?.tables).toHaveLength(1);
716
+ expect(nested?.tables[0].num_rows).toBe(42);
717
+ });
718
+
719
+ it("does not change anything for a missing nested path", () => {
720
+ const newState = reducer(baseState, {
721
+ type: "addSchemaList",
722
+ payload: {
723
+ schemas: [{ name: "deep", tables: [] }],
724
+ sqlSchemaContext: {
725
+ engine: "ice",
726
+ database: "top",
727
+ schemaPath: ["does_not_exist"],
728
+ },
729
+ },
730
+ });
731
+ // The result is unchanged: nested namespace stays unresolved and the
732
+ // database keeps its two schemas (schemaless + nested).
733
+ const newDb = newState.connectionsMap
734
+ .get("ice" as ConnectionName)
735
+ ?.databases.find((d) => d.name === "top");
736
+ expect(findSchema(newState, ["nested"])?.child_schemas_resolved).toBe(
737
+ false,
738
+ );
739
+ expect(newDb?.schemas.length).toBe(2);
740
+ });
741
+ });
742
+
743
+ describe("allTablesAtom with nested namespaces", () => {
744
+ it("enumerates tables from nested namespaces", () => {
745
+ const table = (name: string): DataTable => ({
746
+ name,
747
+ columns: [],
748
+ num_columns: 0,
749
+ num_rows: 0,
750
+ variable_name: null,
751
+ source: "iceberg",
752
+ source_type: "catalog",
753
+ type: "table",
754
+ });
755
+
756
+ const state = addConnection(
757
+ [
758
+ {
759
+ name: "ice" as ConnectionName,
760
+ source: "iceberg",
761
+ display_name: "Iceberg",
762
+ dialect: "iceberg",
763
+ databases: [
764
+ {
765
+ name: "top",
766
+ dialect: "iceberg",
767
+ schemas_resolved: true,
768
+ schemas: [
769
+ {
770
+ name: "",
771
+ tables: [table("toptable")],
772
+ tables_resolved: true,
773
+ },
774
+ {
775
+ name: "nested",
776
+ tables: [table("nestedtable")],
777
+ tables_resolved: true,
778
+ child_schemas_resolved: true,
779
+ child_schemas: [
780
+ {
781
+ name: "deep",
782
+ tables: [table("deeptable")],
783
+ tables_resolved: true,
784
+ child_schemas: [],
785
+ child_schemas_resolved: true,
786
+ },
787
+ ],
788
+ },
789
+ ],
790
+ },
791
+ ],
792
+ },
793
+ ],
794
+ initialState(),
795
+ );
796
+
797
+ store.set(dataSourceConnectionsAtom, state);
798
+ const names = [...store.get(allTablesAtom).values()].map((t) => t.name);
799
+ expect(names).toContain("toptable");
800
+ expect(names).toContain("nestedtable");
801
+ expect(names).toContain("deeptable");
802
+ });
803
+ });