@marimo-team/islands 0.25.3-dev266 → 0.25.3-dev267
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/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-C4yCN38h.js} +1 -1
- package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CewXPyL5.js} +64 -69
- package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-o6Ay6Q63.js} +2 -2
- package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BhiwwlVg.js} +1 -1
- package/dist/{chat-ui-DFxNgzXF.js → chat-ui-CbgR5ivy.js} +1964 -1740
- package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-WMSMf4Nj.js} +1 -1
- package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-BRfADrzD.js} +1 -1
- package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CuK5QFre.js} +1 -1
- package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-Bcun_jhJ.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-CgWDbUDT.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-DYmc9SFU.js} +2 -2
- package/dist/{code-visibility-SRM1JiD_.js → code-visibility-BKBl9Rh6.js} +9 -9
- package/dist/{common-yiRhZ6ZZ.js → common-N3Jd_6m9.js} +5944 -5626
- package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-PPKWipjT.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-niRBZh--.js} +1 -1
- package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Cp59CrqZ.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-DPiP9rot.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-B0WZuaxV.js} +1 -1
- package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-Rn42WBuN.js} +1 -1
- package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-BB93--ry.js} +1 -1
- package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-BpImEfn3.js} +1211 -1205
- package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-V4t5h6mZ.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-C1MsAKXd.js} +1 -1
- package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
- package/dist/{error-entries-CMoaUX9V.js → error-entries-CJGdD2ky.js} +49 -42
- package/dist/{feedback-button-DBUrE4OL.js → feedback-button-Qc1f9iPq.js} +70 -70
- package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-DHjeJP96.js} +2 -2
- package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-hCq3K0sd.js} +2042 -2041
- package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-DC3sk6EX.js} +28 -34
- package/dist/{globals-dIbBIR5-.js → globals-B2_uMYDY.js} +1 -1
- package/dist/{html-to-image-D_5CMreP.js → html-to-image-S7spRQCi.js} +5526 -5828
- package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-Dovv6iKx.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-D2nyygsc.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-KyVx0uTY.js} +1 -1
- package/dist/main.js +1288 -1287
- package/dist/{mermaid-DFYN_PXd.js → mermaid-B-jKSwGt.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-Bz9tKQFl.js} +1 -1
- package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-CfNYvbdz.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-C_qpq6Tv.js} +2 -2
- package/dist/pencil-DcT8AE0h.js +10 -0
- package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-Bp9ucgI4.js} +1 -1
- package/dist/{process-output-CGFNVC60.js → process-output-f34bkXTm.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-tq-VEH6j.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-DW-vHadb.js} +1 -1
- package/dist/{reveal-component-DH09ygHo.js → reveal-component-CUxv-kkP.js} +9 -9
- package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-DTmPQ0EB.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-CkCif0Iw.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-21kCq6l8.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BpRJP6KU.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-Ckgh4lS4.js} +1 -1
- package/dist/{toast-CY1_Qv41.js → toast-DpczUryP.js} +1 -1
- package/dist/{vega-component-C-lqjV0q.js → vega-component-CF9kI6C4.js} +1 -1
- package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-CDei2Bt3.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-CXavT601.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-CvJhcZri.js} +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
- package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
- package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
- package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
- package/src/components/chat/__tests__/user-message.test.tsx +27 -0
- package/src/components/chat/acp/agent-panel.tsx +2 -0
- package/src/components/chat/agent-activity.tsx +132 -0
- package/src/components/chat/chat-display.tsx +125 -96
- package/src/components/chat/chat-panel.tsx +123 -106
- package/src/components/chat/chat-utils.ts +12 -24
- package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
- package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
- package/src/components/chat/tool-call/shared.ts +6 -0
- package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
- package/src/components/chat/tool-call/tool-labels.ts +172 -0
- package/src/components/chat/tool-call/tool-step.tsx +145 -0
- package/src/components/chat/turn-timing.ts +73 -0
- package/src/components/chat/use-assistant-turn.ts +65 -0
- package/src/components/chat/use-chat-generation.ts +5 -4
- package/src/components/chat/user-message.tsx +145 -0
- package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +18 -1
- package/src/components/editor/actions/useCellActionButton.tsx +3 -0
- package/src/components/editor/ai/completion-handlers.tsx +0 -37
- package/src/components/editor/cell/StagedAICell.tsx +51 -144
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
- package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +6 -0
- package/src/components/editor/links/cell-link.tsx +4 -4
- package/src/components/editor/notebook-cell.tsx +3 -11
- package/src/components/ui/toast.tsx +1 -1
- package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
- package/src/core/ai/staged-cells.ts +48 -38
- package/src/core/cells/scrollCellIntoView.ts +6 -0
- package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
- package/src/core/codemirror/placeholder/extensions.ts +23 -0
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
- package/src/core/session/useNotebookSession.tsx +7 -2
- package/src/css/app/Cell.css +7 -36
- package/src/plugins/impl/chat/chat-ui.tsx +2 -3
- package/src/plugins/impl/common/error-banner.tsx +5 -3
- package/src/components/chat/reasoning-accordion.tsx +0 -61
- package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
- package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
- package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
- package/src/components/icons/loading-ellipsis.tsx +0 -46
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ChatAddToolApproveResponseFunction } from "ai";
|
|
4
4
|
import React from "react";
|
|
5
|
-
import { logNever } from "@/utils/assertNever";
|
|
6
5
|
import type { ToolApproval, ToolState } from "./shared";
|
|
7
6
|
import { ToolApprovalCard } from "./tool-approval-card";
|
|
8
|
-
import {
|
|
9
|
-
import { ToolHistoryRow } from "./tool-history-row";
|
|
7
|
+
import { ToolStep } from "./tool-step";
|
|
10
8
|
|
|
11
9
|
interface ToolCallViewProps {
|
|
12
10
|
toolName: string;
|
|
@@ -16,88 +14,36 @@ interface ToolCallViewProps {
|
|
|
16
14
|
input?: unknown;
|
|
17
15
|
approval?: ToolApproval;
|
|
18
16
|
onApprove?: ChatAddToolApproveResponseFunction;
|
|
19
|
-
index?: number;
|
|
20
17
|
className?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Whether this tool call belongs to the latest message in the conversation.
|
|
23
|
-
* Used by error cards to decide whether to stay expanded (live) or collapse
|
|
24
|
-
* (the user has moved on).
|
|
25
|
-
*/
|
|
26
|
-
isLive?: boolean;
|
|
27
18
|
isActive?: boolean;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
export const ToolCallView: React.FC<ToolCallViewProps> = ({
|
|
31
22
|
toolName,
|
|
32
23
|
state,
|
|
33
|
-
result,
|
|
34
|
-
errorText,
|
|
35
|
-
input,
|
|
36
24
|
approval,
|
|
37
25
|
onApprove,
|
|
38
|
-
|
|
39
|
-
className,
|
|
40
|
-
isLive = true,
|
|
41
|
-
isActive = true,
|
|
26
|
+
...props
|
|
42
27
|
}) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
className={className}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return (
|
|
60
|
-
<ToolHistoryRow
|
|
61
|
-
toolName={toolName}
|
|
62
|
-
state="input-available"
|
|
63
|
-
input={input}
|
|
64
|
-
index={index}
|
|
65
|
-
className={className}
|
|
66
|
-
isActive={isActive}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
case "output-error":
|
|
71
|
-
return (
|
|
72
|
-
<ToolErrorCard
|
|
73
|
-
toolName={toolName}
|
|
74
|
-
input={input}
|
|
75
|
-
errorText={errorText}
|
|
76
|
-
isLive={isLive}
|
|
77
|
-
className={className}
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
case "input-streaming":
|
|
82
|
-
case "input-available":
|
|
83
|
-
case "approval-responded":
|
|
84
|
-
case "output-available":
|
|
85
|
-
case "output-denied":
|
|
86
|
-
return (
|
|
87
|
-
<ToolHistoryRow
|
|
88
|
-
toolName={toolName}
|
|
89
|
-
state={state}
|
|
90
|
-
input={input}
|
|
91
|
-
result={result}
|
|
92
|
-
approval={approval}
|
|
93
|
-
index={index}
|
|
94
|
-
className={className}
|
|
95
|
-
isActive={isActive}
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
default:
|
|
100
|
-
logNever(state);
|
|
101
|
-
return null;
|
|
28
|
+
if (state === "approval-requested") {
|
|
29
|
+
return approval != null && onApprove != null ? (
|
|
30
|
+
<ToolApprovalCard
|
|
31
|
+
toolName={toolName}
|
|
32
|
+
input={props.input}
|
|
33
|
+
approval={approval}
|
|
34
|
+
onApprove={onApprove}
|
|
35
|
+
className={props.className}
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<ToolStep {...props} toolName={toolName} state="input-available" />
|
|
39
|
+
);
|
|
102
40
|
}
|
|
41
|
+
return (
|
|
42
|
+
<ToolStep
|
|
43
|
+
{...props}
|
|
44
|
+
toolName={toolName}
|
|
45
|
+
state={state}
|
|
46
|
+
approval={approval}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
103
49
|
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BookOpenIcon,
|
|
5
|
+
BugIcon,
|
|
6
|
+
DatabaseIcon,
|
|
7
|
+
FileSearchIcon,
|
|
8
|
+
ListChecksIcon,
|
|
9
|
+
type LucideIcon,
|
|
10
|
+
NetworkIcon,
|
|
11
|
+
NotebookIcon,
|
|
12
|
+
PencilIcon,
|
|
13
|
+
PlusIcon,
|
|
14
|
+
ScanSearchIcon,
|
|
15
|
+
SquareTerminalIcon,
|
|
16
|
+
Trash2Icon,
|
|
17
|
+
VariableIcon,
|
|
18
|
+
WrenchIcon,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
import { formatToolName } from "./shared";
|
|
21
|
+
|
|
22
|
+
interface ToolDescription {
|
|
23
|
+
label: string;
|
|
24
|
+
detail?: string;
|
|
25
|
+
icon: LucideIcon;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Phase = "running" | "done";
|
|
29
|
+
|
|
30
|
+
const KNOWN_TOOLS: Record<
|
|
31
|
+
string,
|
|
32
|
+
{ running: string; done: string; icon: LucideIcon }
|
|
33
|
+
> = {
|
|
34
|
+
get_marimo_rules: {
|
|
35
|
+
running: "Reading marimo guidelines",
|
|
36
|
+
done: "Read marimo guidelines",
|
|
37
|
+
icon: BookOpenIcon,
|
|
38
|
+
},
|
|
39
|
+
get_active_notebooks: {
|
|
40
|
+
running: "Listing notebooks",
|
|
41
|
+
done: "Listed notebooks",
|
|
42
|
+
icon: NotebookIcon,
|
|
43
|
+
},
|
|
44
|
+
get_lightweight_cell_map: {
|
|
45
|
+
running: "Reading notebook",
|
|
46
|
+
done: "Read notebook",
|
|
47
|
+
icon: FileSearchIcon,
|
|
48
|
+
},
|
|
49
|
+
get_cell_runtime_data: {
|
|
50
|
+
running: "Inspecting cells",
|
|
51
|
+
done: "Inspected cells",
|
|
52
|
+
icon: ScanSearchIcon,
|
|
53
|
+
},
|
|
54
|
+
get_cell_outputs: {
|
|
55
|
+
running: "Reading cell outputs",
|
|
56
|
+
done: "Read cell outputs",
|
|
57
|
+
icon: ScanSearchIcon,
|
|
58
|
+
},
|
|
59
|
+
get_tables_and_variables: {
|
|
60
|
+
running: "Reading variables",
|
|
61
|
+
done: "Read variables",
|
|
62
|
+
icon: VariableIcon,
|
|
63
|
+
},
|
|
64
|
+
get_database_tables: {
|
|
65
|
+
running: "Reading database tables",
|
|
66
|
+
done: "Read database tables",
|
|
67
|
+
icon: DatabaseIcon,
|
|
68
|
+
},
|
|
69
|
+
get_notebook_errors: {
|
|
70
|
+
running: "Checking for errors",
|
|
71
|
+
done: "Checked for errors",
|
|
72
|
+
icon: BugIcon,
|
|
73
|
+
},
|
|
74
|
+
lint_notebook: {
|
|
75
|
+
running: "Linting notebook",
|
|
76
|
+
done: "Linted notebook",
|
|
77
|
+
icon: ListChecksIcon,
|
|
78
|
+
},
|
|
79
|
+
get_cell_dependency_graph: {
|
|
80
|
+
running: "Reading dependency graph",
|
|
81
|
+
done: "Read dependency graph",
|
|
82
|
+
icon: NetworkIcon,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const EDIT_LABELS = {
|
|
87
|
+
add_cell: { running: "Adding cell", done: "Added cell", icon: PlusIcon },
|
|
88
|
+
update_cell: {
|
|
89
|
+
running: "Editing cell",
|
|
90
|
+
done: "Edited cell",
|
|
91
|
+
icon: PencilIcon,
|
|
92
|
+
},
|
|
93
|
+
delete_cell: {
|
|
94
|
+
running: "Deleting cell",
|
|
95
|
+
done: "Deleted cell",
|
|
96
|
+
icon: Trash2Icon,
|
|
97
|
+
},
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
export function describeToolCall(
|
|
101
|
+
toolName: string,
|
|
102
|
+
input: unknown,
|
|
103
|
+
phase: Phase,
|
|
104
|
+
): ToolDescription {
|
|
105
|
+
const name = formatToolName(toolName);
|
|
106
|
+
|
|
107
|
+
if (name === "execute_code") {
|
|
108
|
+
return {
|
|
109
|
+
label: phase === "running" ? "Running code" : "Ran code",
|
|
110
|
+
detail: isRecord(input) ? summarizeCode(input.code) : undefined,
|
|
111
|
+
icon: SquareTerminalIcon,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === "edit_notebook_tool") {
|
|
116
|
+
const edit = getEdit(input);
|
|
117
|
+
if (edit) {
|
|
118
|
+
const labels = EDIT_LABELS[edit.type];
|
|
119
|
+
return {
|
|
120
|
+
label: labels[phase],
|
|
121
|
+
detail: summarizeCode(edit.code),
|
|
122
|
+
icon: labels.icon,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
label: phase === "running" ? "Editing notebook" : "Edited notebook",
|
|
127
|
+
icon: PencilIcon,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (Object.hasOwn(KNOWN_TOOLS, name)) {
|
|
132
|
+
const known = KNOWN_TOOLS[name];
|
|
133
|
+
return { label: known[phase], icon: known.icon };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
label: phase === "running" ? "Calling" : "Called",
|
|
138
|
+
detail: name,
|
|
139
|
+
icon: WrenchIcon,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getEdit(
|
|
144
|
+
input: unknown,
|
|
145
|
+
): { type: keyof typeof EDIT_LABELS; code?: string } | undefined {
|
|
146
|
+
if (!isRecord(input) || !isRecord(input.edit)) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const { type, code } = input.edit;
|
|
150
|
+
if (typeof type !== "string" || !Object.hasOwn(EDIT_LABELS, type)) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
type: type as keyof typeof EDIT_LABELS,
|
|
155
|
+
code: typeof code === "string" ? code : undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function summarizeCode(code: unknown): string | undefined {
|
|
160
|
+
if (typeof code !== "string") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const lines = code
|
|
164
|
+
.split("\n")
|
|
165
|
+
.map((line) => line.trim())
|
|
166
|
+
.filter((line) => line.length > 0);
|
|
167
|
+
return lines.find((line) => !/^(import\s|from\s|#)/.test(line)) ?? lines[0];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
171
|
+
return typeof value === "object" && value !== null;
|
|
172
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BanIcon,
|
|
5
|
+
ChevronRightIcon,
|
|
6
|
+
CircleSlashIcon,
|
|
7
|
+
Loader2Icon,
|
|
8
|
+
type LucideIcon,
|
|
9
|
+
XCircleIcon,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { cn } from "@/utils/cn";
|
|
13
|
+
import {
|
|
14
|
+
PENDING_TOOL_STATES,
|
|
15
|
+
type ToolApproval,
|
|
16
|
+
type ToolState,
|
|
17
|
+
} from "./shared";
|
|
18
|
+
import { ToolArgsRenderer } from "./tool-args";
|
|
19
|
+
import { describeToolCall } from "./tool-labels";
|
|
20
|
+
import { ResultRenderer } from "./tool-result";
|
|
21
|
+
|
|
22
|
+
type ToolStepState = Exclude<ToolState, "approval-requested">;
|
|
23
|
+
|
|
24
|
+
interface ToolStepProps {
|
|
25
|
+
toolName: string;
|
|
26
|
+
state: ToolStepState;
|
|
27
|
+
input?: unknown;
|
|
28
|
+
result?: unknown;
|
|
29
|
+
errorText?: string;
|
|
30
|
+
approval?: ToolApproval;
|
|
31
|
+
isActive?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ToolStep: React.FC<ToolStepProps> = ({
|
|
36
|
+
toolName,
|
|
37
|
+
state,
|
|
38
|
+
input,
|
|
39
|
+
result,
|
|
40
|
+
errorText,
|
|
41
|
+
approval,
|
|
42
|
+
isActive = true,
|
|
43
|
+
className,
|
|
44
|
+
}) => {
|
|
45
|
+
const [open, setOpen] = React.useState(false);
|
|
46
|
+
const pending = PENDING_TOOL_STATES.has(state);
|
|
47
|
+
const { label, detail, icon } = describeToolCall(
|
|
48
|
+
toolName,
|
|
49
|
+
input,
|
|
50
|
+
pending ? "running" : "done",
|
|
51
|
+
);
|
|
52
|
+
const status = getStatus(state, pending && !isActive, icon);
|
|
53
|
+
const failed = state === "output-error";
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("w-full", className)}>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
60
|
+
aria-expanded={open}
|
|
61
|
+
className={cn(
|
|
62
|
+
"group/step -mx-1.5 flex w-fit max-w-full min-w-0 items-center gap-1.5 rounded-md px-1.5 py-0.5 text-left text-xs leading-4 text-muted-foreground transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-1 focus-visible:outline-(--slate-8)",
|
|
63
|
+
open && "text-foreground",
|
|
64
|
+
failed && "text-(--red-11) hover:bg-(--red-3) hover:text-(--red-11)",
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<status.icon
|
|
68
|
+
aria-hidden={true}
|
|
69
|
+
className={cn(
|
|
70
|
+
// Offsets lucide's built-in padding.
|
|
71
|
+
"-ml-px size-3 shrink-0",
|
|
72
|
+
status.spinning && "animate-spin",
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
<span className="shrink-0">{label}</span>
|
|
76
|
+
{detail && (
|
|
77
|
+
<code className="min-w-0 truncate font-mono text-[0.6875rem] opacity-80">
|
|
78
|
+
{detail}
|
|
79
|
+
</code>
|
|
80
|
+
)}
|
|
81
|
+
{status.suffix && (
|
|
82
|
+
<span className="shrink-0 opacity-80">· {status.suffix}</span>
|
|
83
|
+
)}
|
|
84
|
+
<ChevronRightIcon
|
|
85
|
+
aria-hidden={true}
|
|
86
|
+
className={cn(
|
|
87
|
+
"size-3 shrink-0 opacity-40 transition-transform group-hover/step:opacity-100",
|
|
88
|
+
open && "rotate-90 opacity-100",
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
</button>
|
|
92
|
+
{open && (
|
|
93
|
+
<ToolStepDetails
|
|
94
|
+
state={state}
|
|
95
|
+
input={input}
|
|
96
|
+
result={result}
|
|
97
|
+
errorText={errorText}
|
|
98
|
+
approval={approval}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function getStatus(
|
|
106
|
+
state: ToolStepState,
|
|
107
|
+
interrupted: boolean,
|
|
108
|
+
doneIcon: LucideIcon,
|
|
109
|
+
): { icon: LucideIcon; suffix?: string; spinning?: boolean } {
|
|
110
|
+
if (interrupted) {
|
|
111
|
+
return { icon: CircleSlashIcon, suffix: "Stopped" };
|
|
112
|
+
}
|
|
113
|
+
switch (state) {
|
|
114
|
+
case "output-error":
|
|
115
|
+
return { icon: XCircleIcon, suffix: "Failed" };
|
|
116
|
+
case "output-denied":
|
|
117
|
+
return { icon: BanIcon, suffix: "Denied" };
|
|
118
|
+
case "output-available":
|
|
119
|
+
return { icon: doneIcon };
|
|
120
|
+
default:
|
|
121
|
+
return { icon: Loader2Icon, spinning: true };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ToolStepDetails: React.FC<
|
|
126
|
+
Pick<ToolStepProps, "state" | "input" | "result" | "errorText" | "approval">
|
|
127
|
+
> = ({ state, input, result, errorText, approval }) => (
|
|
128
|
+
<div className="mt-1 mb-2 ml-5.5 space-y-3">
|
|
129
|
+
<ToolArgsRenderer input={input} />
|
|
130
|
+
{state === "output-available" && result != null && (
|
|
131
|
+
<ResultRenderer result={result} />
|
|
132
|
+
)}
|
|
133
|
+
{state === "output-error" && errorText && (
|
|
134
|
+
<pre className="max-h-64 overflow-auto rounded border border-(--red-6) bg-(--red-2) p-2 text-xs leading-relaxed whitespace-pre-wrap text-(--red-11) scrollbar-thin">
|
|
135
|
+
{errorText}
|
|
136
|
+
</pre>
|
|
137
|
+
)}
|
|
138
|
+
{state === "output-denied" && (
|
|
139
|
+
<p className="text-xs text-muted-foreground">
|
|
140
|
+
Tool execution was denied
|
|
141
|
+
{approval?.reason ? `: ${approval.reason}` : "."}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { UIMessage } from "ai";
|
|
4
|
+
|
|
5
|
+
export interface TurnTiming {
|
|
6
|
+
startedAt: number;
|
|
7
|
+
finishedAt?: number;
|
|
8
|
+
stopped?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTurnTiming(message: UIMessage): TurnTiming | undefined {
|
|
12
|
+
const metadata = message.metadata;
|
|
13
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const { startedAt, finishedAt, stopped } = metadata as Record<
|
|
17
|
+
string,
|
|
18
|
+
unknown
|
|
19
|
+
>;
|
|
20
|
+
if (typeof startedAt !== "number") {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
startedAt,
|
|
25
|
+
finishedAt: typeof finishedAt === "number" ? finishedAt : undefined,
|
|
26
|
+
stopped: stopped === true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function withTurnTiming(
|
|
31
|
+
messages: UIMessage[],
|
|
32
|
+
turn: { startedAt: number | null; stopped: boolean },
|
|
33
|
+
now = Date.now(),
|
|
34
|
+
): UIMessage[] {
|
|
35
|
+
const last = messages.at(-1);
|
|
36
|
+
if (last?.role !== "assistant") {
|
|
37
|
+
return messages;
|
|
38
|
+
}
|
|
39
|
+
const startedAt = turn.startedAt ?? getTurnTiming(last)?.startedAt;
|
|
40
|
+
if (startedAt == null) {
|
|
41
|
+
return messages;
|
|
42
|
+
}
|
|
43
|
+
const metadata =
|
|
44
|
+
typeof last.metadata === "object" && last.metadata !== null
|
|
45
|
+
? last.metadata
|
|
46
|
+
: {};
|
|
47
|
+
return [
|
|
48
|
+
...messages.slice(0, -1),
|
|
49
|
+
{
|
|
50
|
+
...last,
|
|
51
|
+
metadata: {
|
|
52
|
+
...metadata,
|
|
53
|
+
startedAt,
|
|
54
|
+
finishedAt: now,
|
|
55
|
+
stopped: turn.stopped,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatDuration(ms: number): string {
|
|
62
|
+
const totalSeconds = Math.max(1, Math.round(ms / 1000));
|
|
63
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
64
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
65
|
+
const seconds = totalSeconds % 60;
|
|
66
|
+
if (hours > 0) {
|
|
67
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
68
|
+
}
|
|
69
|
+
if (minutes > 0) {
|
|
70
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
71
|
+
}
|
|
72
|
+
return `${seconds}s`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { UIMessage } from "ai";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import useEvent from "react-use-event-hook";
|
|
6
|
+
import { willAutoContinue } from "./chat-utils";
|
|
7
|
+
import { getTurnTiming, withTurnTiming } from "./turn-timing";
|
|
8
|
+
|
|
9
|
+
export function useAssistantTurn() {
|
|
10
|
+
const [turn, setTurn] = useState<{
|
|
11
|
+
startedAt: number;
|
|
12
|
+
ended: boolean;
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
const start = useEvent(() =>
|
|
16
|
+
setTurn({ startedAt: Date.now(), ended: false }),
|
|
17
|
+
);
|
|
18
|
+
const reset = useEvent(() => setTurn(null));
|
|
19
|
+
const stop = useEvent(() =>
|
|
20
|
+
setTurn((prev) => prev && { ...prev, ended: true }),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Shift the start forward so time spent waiting on the user isn't counted.
|
|
24
|
+
const resume = useEvent((reply: UIMessage | undefined) => {
|
|
25
|
+
const timing = reply && getTurnTiming(reply);
|
|
26
|
+
const workedMs =
|
|
27
|
+
timing?.finishedAt == null ? 0 : timing.finishedAt - timing.startedAt;
|
|
28
|
+
setTurn({ startedAt: Date.now() - workedMs, ended: false });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const finish = useEvent(
|
|
32
|
+
(finished: UIMessage[], opts: { isAbort: boolean; isError: boolean }) => {
|
|
33
|
+
if (opts.isAbort || opts.isError) {
|
|
34
|
+
stop();
|
|
35
|
+
}
|
|
36
|
+
return withTurnTiming(finished, {
|
|
37
|
+
startedAt: turn?.startedAt ?? null,
|
|
38
|
+
stopped: opts.isAbort,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Between requests of one turn the SDK briefly reports "ready".
|
|
44
|
+
const isActive = (state: {
|
|
45
|
+
isLoading: boolean;
|
|
46
|
+
hasError: boolean;
|
|
47
|
+
messages: UIMessage[];
|
|
48
|
+
}) =>
|
|
49
|
+
state.isLoading ||
|
|
50
|
+
(turn != null &&
|
|
51
|
+
!turn.ended &&
|
|
52
|
+
!state.hasError &&
|
|
53
|
+
willAutoContinue(state.messages));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
startedAt: turn?.startedAt ?? null,
|
|
57
|
+
hasEnded: turn?.ended === true,
|
|
58
|
+
start,
|
|
59
|
+
resume,
|
|
60
|
+
stop,
|
|
61
|
+
reset,
|
|
62
|
+
finish,
|
|
63
|
+
isActive,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -5,15 +5,16 @@ import { useSetAtom } from "jotai";
|
|
|
5
5
|
import { useEffect } from "react";
|
|
6
6
|
import { chatGenerationInProgressAtom } from "@/core/ai/state";
|
|
7
7
|
|
|
8
|
-
export function
|
|
8
|
+
export function isChatBusy(status: ChatStatus): boolean {
|
|
9
|
+
return status === "submitted" || status === "streaming";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useChatGeneration(isGenerating: boolean) {
|
|
9
13
|
const setGenerating = useSetAtom(chatGenerationInProgressAtom);
|
|
10
|
-
const isGenerating = status === "submitted" || status === "streaming";
|
|
11
14
|
|
|
12
15
|
useEffect(() => {
|
|
13
16
|
setGenerating(isGenerating);
|
|
14
17
|
// useChat stops its request when the panel unmounts or becomes hidden.
|
|
15
18
|
return () => setGenerating(false);
|
|
16
19
|
}, [isGenerating, setGenerating]);
|
|
17
|
-
|
|
18
|
-
return isGenerating;
|
|
19
20
|
}
|