@marimo-team/islands 0.25.3-dev3 → 0.25.3-dev5
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-CNSxdKyJ.js → ConnectedDataExplorerComponent-CNMliVzw.js} +1 -1
- package/dist/{ErrorBoundary-iHE9DnZC.js → ErrorBoundary-BiZWOrv4.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-DQDjNXt3.js → abnfDiagram-VCTEODGH-D-Ez91gV.js} +2 -2
- package/dist/{any-language-editor-B9REGlD9.js → any-language-editor-DZ_YTVus.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-Nmls7Wxn.js → architectureDiagram-5GKGNRK7-CTpcNtO8.js} +1 -1
- package/dist/{chat-ui-BDpymeoe.js → chat-ui-0lKdOoQ3.js} +436 -437
- package/dist/{chunk-IMKFNOWR-CoIFEo-1.js → chunk-IMKFNOWR-DGxi-zeo.js} +1 -1
- package/dist/{chunk-SHT3W25Y-lHiGx3ZC.js → chunk-SHT3W25Y-CV9J3rAB.js} +1 -1
- package/dist/{chunk-SVP7TREG-BvsHIrY_.js → chunk-SVP7TREG-BL-9Fqgg.js} +1 -1
- package/dist/{chunk-TICWLB2K-BC8YIm_n.js → chunk-TICWLB2K-Cu_XMjDM.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Bt98lkkv.js → classDiagram-ZZMXUADV-BXXV13Hq.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D9bfbcC3.js → classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js} +2 -2
- package/dist/{code-visibility-BCpXa0Xh.js → code-visibility-CW5_8ZOI.js} +14 -14
- package/dist/{common-BrBUdtPy.js → common-mzWw4FdI.js} +5466 -5236
- package/dist/{cynefinDiagram-5FMLGOSQ-CgWra7wt.js → cynefinDiagram-5FMLGOSQ-CH96rwba.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CFZ8-35c.js → dagre-GXQ25YYZ-BD3PmpFY.js} +1 -1
- package/dist/{data-grid-overlay-editor-Ch2SgmvG.js → data-grid-overlay-editor-CJmehSdc.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-lCNQrW7d.js → diagram-S7CK7UJ4-BsSJJldB.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-CQPQ6pP_.js → diagram-UQ7AKVKN-muQtNZc4.js} +1 -1
- package/dist/{diagram-VX7I27RA-Cf3b0llz.js → diagram-VX7I27RA-CdhuZAqR.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bt4QLrpn.js → diagram-Z3DM3KII-BD8mXjOj.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-D3NAyPFY.js → ebnfDiagram-PWID7BFC-BIN1sweE.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Tlu09_Nm.js → erDiagram-RLTQ6QDP-Ctp5cfoN.js} +1 -1
- package/dist/{error-banner-Da600cwz.js → error-banner-D-WCIiMt.js} +10 -5
- package/dist/{cells-C8iCYvLU.js → error-entries-DSvFWT_M.js} +2262 -2206
- package/dist/{feedback-button-DxXNcC6q.js → feedback-button-BcT1FBud.js} +115 -116
- package/dist/{flowDiagram-HODETNUW-CAYbpJmV.js → flowDiagram-HODETNUW-3EfLrlFb.js} +2 -2
- package/dist/{formats-BGh2thRc.js → formats-CeOmU1MU.js} +1 -1
- package/dist/{glide-data-editor-BQ7uKbeo.js → glide-data-editor-XckatD0j.js} +3 -3
- package/dist/{globals-DSavh2ZB.js → globals-jRrLCkrP.js} +1 -1
- package/dist/{html-to-image-CZY1pIZH.js → html-to-image-B8s5SgGm.js} +23 -23
- package/dist/{infoDiagram-27XIBGKW-XrDxhr0x.js → infoDiagram-27XIBGKW-DipQoHxl.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CD1muzov.js → ishikawaDiagram-5VMMS53U-DgFMjSGz.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-Dg9W142_.js → kanban-definition-UXKFOSKX-w1ACVjgl.js} +1 -1
- package/dist/main.js +164 -163
- package/dist/{mermaid-CiAXjLpA.js → mermaid-CHo49Yv-.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-DgCidgYJ.js → mindmap-definition-YA3MSWOX-CrT0KSfX.js} +1 -1
- package/dist/{number-overlay-editor-D6kYaqh5.js → number-overlay-editor-pZx8dUDV.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-coOYYKB6.js → pegDiagram-XKGWAZYB-BqvywNJA.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BnXTc3bj.js → pieDiagram-E7YTZNPT-BfxJ_XCc.js} +1 -1
- package/dist/{process-output-e4jj9Ijt.js → process-output-CKRXyszb.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-B28-u83n.js → railroadDiagram-O6MQD6OU-8vFLVTPu.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CdYCTMu0.js → requirementDiagram-BXWQKSXE-a_MxGvxe.js} +1 -1
- package/dist/{reveal-component-BTBfJ3ju.js → reveal-component-79ez8-D5.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CM3aRAe_.js → sequenceDiagram-WJ2MYXX4-CXFMnxGh.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-DOZUMVj8.js → stateDiagram-D77RDMKH-DQeA_CwI.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CISYxma6.js → stateDiagram-v2-MP3YSRHH-BxxQucm1.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-BkSppxp8.js → swimlanes-42K2YHIH-Do6L-cYg.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-c8G5pGxC.js → swimlanesDiagram-VR7AAH4N-DUzPvSVU.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-CaMQayqK.js → timeline-definition-24CTP7MA-D3c46WM5.js} +1 -1
- package/dist/{toDate-BNJf01u1.js → toDate-vIXljnsr.js} +39 -38
- package/dist/{vega-component-DG29TAB7.js → vega-component-DTG6zxaY.js} +3 -3
- package/dist/{vennDiagram-4TSXK5OY-CF8pfaK3.js → vennDiagram-4TSXK5OY-BSZD8JcZ.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-dGPu39d-.js → wardleyDiagram-VM6X3IG4-48cUWkfA.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CP6-9q56.js → xychartDiagram-S5SC5T6Z-CswYgYeu.js} +1 -1
- package/package.json +1 -1
- package/src/components/data-table/__tests__/context-menu.test.ts +176 -0
- package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +24 -1
- package/src/components/data-table/context-menu.tsx +125 -26
- package/src/components/data-table/date-filter-inputs.tsx +15 -42
- package/src/components/data-table/utils.ts +18 -0
- package/src/components/editor/Output.tsx +5 -2
- package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +167 -0
- package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +82 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +434 -0
- package/src/components/editor/errors/send-error-report-button.tsx +66 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
- package/src/components/editor/output/MarimoTracebackOutput.tsx +9 -0
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/core/attachments/state.ts +21 -0
- package/src/core/cells/__tests__/cells.test.ts +52 -0
- package/src/core/cells/__tests__/error-report.test.ts +89 -0
- package/src/core/cells/__tests__/session.test.ts +34 -0
- package/src/core/cells/error-report.ts +55 -0
- package/src/core/cells/session.ts +9 -0
- package/src/core/islands/__tests__/bootstrap.test.ts +26 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +193 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +9 -0
- package/src/utils/__tests__/dates.test.ts +116 -0
- package/src/utils/dates.ts +94 -0
- package/dist/error-entries-CzoYkIQZ.js +0 -56
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
dateToLocalISODate,
|
|
15
15
|
dateToLocalISODateTime,
|
|
16
16
|
dateToLocalISOTime,
|
|
17
|
+
parseLocalISODate,
|
|
18
|
+
parseLocalISODateTime,
|
|
19
|
+
parseTimeToDate,
|
|
17
20
|
} from "@/utils/dates";
|
|
18
21
|
import type { FilterType } from "./filters";
|
|
19
22
|
|
|
@@ -72,53 +75,23 @@ export function parsePastedDate(
|
|
|
72
75
|
return undefined;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (timeMatch) {
|
|
80
|
-
const [, hStr, mStr, sStr, ampm] = timeMatch;
|
|
81
|
-
let hour = Number.parseInt(hStr, 10);
|
|
82
|
-
const minute = Number.parseInt(mStr, 10);
|
|
83
|
-
const second = sStr ? Number.parseInt(sStr, 10) : 0;
|
|
84
|
-
if (ampm) {
|
|
85
|
-
const isPm = ampm.toLowerCase() === "pm";
|
|
86
|
-
if (hour === 12) {
|
|
87
|
-
hour = isPm ? 12 : 0;
|
|
88
|
-
} else if (isPm) {
|
|
89
|
-
hour += 12;
|
|
90
|
-
}
|
|
78
|
+
if (filterType === "time") {
|
|
79
|
+
const date = parseTimeToDate(trimmed);
|
|
80
|
+
if (date) {
|
|
81
|
+
return date;
|
|
91
82
|
}
|
|
92
|
-
return new Date(1970, 0, 1, hour, minute, second);
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Number.parseInt(m, 10) - 1,
|
|
101
|
-
Number.parseInt(d, 10),
|
|
102
|
-
);
|
|
85
|
+
if (filterType === "date") {
|
|
86
|
+
const date = parseLocalISODate(trimmed);
|
|
87
|
+
if (date) {
|
|
88
|
+
return date;
|
|
89
|
+
}
|
|
103
90
|
}
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// viewer's timezone.
|
|
109
|
-
const isoMatch = trimmed.match(
|
|
110
|
-
/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/,
|
|
111
|
-
);
|
|
112
|
-
if (isoMatch) {
|
|
113
|
-
const [, y, mo, d, h, mi, s] = isoMatch;
|
|
114
|
-
return new Date(
|
|
115
|
-
Number.parseInt(y, 10),
|
|
116
|
-
Number.parseInt(mo, 10) - 1,
|
|
117
|
-
Number.parseInt(d, 10),
|
|
118
|
-
Number.parseInt(h, 10),
|
|
119
|
-
Number.parseInt(mi, 10),
|
|
120
|
-
s ? Number.parseInt(s, 10) : 0,
|
|
121
|
-
);
|
|
92
|
+
const isoDateTime = parseLocalISODateTime(trimmed);
|
|
93
|
+
if (isoDateTime) {
|
|
94
|
+
return isoDateTime;
|
|
122
95
|
}
|
|
123
96
|
|
|
124
97
|
const parsed = new Date(trimmed);
|
|
@@ -228,6 +228,24 @@ export function detectSentinel(
|
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
type NullishSentinelType = Extract<CellValueSentinel["type"], "null" | "nat">;
|
|
232
|
+
|
|
233
|
+
const NULLISH_SENTINEL_TYPES: ReadonlySet<CellValueSentinel["type"]> =
|
|
234
|
+
new Set<NullishSentinelType>(["null", "nat"]);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* True when the cell value represents a missing value (null, NaT)
|
|
238
|
+
* rather than a real value that happens to look special.
|
|
239
|
+
*/
|
|
240
|
+
export function isNullishSentinel(
|
|
241
|
+
value: unknown,
|
|
242
|
+
dataType: DataType | undefined,
|
|
243
|
+
dtype?: string,
|
|
244
|
+
): boolean {
|
|
245
|
+
const sentinel = detectSentinel(value, dataType, dtype);
|
|
246
|
+
return sentinel !== null && NULLISH_SENTINEL_TYPES.has(sentinel.type);
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
/**
|
|
232
250
|
* Stringify an unknown value. Converts objects to JSON strings.
|
|
233
251
|
* @param opts.value - The value to stringify.
|
|
@@ -66,7 +66,7 @@ export type OnRefactorWithAI = (opts: {
|
|
|
66
66
|
* Renders an output based on an OutputMessage.
|
|
67
67
|
*/
|
|
68
68
|
export const OutputRenderer: React.FC<{
|
|
69
|
-
message: Pick<OutputMessage, "channel" | "data" | "mimetype">;
|
|
69
|
+
message: Pick<OutputMessage, "channel" | "data" | "mimetype" | "code">;
|
|
70
70
|
cellId?: CellId;
|
|
71
71
|
onRefactorWithAI?: OnRefactorWithAI;
|
|
72
72
|
wrapText?: boolean;
|
|
@@ -172,7 +172,9 @@ export const OutputRenderer: React.FC<{
|
|
|
172
172
|
|
|
173
173
|
case "application/vnd.marimo+error":
|
|
174
174
|
invariant(Array.isArray(data), "Expected array data");
|
|
175
|
-
return
|
|
175
|
+
return (
|
|
176
|
+
<MarimoErrorOutput cellId={cellId} errors={data} code={message.code} />
|
|
177
|
+
);
|
|
176
178
|
|
|
177
179
|
case "application/vnd.marimo+traceback":
|
|
178
180
|
invariant(
|
|
@@ -183,6 +185,7 @@ export const OutputRenderer: React.FC<{
|
|
|
183
185
|
<MarimoTracebackOutput
|
|
184
186
|
onRefactorWithAI={onRefactorWithAI}
|
|
185
187
|
traceback={data}
|
|
188
|
+
code={message.code}
|
|
186
189
|
cellId={cellId}
|
|
187
190
|
/>
|
|
188
191
|
);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
5
|
+
import { createStore, Provider } from "jotai";
|
|
6
|
+
import { userEvent } from "storybook/test";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { PAIR_PREVIEW } from "@/__tests__/fixtures/pair-preview";
|
|
9
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
10
|
+
import { type Attachment, attachmentsAtom } from "@/core/attachments/state";
|
|
11
|
+
import { pairPreviewAtom } from "@/core/config/pair";
|
|
12
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
13
|
+
import { WebSocketClosedReason, ConnectionState } from "@/core/websocket/types";
|
|
14
|
+
import { AttachmentStatus } from "../footer-items/attachment-status";
|
|
15
|
+
|
|
16
|
+
const AGENT: Attachment = {
|
|
17
|
+
id: "a1",
|
|
18
|
+
kind: "agent",
|
|
19
|
+
name: "Pi",
|
|
20
|
+
notebook_id: "nb-test",
|
|
21
|
+
identity: "pi-thread",
|
|
22
|
+
since: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function renderStatus(attachments: Attachment[] = [AGENT], preview = true) {
|
|
26
|
+
const store = createStore();
|
|
27
|
+
store.set(connectionAtom, { state: ConnectionState.OPEN });
|
|
28
|
+
store.set(pairPreviewAtom, preview ? PAIR_PREVIEW : undefined);
|
|
29
|
+
store.set(attachmentsAtom, attachments);
|
|
30
|
+
return {
|
|
31
|
+
store,
|
|
32
|
+
...render(
|
|
33
|
+
<Provider store={store}>
|
|
34
|
+
<TooltipProvider>
|
|
35
|
+
<AttachmentStatus />
|
|
36
|
+
</TooltipProvider>
|
|
37
|
+
</Provider>,
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("AttachmentStatus", () => {
|
|
43
|
+
it("renders nothing outside the preview", () => {
|
|
44
|
+
expect(renderStatus([AGENT], false).container).toBeEmptyDOMElement();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders nothing without agents", () => {
|
|
48
|
+
expect(
|
|
49
|
+
renderStatus([{ ...AGENT, kind: "client" }]).container,
|
|
50
|
+
).toBeEmptyDOMElement();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("lists each agent by its self-described name", () => {
|
|
54
|
+
renderStatus([
|
|
55
|
+
AGENT,
|
|
56
|
+
{ ...AGENT, id: "a2", identity: "claude-thread", name: "Claude Code" },
|
|
57
|
+
{ ...AGENT, id: "c1", kind: "client", name: "Browser" },
|
|
58
|
+
]);
|
|
59
|
+
expect(screen.getAllByTestId("footer-attachment-status")).toHaveLength(2);
|
|
60
|
+
expect(screen.getByLabelText("Pi: Connected")).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByLabelText("Claude Code: Connected")).toBeInTheDocument();
|
|
62
|
+
expect(screen.queryByText("Browser")).not.toBeInTheDocument();
|
|
63
|
+
fireEvent.click(screen.getByLabelText("Pi: Connected"));
|
|
64
|
+
expect(screen.getByText(/^Connected /)).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("uses Agent when no name was supplied", () => {
|
|
68
|
+
renderStatus([{ ...AGENT, name: null }]);
|
|
69
|
+
expect(screen.getByLabelText("Agent: Connected")).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("groups attachments by identity while retaining each connection row", () => {
|
|
73
|
+
renderStatus([
|
|
74
|
+
AGENT,
|
|
75
|
+
{ ...AGENT, id: "a2", since: "2026-10-14T00:00:00Z" },
|
|
76
|
+
]);
|
|
77
|
+
expect(screen.getAllByTestId("footer-attachment-status")).toHaveLength(1);
|
|
78
|
+
fireEvent.click(screen.getByLabelText("Pi: Connected"));
|
|
79
|
+
expect(screen.getAllByText(/^Connected /)).toHaveLength(2);
|
|
80
|
+
expect(screen.getByText("a1")).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByText("a2")).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("preserves an open popover when the same identity reconnects", () => {
|
|
85
|
+
const { store } = renderStatus();
|
|
86
|
+
fireEvent.click(screen.getByLabelText("Pi: Connected"));
|
|
87
|
+
act(() =>
|
|
88
|
+
store.set(attachmentsAtom, [
|
|
89
|
+
{ ...AGENT, id: "a2", since: "2026-10-14T00:00:00Z" },
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByRole("button", { name: "Pi: Connected" }),
|
|
94
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
95
|
+
expect(screen.queryByText("a1")).not.toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText("a2")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not open a closed popover when the same identity reconnects", () => {
|
|
100
|
+
const { store } = renderStatus();
|
|
101
|
+
act(() => store.set(attachmentsAtom, [{ ...AGENT, id: "a2" }]));
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole("button", { name: "Pi: Connected" }),
|
|
104
|
+
).toHaveAttribute("aria-expanded", "false");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps anonymous attachments separate from matching client identities", () => {
|
|
108
|
+
renderStatus([
|
|
109
|
+
{ ...AGENT, identity: undefined },
|
|
110
|
+
{ ...AGENT, id: "a2", identity: undefined },
|
|
111
|
+
{ ...AGENT, id: "a3", identity: "a1" },
|
|
112
|
+
]);
|
|
113
|
+
expect(screen.getAllByTestId("footer-attachment-status")).toHaveLength(3);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("accepts client identities that match object property names", () => {
|
|
117
|
+
renderStatus([
|
|
118
|
+
{ ...AGENT, identity: "__proto__" },
|
|
119
|
+
{ ...AGENT, id: "a2", identity: "constructor" },
|
|
120
|
+
]);
|
|
121
|
+
expect(screen.getAllByTestId("footer-attachment-status")).toHaveLength(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it.each(["{Enter}", " "])("opens from the keyboard with %j", async (key) => {
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
renderStatus();
|
|
127
|
+
const button = screen.getByRole("button", { name: "Pi: Connected" });
|
|
128
|
+
expect(button.tagName).toBe("BUTTON");
|
|
129
|
+
expect(button).toHaveAttribute("type", "button");
|
|
130
|
+
await user.tab();
|
|
131
|
+
expect(button).toHaveFocus();
|
|
132
|
+
await user.keyboard(key);
|
|
133
|
+
expect(button).toHaveAttribute("aria-expanded", "true");
|
|
134
|
+
expect(screen.getByText(/^Connected /)).toBeInTheDocument();
|
|
135
|
+
await user.keyboard("{Escape}");
|
|
136
|
+
expect(button).toHaveAttribute("aria-expanded", "false");
|
|
137
|
+
expect(button).toHaveFocus();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it.each([
|
|
141
|
+
{ state: ConnectionState.CONNECTING },
|
|
142
|
+
{ state: ConnectionState.CLOSING },
|
|
143
|
+
{ state: ConnectionState.NOT_STARTED },
|
|
144
|
+
{
|
|
145
|
+
state: ConnectionState.CLOSED,
|
|
146
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
147
|
+
reason: "offline",
|
|
148
|
+
},
|
|
149
|
+
])("hides connected claims while the browser is %j", (connection) => {
|
|
150
|
+
const { store, container } = renderStatus();
|
|
151
|
+
act(() => store.set(connectionAtom, connection));
|
|
152
|
+
expect(container).toBeEmptyDOMElement();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("removes detached agents when the list is replaced", () => {
|
|
156
|
+
const { store, container } = renderStatus([
|
|
157
|
+
AGENT,
|
|
158
|
+
{ ...AGENT, id: "a2", identity: "claude-thread", name: "Claude Code" },
|
|
159
|
+
]);
|
|
160
|
+
act(() => store.set(attachmentsAtom, [AGENT]));
|
|
161
|
+
expect(
|
|
162
|
+
screen.queryByLabelText("Claude Code: Connected"),
|
|
163
|
+
).not.toBeInTheDocument();
|
|
164
|
+
act(() => store.set(attachmentsAtom, []));
|
|
165
|
+
expect(container).toBeEmptyDOMElement();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type React from "react";
|
|
4
|
+
import { Slot as SlotPrimitive } from "radix-ui";
|
|
4
5
|
import { forwardRef } from "react";
|
|
5
6
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
6
7
|
import { cn } from "@/utils/cn";
|
|
@@ -8,36 +9,43 @@ import { cn } from "@/utils/cn";
|
|
|
8
9
|
type FooterItemProps = {
|
|
9
10
|
selected: boolean;
|
|
10
11
|
tooltip: React.ReactNode;
|
|
12
|
+
asChild?: boolean;
|
|
11
13
|
} & React.HTMLAttributes<HTMLDivElement>;
|
|
12
14
|
|
|
13
15
|
export const FooterItem: React.FC<FooterItemProps> = forwardRef<
|
|
14
16
|
HTMLDivElement,
|
|
15
17
|
FooterItemProps
|
|
16
|
-
>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{content}
|
|
36
|
-
</Tooltip>
|
|
18
|
+
>(
|
|
19
|
+
(
|
|
20
|
+
{ children, tooltip, selected, className, asChild = false, ...rest },
|
|
21
|
+
ref,
|
|
22
|
+
) => {
|
|
23
|
+
const Component = asChild ? SlotPrimitive.Slot : "div";
|
|
24
|
+
const content = (
|
|
25
|
+
<Component
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
"h-full flex items-center p-2 text-sm shadow-inset font-mono cursor-pointer rounded",
|
|
29
|
+
!selected && "hover:bg-(--sage-3)",
|
|
30
|
+
selected && "bg-(--sage-4)",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Component>
|
|
37
37
|
);
|
|
38
|
-
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if (tooltip) {
|
|
40
|
+
return (
|
|
41
|
+
<Tooltip content={tooltip} side="top" delayDuration={200}>
|
|
42
|
+
{content}
|
|
43
|
+
</Tooltip>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return content;
|
|
48
|
+
},
|
|
49
|
+
);
|
|
42
50
|
|
|
43
51
|
FooterItem.displayName = "FooterItem";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { BotIcon } from "lucide-react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverContent,
|
|
9
|
+
PopoverTrigger,
|
|
10
|
+
} from "@/components/ui/popover";
|
|
11
|
+
import {
|
|
12
|
+
type Attachment,
|
|
13
|
+
agentAttachmentsAtom,
|
|
14
|
+
attachmentLabel,
|
|
15
|
+
} from "@/core/attachments/state";
|
|
16
|
+
import { pairPreviewAtom } from "@/core/config/pair";
|
|
17
|
+
import { useTimeAgo } from "@/hooks/useFormatting";
|
|
18
|
+
import { Objects } from "@/utils/objects";
|
|
19
|
+
import { FooterItem } from "../footer-item";
|
|
20
|
+
|
|
21
|
+
export function AttachmentStatus() {
|
|
22
|
+
const pairPreview = useAtomValue(pairPreviewAtom);
|
|
23
|
+
const agents = useAtomValue(agentAttachmentsAtom);
|
|
24
|
+
if (!pairPreview) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const groups = Objects.groupBy(
|
|
28
|
+
agents,
|
|
29
|
+
(attachment) =>
|
|
30
|
+
attachment.identity
|
|
31
|
+
? `identity:${attachment.identity}`
|
|
32
|
+
: `attachment:${attachment.id}`,
|
|
33
|
+
(attachment) => attachment,
|
|
34
|
+
);
|
|
35
|
+
return Object.entries(groups).map(([identity, attachments]) => (
|
|
36
|
+
<AttachmentStatusItem key={identity} attachments={attachments} />
|
|
37
|
+
));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function AttachmentStatusItem({ attachments }: { attachments: Attachment[] }) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const timeAgo = useTimeAgo();
|
|
43
|
+
const label = attachmentLabel(attachments[0]);
|
|
44
|
+
return (
|
|
45
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
46
|
+
<PopoverTrigger asChild={true}>
|
|
47
|
+
<FooterItem
|
|
48
|
+
asChild={true}
|
|
49
|
+
tooltip={`${label}: connected`}
|
|
50
|
+
selected={open}
|
|
51
|
+
data-testid="footer-attachment-status"
|
|
52
|
+
aria-label={`${label}: Connected`}
|
|
53
|
+
>
|
|
54
|
+
<button type="button">
|
|
55
|
+
<span className="flex items-center gap-1.5">
|
|
56
|
+
<BotIcon className="w-4 h-4" />
|
|
57
|
+
<span className="max-w-48 truncate">{label}</span>
|
|
58
|
+
</span>
|
|
59
|
+
</button>
|
|
60
|
+
</FooterItem>
|
|
61
|
+
</PopoverTrigger>
|
|
62
|
+
<PopoverContent className="w-64 text-sm">
|
|
63
|
+
<h4 className="font-medium">{label}</h4>
|
|
64
|
+
<ul className="space-y-2">
|
|
65
|
+
{attachments.map((attachment) => (
|
|
66
|
+
<li key={attachment.id}>
|
|
67
|
+
<p
|
|
68
|
+
className="truncate text-xs text-muted-foreground"
|
|
69
|
+
title={attachment.id}
|
|
70
|
+
>
|
|
71
|
+
{attachment.id}
|
|
72
|
+
</p>
|
|
73
|
+
<p className="text-muted-foreground">
|
|
74
|
+
Connected {timeAgo(attachment.since)}
|
|
75
|
+
</p>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
</PopoverContent>
|
|
80
|
+
</Popover>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -12,6 +12,7 @@ import { ShowInKioskMode } from "../../kiosk-mode";
|
|
|
12
12
|
import { panelLayoutAtom, useChromeActions, useChromeState } from "../state";
|
|
13
13
|
import { FooterItem } from "./footer-item";
|
|
14
14
|
import { AIStatusIcon } from "./footer-items/ai-status";
|
|
15
|
+
import { AttachmentStatus } from "./footer-items/attachment-status";
|
|
15
16
|
import {
|
|
16
17
|
BackendConnectionStatus,
|
|
17
18
|
connectionStatusAtom,
|
|
@@ -107,6 +108,7 @@ export const Footer: React.FC = () => {
|
|
|
107
108
|
|
|
108
109
|
<div className="flex items-center shrink-0 min-w-0">
|
|
109
110
|
<MachineStats />
|
|
111
|
+
<AttachmentStatus />
|
|
110
112
|
<AIStatusIcon />
|
|
111
113
|
<CopilotStatusIcon />
|
|
112
114
|
<RTCStatus />
|