@marimo-team/frontend 0.24.1-dev57 → 0.24.1-dev59
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/assets/add-cell-with-ai-DVGZNNUv.js +76 -0
- package/dist/assets/{agent-panel-D7mo8YLQ.js → agent-panel-BTY3oeHD.js} +3 -3
- package/dist/assets/cell-editor-D3YsOG7a.js +21 -0
- package/dist/assets/{chat-display-Cgg0xLir.js → chat-display-NTNQQyoE.js} +1 -1
- package/dist/assets/chat-panel-Bow0QKrA.js +4 -0
- package/dist/assets/{chat-ui-DYLpA8l4.js → chat-ui-Cg69zAqt.js} +4 -4
- package/dist/assets/{command-palette-DiT0E_K7.js → command-palette-QFmIvDcT.js} +1 -1
- package/dist/assets/{edit-page-DAC4cNKe.js → edit-page-ds4edlkF.js} +6 -6
- package/dist/assets/{index-DTQ4RS9q.js → index-B36dhEV6.js} +3 -3
- package/dist/assets/index-ecw9ngZu.css +2 -0
- package/dist/assets/{layout-D3zwsTvR.js → layout-iNJ-1JI2.js} +2 -2
- package/dist/assets/{panels-ChFSBlsU.js → panels-BbkuRtEi.js} +1 -1
- package/dist/assets/{reveal-component-BFRmOcQr.js → reveal-component-C4pF9OE7.js} +1 -1
- package/dist/assets/{run-page-MZCCEXct.js → run-page-Bo57bjbp.js} +1 -1
- package/dist/assets/{scratchpad-panel-BqYT2vj3.js → scratchpad-panel-vBIq9BfA.js} +1 -1
- package/dist/assets/{skeleton-gmxAkHfa.js → skeleton-B5iAa5n_.js} +1 -1
- package/dist/assets/{useNotebookActions-BoFJmccb.js → useNotebookActions-DGuDG0Px.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +0 -178
- package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +106 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +83 -62
- package/src/components/editor/ai/ai-completion-editor.tsx +71 -37
- package/src/components/editor/ai/completion-handlers.tsx +4 -0
- package/src/components/editor/ai/completion-utils.ts +0 -92
- package/src/components/editor/ai/staged-cell-submission.ts +46 -0
- package/src/components/editor/cell/StagedAICell.tsx +4 -2
- package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +64 -0
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +77 -0
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +8 -2
- package/src/core/ai/__tests__/staged-cells.test.ts +442 -125
- package/src/core/ai/__tests__/stream-completion-text.test.ts +74 -0
- package/src/core/ai/completion-output.ts +56 -0
- package/src/core/ai/staged-cells.ts +245 -164
- package/src/core/ai/stream-completion-text.ts +23 -6
- package/src/css/globals.css +1 -0
- package/dist/assets/add-cell-with-ai-0dqk0f7O.js +0 -77
- package/dist/assets/cell-editor-e7_o4mEh.js +0 -24
- package/dist/assets/chat-panel-BaG-5LiN.js +0 -4
- package/dist/assets/index-BrmH4-pI.css +0 -2
- package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +0 -133
- package/src/core/ai/strip-wrapping-backticks.ts +0 -88
|
@@ -19,7 +19,6 @@ import { store } from "@/core/state/jotai";
|
|
|
19
19
|
import { variablesAtom } from "@/core/variables/state";
|
|
20
20
|
import type { FileUIPart, UIMessage } from "ai";
|
|
21
21
|
import {
|
|
22
|
-
codeToCells,
|
|
23
22
|
getAICompletionBody,
|
|
24
23
|
getAICompletionBodyWithAttachments,
|
|
25
24
|
isContextAttachment,
|
|
@@ -485,180 +484,3 @@ describe("context attachment stamping", () => {
|
|
|
485
484
|
expect(isContextAttachment(attachments[0])).toBe(true);
|
|
486
485
|
});
|
|
487
486
|
});
|
|
488
|
-
|
|
489
|
-
describe("codeToCells", () => {
|
|
490
|
-
it("should return empty array for empty string", () => {
|
|
491
|
-
const code = "";
|
|
492
|
-
const result = codeToCells(code);
|
|
493
|
-
expect(result).toEqual([]);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it("should return empty array for whitespace only", () => {
|
|
497
|
-
const code = " \n\t ";
|
|
498
|
-
const result = codeToCells(code);
|
|
499
|
-
expect(result).toEqual([]);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it("should convert code without backticks to single python cell", () => {
|
|
503
|
-
const code = "print('Hello, world!')";
|
|
504
|
-
const result = codeToCells(code);
|
|
505
|
-
expect(result).toEqual([
|
|
506
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
507
|
-
]);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it("should convert code with single closed backticks to cells", () => {
|
|
511
|
-
const code = "```python\nprint('Hello, world!')\n```";
|
|
512
|
-
const result = codeToCells(code);
|
|
513
|
-
expect(result).toEqual([
|
|
514
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
515
|
-
]);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("should convert code with unclosed backticks to cells", () => {
|
|
519
|
-
const code = "```python\nprint('Hello, world!')\n";
|
|
520
|
-
const result = codeToCells(code);
|
|
521
|
-
expect(result).toEqual([
|
|
522
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
523
|
-
]);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it("should convert code with multiple closed cells", () => {
|
|
527
|
-
const code =
|
|
528
|
-
"```python\nprint('Hello, world!')\n```\n```sql\nSELECT * FROM users\n```";
|
|
529
|
-
const result = codeToCells(code);
|
|
530
|
-
expect(result).toEqual([
|
|
531
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
532
|
-
{ language: "sql", code: "SELECT * FROM users" },
|
|
533
|
-
]);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it("should handle code with no language identifier", () => {
|
|
537
|
-
const code = "```\nprint('Hello, world!')\n```";
|
|
538
|
-
const result = codeToCells(code);
|
|
539
|
-
expect(result).toEqual([
|
|
540
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
541
|
-
]);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("should handle unclosed code with no language identifier", () => {
|
|
545
|
-
const code = "```\nprint('Hello, world!')\n";
|
|
546
|
-
const result = codeToCells(code);
|
|
547
|
-
expect(result).toEqual([
|
|
548
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
549
|
-
]);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("should handle markdown language", () => {
|
|
553
|
-
const code = "```markdown\n# Hello, world!\n```";
|
|
554
|
-
const result = codeToCells(code);
|
|
555
|
-
expect(result).toEqual([{ language: "markdown", code: "# Hello, world!" }]);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("should handle sql language", () => {
|
|
559
|
-
const code = "```sql\nSELECT * FROM users\n```";
|
|
560
|
-
const result = codeToCells(code);
|
|
561
|
-
expect(result).toEqual([{ language: "sql", code: "SELECT * FROM users" }]);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it("should handle unclosed markdown cell", () => {
|
|
565
|
-
const code = "```markdown\n# Hello, world!\n";
|
|
566
|
-
const result = codeToCells(code);
|
|
567
|
-
expect(result).toEqual([{ language: "markdown", code: "# Hello, world!" }]);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it("should handle unclosed sql cell", () => {
|
|
571
|
-
const code = "```sql\nSELECT * FROM users\n";
|
|
572
|
-
const result = codeToCells(code);
|
|
573
|
-
expect(result).toEqual([{ language: "sql", code: "SELECT * FROM users" }]);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it("should handle empty cells and skip them", () => {
|
|
577
|
-
const code = "```python\n\n```\n```sql\nSELECT * FROM users\n```";
|
|
578
|
-
const result = codeToCells(code);
|
|
579
|
-
expect(result).toEqual([{ language: "sql", code: "SELECT * FROM users" }]);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it("should handle cells with only whitespace and skip them", () => {
|
|
583
|
-
const code = "```python\n \n```\n```sql\nSELECT * FROM users\n```";
|
|
584
|
-
const result = codeToCells(code);
|
|
585
|
-
expect(result).toEqual([{ language: "sql", code: "SELECT * FROM users" }]);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it("should handle code with trailing newlines", () => {
|
|
589
|
-
const code = "```python\nprint('Hello, world!')\n\n\n```";
|
|
590
|
-
const result = codeToCells(code);
|
|
591
|
-
expect(result).toEqual([
|
|
592
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
593
|
-
]);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it("should handle unclosed code with trailing newlines", () => {
|
|
597
|
-
const code = "```python\nprint('Hello, world!')\n\n\n";
|
|
598
|
-
const result = codeToCells(code);
|
|
599
|
-
expect(result).toEqual([
|
|
600
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
601
|
-
]);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it("should handle multiple cells with different languages", () => {
|
|
605
|
-
const code =
|
|
606
|
-
"```python\nprint('Hello, world!')\n```\n```sql\nSELECT * FROM users\n```\n```markdown\n# Title\nThis is markdown\n```";
|
|
607
|
-
|
|
608
|
-
const result = codeToCells(code);
|
|
609
|
-
expect(result).toEqual([
|
|
610
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
611
|
-
{ language: "sql", code: "SELECT * FROM users" },
|
|
612
|
-
{ language: "markdown", code: "# Title\nThis is markdown" },
|
|
613
|
-
]);
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it("should handle complex multiline code", () => {
|
|
617
|
-
const code =
|
|
618
|
-
'```python\ndef hello():\n print("Hello, world!")\n return "success"\n\nhello()\n```\n```sql\nSELECT \n id,\n name,\n email\nFROM users\nWHERE active = true\nORDER BY name;\n```';
|
|
619
|
-
|
|
620
|
-
const result = codeToCells(code);
|
|
621
|
-
expect(result).toEqual([
|
|
622
|
-
{
|
|
623
|
-
language: "python",
|
|
624
|
-
code: 'def hello():\n print("Hello, world!")\n return "success"\n\nhello()',
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
language: "sql",
|
|
628
|
-
code: "SELECT \n id,\n name,\n email\nFROM users\nWHERE active = true\nORDER BY name;",
|
|
629
|
-
},
|
|
630
|
-
]);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it("should handle code with backticks in the content", () => {
|
|
634
|
-
const code = "```python\nprint('```')\n```";
|
|
635
|
-
const result = codeToCells(code);
|
|
636
|
-
expect(result).toEqual([{ language: "python", code: "print('" }]);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it("should handle code with no backticks in the last cell", () => {
|
|
640
|
-
const code =
|
|
641
|
-
"```python\nprint('Hello, world!')\n```\n```python\nprint('Hello, world!')";
|
|
642
|
-
const result = codeToCells(code);
|
|
643
|
-
expect(result).toEqual([
|
|
644
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
645
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
646
|
-
]);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
it("should handle case insensitive language detection", () => {
|
|
650
|
-
const code = "```PYTHON\nprint('Hello, world!')\n```";
|
|
651
|
-
const result = codeToCells(code);
|
|
652
|
-
expect(result).toEqual([
|
|
653
|
-
{ language: "python", code: "print('Hello, world!')" },
|
|
654
|
-
]);
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
it("should handle unknown language", { fails: true }, () => {
|
|
658
|
-
const code = "```javascript\nconsole.log('Hello, world!')\n```";
|
|
659
|
-
const result = codeToCells(code);
|
|
660
|
-
expect(result).toEqual([
|
|
661
|
-
{ language: "javascript", code: "console.log('Hello, world!')" },
|
|
662
|
-
]);
|
|
663
|
-
});
|
|
664
|
-
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Deferred } from "@/utils/Deferred";
|
|
5
|
+
import { StagedCellSubmissionController } from "../staged-cell-submission";
|
|
6
|
+
|
|
7
|
+
describe("StagedCellSubmissionController", () => {
|
|
8
|
+
it("ignores duplicate submissions while preprocessing", async () => {
|
|
9
|
+
const preparation = new Deferred<string>();
|
|
10
|
+
const submit = vi.fn(async () => undefined);
|
|
11
|
+
const onError = vi.fn();
|
|
12
|
+
const controller = new StagedCellSubmissionController();
|
|
13
|
+
|
|
14
|
+
const first = controller.run({
|
|
15
|
+
prepare: () => preparation.promise,
|
|
16
|
+
submit,
|
|
17
|
+
onError,
|
|
18
|
+
});
|
|
19
|
+
await controller.run({
|
|
20
|
+
prepare: vi.fn(async () => "duplicate"),
|
|
21
|
+
submit,
|
|
22
|
+
onError,
|
|
23
|
+
});
|
|
24
|
+
preparation.resolve("prepared");
|
|
25
|
+
await first;
|
|
26
|
+
|
|
27
|
+
expect(submit).toHaveBeenCalledExactlyOnceWith("prepared");
|
|
28
|
+
expect(onError).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not submit after cancellation during preprocessing", async () => {
|
|
32
|
+
const preparation = new Deferred<string>();
|
|
33
|
+
const submit = vi.fn(async () => undefined);
|
|
34
|
+
const onError = vi.fn();
|
|
35
|
+
const controller = new StagedCellSubmissionController();
|
|
36
|
+
|
|
37
|
+
const run = controller.run({
|
|
38
|
+
prepare: () => preparation.promise,
|
|
39
|
+
submit,
|
|
40
|
+
onError,
|
|
41
|
+
});
|
|
42
|
+
controller.cancel();
|
|
43
|
+
preparation.resolve("prepared");
|
|
44
|
+
await run;
|
|
45
|
+
|
|
46
|
+
expect(submit).not.toHaveBeenCalled();
|
|
47
|
+
expect(onError).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("blocks another submission until a cancelled submission settles", async () => {
|
|
51
|
+
const submission = new Deferred<void>();
|
|
52
|
+
const firstSubmit = vi.fn(() => submission.promise);
|
|
53
|
+
const secondSubmit = vi.fn(async () => undefined);
|
|
54
|
+
const onError = vi.fn();
|
|
55
|
+
const controller = new StagedCellSubmissionController();
|
|
56
|
+
|
|
57
|
+
const firstRun = controller.run({
|
|
58
|
+
prepare: async () => "first",
|
|
59
|
+
submit: firstSubmit,
|
|
60
|
+
onError,
|
|
61
|
+
});
|
|
62
|
+
await vi.waitFor(() => {
|
|
63
|
+
expect(firstSubmit).toHaveBeenCalledExactlyOnceWith("first");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
controller.cancel();
|
|
67
|
+
await controller.run({
|
|
68
|
+
prepare: async () => "second",
|
|
69
|
+
submit: secondSubmit,
|
|
70
|
+
onError,
|
|
71
|
+
});
|
|
72
|
+
expect(secondSubmit).not.toHaveBeenCalled();
|
|
73
|
+
|
|
74
|
+
submission.resolve();
|
|
75
|
+
await firstRun;
|
|
76
|
+
await controller.run({
|
|
77
|
+
prepare: async () => "second",
|
|
78
|
+
submit: secondSubmit,
|
|
79
|
+
onError,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(secondSubmit).toHaveBeenCalledExactlyOnceWith("second");
|
|
83
|
+
expect(onError).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("reports preprocessing and submission errors", async () => {
|
|
87
|
+
const preprocessingError = new Error("preprocessing failed");
|
|
88
|
+
const submissionError = new Error("submission failed");
|
|
89
|
+
const onError = vi.fn();
|
|
90
|
+
const controller = new StagedCellSubmissionController();
|
|
91
|
+
|
|
92
|
+
await controller.run({
|
|
93
|
+
prepare: async () => Promise.reject(preprocessingError),
|
|
94
|
+
submit: vi.fn(),
|
|
95
|
+
onError,
|
|
96
|
+
});
|
|
97
|
+
await controller.run({
|
|
98
|
+
prepare: async () => "prepared",
|
|
99
|
+
submit: async () => Promise.reject(submissionError),
|
|
100
|
+
onError,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(onError).toHaveBeenNthCalledWith(1, preprocessingError);
|
|
104
|
+
expect(onError).toHaveBeenNthCalledWith(2, submissionError);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
SparklesIcon,
|
|
25
25
|
XIcon,
|
|
26
26
|
} from "lucide-react";
|
|
27
|
-
import { useMemo, useRef, useState } from "react";
|
|
27
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
28
28
|
import useEvent from "react-use-event-hook";
|
|
29
|
+
import { DefaultChatTransport } from "ai";
|
|
29
30
|
import { z } from "zod";
|
|
30
31
|
import { AIModelDropdown } from "@/components/ai/ai-model-dropdown";
|
|
31
32
|
import {
|
|
@@ -37,7 +38,6 @@ import {
|
|
|
37
38
|
import {
|
|
38
39
|
buildCompletionRequestBody,
|
|
39
40
|
convertToFileUIPart,
|
|
40
|
-
handleToolCall,
|
|
41
41
|
PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
|
|
42
42
|
useFileState,
|
|
43
43
|
} from "@/components/chat/chat-utils";
|
|
@@ -52,13 +52,11 @@ import {
|
|
|
52
52
|
import { toast } from "@/components/ui/use-toast";
|
|
53
53
|
import { AiModelId } from "@/core/ai/ids/ids";
|
|
54
54
|
import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
|
|
55
|
-
import {
|
|
56
|
-
import
|
|
57
|
-
import { useCellActions } from "@/core/cells/cells";
|
|
55
|
+
import type { CompletionUIMessage } from "@/core/ai/completion-output";
|
|
56
|
+
import { useStagedCellGeneration } from "@/core/ai/staged-cells";
|
|
58
57
|
import { resourceExtension } from "@/core/codemirror/ai/resources";
|
|
59
58
|
import { aiAtom } from "@/core/config/config";
|
|
60
59
|
import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
|
|
61
|
-
import { useRequestClient } from "@/core/network/requests";
|
|
62
60
|
import type { AiCompletionRequest } from "@/core/network/types";
|
|
63
61
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
64
62
|
import { useTheme } from "@/theme/useTheme";
|
|
@@ -73,7 +71,7 @@ import {
|
|
|
73
71
|
CONTEXT_TRIGGER,
|
|
74
72
|
mentionsCompletionSource,
|
|
75
73
|
} from "./completion-utils";
|
|
76
|
-
import {
|
|
74
|
+
import { StagedCellSubmissionController } from "./staged-cell-submission";
|
|
77
75
|
|
|
78
76
|
// Persist across sessions
|
|
79
77
|
const languageAtom = atomWithStorage<"python" | "sql">(
|
|
@@ -95,78 +93,95 @@ export const AddCellWithAI: React.FC<{
|
|
|
95
93
|
const store = useStore();
|
|
96
94
|
const [input, setInput] = useState("");
|
|
97
95
|
|
|
98
|
-
const {
|
|
99
|
-
|
|
96
|
+
const {
|
|
97
|
+
acceptOwnedStagedCells,
|
|
98
|
+
beginStagedCellGeneration,
|
|
99
|
+
discardOwnedStagedCells,
|
|
100
|
+
finishStagedCellGeneration,
|
|
101
|
+
hasOwnedStagedCells,
|
|
102
|
+
onData,
|
|
103
|
+
} = useStagedCellGeneration(store);
|
|
100
104
|
const [language, setLanguage] = useAtom(languageAtom);
|
|
101
105
|
const runtimeManager = useRuntimeManager();
|
|
102
|
-
const { invokeAiTool, sendRun } = useRequestClient();
|
|
103
106
|
|
|
104
|
-
const stagedAICells = useAtomValue(stagedAICellsAtom);
|
|
105
107
|
const inputRef = useRef<ReactCodeMirrorRef>(null);
|
|
108
|
+
const submissionController = useRef(
|
|
109
|
+
new StagedCellSubmissionController(),
|
|
110
|
+
).current;
|
|
106
111
|
|
|
107
112
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
108
113
|
const { files, addFiles, removeFile } = useFileState();
|
|
109
114
|
const aiConfig = useAtomValue(aiAtom);
|
|
110
115
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
addStagedCell,
|
|
115
|
-
createNewCell,
|
|
116
|
-
prepareForRun,
|
|
117
|
-
sendRun,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const { sendMessage, stop, status, addToolOutput } = useChat({
|
|
121
|
-
throttle: AI_SDK_UI_THROTTLE_MS,
|
|
122
|
-
transport: new StreamingChunkTransport(
|
|
123
|
-
{
|
|
116
|
+
const transport = useMemo(
|
|
117
|
+
() =>
|
|
118
|
+
new DefaultChatTransport<CompletionUIMessage>({
|
|
124
119
|
api: runtimeManager.getAiURL("completion").toString(),
|
|
125
120
|
headers: () => runtimeManager.headers(),
|
|
126
121
|
prepareSendMessagesRequest: async (options) => {
|
|
127
122
|
const completionBody = await buildCompletionRequestBody(
|
|
128
123
|
options.messages,
|
|
129
124
|
);
|
|
130
|
-
const body
|
|
131
|
-
...options,
|
|
125
|
+
const body = {
|
|
132
126
|
...completionBody,
|
|
133
127
|
code: "",
|
|
134
128
|
prompt: "", // Don't need prompt since we are using messages
|
|
135
129
|
language: language,
|
|
136
|
-
};
|
|
130
|
+
} satisfies AiCompletionRequest;
|
|
137
131
|
|
|
138
132
|
return {
|
|
139
133
|
api: runtimeManager.getAiURL("completion").toString(),
|
|
140
134
|
body: body,
|
|
141
135
|
};
|
|
142
136
|
},
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
}),
|
|
138
|
+
[language, runtimeManager],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const handleGenerationError = useEvent((error: unknown) => {
|
|
142
|
+
finishStagedCellGeneration(false);
|
|
143
|
+
toast({
|
|
144
|
+
title: "Generate with AI failed",
|
|
145
|
+
description: prettyError(error),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const {
|
|
150
|
+
sendMessage,
|
|
151
|
+
stop: stopChat,
|
|
152
|
+
status,
|
|
153
|
+
} = useChat<CompletionUIMessage>({
|
|
154
|
+
throttle: AI_SDK_UI_THROTTLE_MS,
|
|
155
|
+
transport,
|
|
156
|
+
onData,
|
|
157
|
+
onError: handleGenerationError,
|
|
158
|
+
onFinish: ({ isAbort, isDisconnect, isError, finishReason }) => {
|
|
159
|
+
finishStagedCellGeneration(
|
|
160
|
+
!isAbort && !isDisconnect && !isError && finishReason === "stop",
|
|
161
|
+
);
|
|
165
162
|
},
|
|
166
163
|
});
|
|
167
164
|
|
|
168
165
|
const isLoading = status === "streaming" || status === "submitted";
|
|
169
|
-
const hasCompletion =
|
|
166
|
+
const hasCompletion = hasOwnedStagedCells();
|
|
167
|
+
const stop = useEvent(() => {
|
|
168
|
+
submissionController.cancel();
|
|
169
|
+
finishStagedCellGeneration(false);
|
|
170
|
+
void stopChat();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const reject = useEvent(() => {
|
|
174
|
+
submissionController.cancel();
|
|
175
|
+
discardOwnedStagedCells();
|
|
176
|
+
void stopChat();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Parent-driven unmounts bypass the close handlers, so cancel in-flight work here.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
return () => {
|
|
182
|
+
stop();
|
|
183
|
+
};
|
|
184
|
+
}, [stop]);
|
|
170
185
|
|
|
171
186
|
const currentModel = aiConfig?.models?.edit_model || DEFAULT_AI_MODEL;
|
|
172
187
|
const currentProvider = AiModelId.parse(currentModel).providerId;
|
|
@@ -175,14 +190,19 @@ export const AddCellWithAI: React.FC<{
|
|
|
175
190
|
|
|
176
191
|
const submit = async () => {
|
|
177
192
|
if (!isLoading) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
await submissionController.run({
|
|
194
|
+
prepare: async () => {
|
|
195
|
+
if (inputRef.current?.view) {
|
|
196
|
+
storePrompt(inputRef.current.view);
|
|
197
|
+
}
|
|
198
|
+
return files ? await convertToFileUIPart(files) : undefined;
|
|
199
|
+
},
|
|
200
|
+
submit: async (fileParts) => {
|
|
201
|
+
beginStagedCellGeneration();
|
|
202
|
+
await sendMessage({ text: input, files: fileParts });
|
|
203
|
+
},
|
|
204
|
+
onError: handleGenerationError,
|
|
205
|
+
});
|
|
186
206
|
}
|
|
187
207
|
};
|
|
188
208
|
|
|
@@ -225,18 +245,19 @@ export const AddCellWithAI: React.FC<{
|
|
|
225
245
|
);
|
|
226
246
|
|
|
227
247
|
const handleAcceptCompletion = () => {
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
if (acceptOwnedStagedCells()) {
|
|
249
|
+
onClose();
|
|
250
|
+
}
|
|
230
251
|
};
|
|
231
252
|
|
|
232
253
|
const handleDeclineCompletion = () => {
|
|
233
|
-
|
|
254
|
+
reject();
|
|
234
255
|
// Focus the input so the user can refine the prompt.
|
|
235
256
|
inputRef.current?.view?.focus();
|
|
236
257
|
};
|
|
237
258
|
|
|
238
259
|
const handleClose = () => {
|
|
239
|
-
|
|
260
|
+
reject();
|
|
240
261
|
onClose();
|
|
241
262
|
};
|
|
242
263
|
|