@marimo-team/islands 0.23.12-dev20 → 0.23.12-dev23
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.
- package/dist/{code-visibility-02AuLxDs.js → code-visibility-BbZRGQaf.js} +1 -1
- package/dist/main.js +3 -3
- package/dist/{reveal-component-CX0nM3qj.js → reveal-component-D3ZnoXh6.js} +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +48 -2
- package/src/components/editor/ai/completion-utils.ts +54 -36
- package/src/components/editor/app-container.tsx +3 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout-wrapper.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
type Mock,
|
|
9
|
+
vi,
|
|
10
|
+
} from "vitest";
|
|
3
11
|
import { variableName } from "@/__tests__/branded";
|
|
12
|
+
import * as aiContext from "@/core/ai/context/context";
|
|
4
13
|
import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
5
14
|
import { dataSourceConnectionsAtom } from "@/core/datasets/data-source-connections";
|
|
6
15
|
import { DUCKDB_ENGINE } from "@/core/datasets/engines";
|
|
@@ -8,10 +17,11 @@ import { datasetsAtom } from "@/core/datasets/state";
|
|
|
8
17
|
import type { DatasetsState } from "@/core/datasets/types";
|
|
9
18
|
import { store } from "@/core/state/jotai";
|
|
10
19
|
import { variablesAtom } from "@/core/variables/state";
|
|
11
|
-
import type { UIMessage } from "ai";
|
|
20
|
+
import type { FileUIPart, UIMessage } from "ai";
|
|
12
21
|
import {
|
|
13
22
|
codeToCells,
|
|
14
23
|
getAICompletionBody,
|
|
24
|
+
getAICompletionBodyWithAttachments,
|
|
15
25
|
isContextAttachment,
|
|
16
26
|
MARIMO_CONTEXT_PART_TYPE,
|
|
17
27
|
resolveChatContext,
|
|
@@ -440,6 +450,42 @@ describe("isContextAttachment", () => {
|
|
|
440
450
|
});
|
|
441
451
|
});
|
|
442
452
|
|
|
453
|
+
describe("context attachment stamping", () => {
|
|
454
|
+
const rawAttachment: FileUIPart = {
|
|
455
|
+
type: "file",
|
|
456
|
+
mediaType: "image/png",
|
|
457
|
+
url: "data:image/png;base64,abc",
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
vi.spyOn(aiContext, "getAIContextRegistry").mockReturnValue({
|
|
462
|
+
parseAllContextIds: () => ["data://t1"],
|
|
463
|
+
formatContextForAI: () => '<data name="t1" />',
|
|
464
|
+
getAttachmentsForContext: async () => [rawAttachment],
|
|
465
|
+
} as unknown as ReturnType<typeof aiContext.getAIContextRegistry>);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
afterEach(() => {
|
|
469
|
+
vi.restoreAllMocks();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("stamps chat attachments as context-derived", async () => {
|
|
473
|
+
const { attachments } = await resolveChatContext("see @data://t1");
|
|
474
|
+
expect(attachments).toHaveLength(1);
|
|
475
|
+
expect(isContextAttachment(attachments[0])).toBe(true);
|
|
476
|
+
// The original attachment is left untouched (we return a stamped copy).
|
|
477
|
+
expect(rawAttachment.providerMetadata).toBeUndefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("stamps completion attachments the same way as chat", async () => {
|
|
481
|
+
const { attachments } = await getAICompletionBodyWithAttachments({
|
|
482
|
+
input: "see @data://t1",
|
|
483
|
+
});
|
|
484
|
+
expect(attachments).toHaveLength(1);
|
|
485
|
+
expect(isContextAttachment(attachments[0])).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
443
489
|
describe("codeToCells", () => {
|
|
444
490
|
it("should return empty array for empty string", () => {
|
|
445
491
|
const code = "";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
|
10
10
|
import type { DataUIPart, FileUIPart, UIMessage } from "ai";
|
|
11
11
|
import { getAIContextRegistry } from "@/core/ai/context/context";
|
|
12
|
+
import type { ContextLocatorId } from "@/core/ai/context/registry";
|
|
12
13
|
import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
13
14
|
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
|
|
14
15
|
import type { AiCompletionRequest } from "@/core/network/types";
|
|
@@ -89,20 +90,51 @@ export function isContextAttachment(part: UIMessage["parts"][number]): boolean {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
93
|
+
* Stamp a context-derived attachment with a provenance marker.
|
|
94
|
+
*
|
|
95
|
+
* Some @-mentions resolve to file attachments (e.g. a cell's image output),
|
|
96
|
+
* which get appended to the user message right alongside files the user
|
|
97
|
+
* uploaded by hand. Once they're in the message the two are indistinguishable,
|
|
98
|
+
* so we mark the context-derived ones. This matters on message edit: we
|
|
99
|
+
* re-resolve context from the edited text, and `isContextAttachment` lets us
|
|
100
|
+
* drop only the stale context attachments while preserving the user's own
|
|
101
|
+
* uploads
|
|
94
102
|
*/
|
|
95
|
-
|
|
103
|
+
function stampContextAttachment(attachment: FileUIPart): FileUIPart {
|
|
104
|
+
return {
|
|
105
|
+
...attachment,
|
|
106
|
+
providerMetadata: {
|
|
107
|
+
...attachment.providerMetadata,
|
|
108
|
+
// Merge within the `marimo` namespace so we don't clobber any other
|
|
109
|
+
// marimo metadata a provider may have already set.
|
|
110
|
+
marimo: {
|
|
111
|
+
...attachment.providerMetadata?.marimo,
|
|
112
|
+
...CONTEXT_ATTACHMENT_METADATA.marimo,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ResolvedContext {
|
|
119
|
+
plainText: string;
|
|
120
|
+
contextIds: ContextLocatorId[];
|
|
121
|
+
attachments: FileUIPart[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse @-context for messages
|
|
126
|
+
*/
|
|
127
|
+
async function resolveContextAttachments(
|
|
96
128
|
input: string,
|
|
97
|
-
): Promise<
|
|
129
|
+
): Promise<ResolvedContext> {
|
|
98
130
|
if (!input.includes(CONTEXT_TRIGGER)) {
|
|
99
|
-
return {
|
|
131
|
+
return { plainText: "", contextIds: [], attachments: [] };
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
const registry = getAIContextRegistry(store);
|
|
103
135
|
const contextIds = registry.parseAllContextIds(input);
|
|
104
136
|
if (contextIds.length === 0) {
|
|
105
|
-
return {
|
|
137
|
+
return { plainText: "", contextIds: [], attachments: [] };
|
|
106
138
|
}
|
|
107
139
|
|
|
108
140
|
const plainText = registry.formatContextForAI(contextIds);
|
|
@@ -110,20 +142,24 @@ export async function resolveChatContext(
|
|
|
110
142
|
let attachments: FileUIPart[] = [];
|
|
111
143
|
try {
|
|
112
144
|
const resolved = await registry.getAttachmentsForContext(contextIds);
|
|
113
|
-
attachments = resolved.map(
|
|
114
|
-
...attachment,
|
|
115
|
-
providerMetadata: {
|
|
116
|
-
...attachment.providerMetadata,
|
|
117
|
-
marimo: {
|
|
118
|
-
...attachment.providerMetadata?.marimo,
|
|
119
|
-
...CONTEXT_ATTACHMENT_METADATA.marimo,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}));
|
|
145
|
+
attachments = resolved.map(stampContextAttachment);
|
|
123
146
|
} catch (error) {
|
|
124
147
|
Logger.error("Error getting attachments:", error);
|
|
125
148
|
}
|
|
126
149
|
|
|
150
|
+
return { plainText, contextIds, attachments };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve @-context for messages. They represent referenced
|
|
155
|
+
* datasets, variables, or other context from the user's prompt.
|
|
156
|
+
*/
|
|
157
|
+
export async function resolveChatContext(
|
|
158
|
+
input: string,
|
|
159
|
+
): Promise<ResolvedChatContext> {
|
|
160
|
+
const { plainText, contextIds, attachments } =
|
|
161
|
+
await resolveContextAttachments(input);
|
|
162
|
+
|
|
127
163
|
let contextPart: MarimoContextUIPart | null = null;
|
|
128
164
|
if (plainText.trim()) {
|
|
129
165
|
contextPart = {
|
|
@@ -141,31 +177,13 @@ export async function resolveChatContext(
|
|
|
141
177
|
export async function getAICompletionBodyWithAttachments({
|
|
142
178
|
input,
|
|
143
179
|
}: Opts): Promise<AICompletionBodyWithAttachments> {
|
|
144
|
-
|
|
145
|
-
let attachments: FileUIPart[] = [];
|
|
146
|
-
|
|
147
|
-
// Skip if no '@' in the input
|
|
148
|
-
if (input.includes("@")) {
|
|
149
|
-
const registry = getAIContextRegistry(store);
|
|
150
|
-
const contextIds = registry.parseAllContextIds(input);
|
|
151
|
-
|
|
152
|
-
// Get context string
|
|
153
|
-
contextString = registry.formatContextForAI(contextIds);
|
|
154
|
-
|
|
155
|
-
// Get attachments
|
|
156
|
-
try {
|
|
157
|
-
attachments = await registry.getAttachmentsForContext(contextIds);
|
|
158
|
-
Logger.debug("Included attachments", attachments.length);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
Logger.error("Error getting attachments:", error);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
180
|
+
const { plainText, attachments } = await resolveContextAttachments(input);
|
|
163
181
|
|
|
164
182
|
return {
|
|
165
183
|
body: {
|
|
166
184
|
includeOtherCode: getCodes(""),
|
|
167
185
|
context: {
|
|
168
|
-
plainText
|
|
186
|
+
plainText,
|
|
169
187
|
schema: [],
|
|
170
188
|
variables: [],
|
|
171
189
|
},
|
|
@@ -48,7 +48,9 @@ export const AppContainer: React.FC<PropsWithChildren<Props>> = ({
|
|
|
48
48
|
"bg-background w-full h-full text-textColor",
|
|
49
49
|
"flex flex-col overflow-y-auto",
|
|
50
50
|
width === "full" && "config-width-full",
|
|
51
|
-
width === "columns"
|
|
51
|
+
width === "columns"
|
|
52
|
+
? "overflow-x-auto"
|
|
53
|
+
: "overflow-x-auto sm:overflow-x-hidden",
|
|
52
54
|
"print:height-fit",
|
|
53
55
|
)}
|
|
54
56
|
>
|
|
@@ -32,9 +32,9 @@ export const VerticalLayoutWrapper: React.FC<PropsWithChildren<Props>> = ({
|
|
|
32
32
|
// This padding needs to be the same from above to be correctly applied
|
|
33
33
|
"pb-24 sm:pb-12",
|
|
34
34
|
appConfig.width === "compact" &&
|
|
35
|
-
"max-w-(--content-width) min-w-[400px]",
|
|
35
|
+
"max-w-(--content-width) sm:min-w-[400px]",
|
|
36
36
|
appConfig.width === "medium" &&
|
|
37
|
-
"max-w-(--content-width-medium) min-w-[400px]",
|
|
37
|
+
"max-w-(--content-width-medium) sm:min-w-[400px]",
|
|
38
38
|
appConfig.width === "columns" && "w-fit",
|
|
39
39
|
appConfig.width === "full" && "max-w-full",
|
|
40
40
|
// Hide the cells for a fake loading effect, to avoid flickering
|