jupyterlab-codex-sidebar 0.1.4 → 0.1.6

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 (153) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.github/workflows/unit-tests.yml +27 -0
  3. package/.jupyterlab-playwright.log +0 -0
  4. package/README.md +83 -9
  5. package/docs/images/codex-sidebar-screenshot.png +0 -0
  6. package/jupyterlab_codex/handlers.py +938 -297
  7. package/jupyterlab_codex/labextension/package.json +13 -3
  8. package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
  9. package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
  10. package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
  11. package/jupyterlab_codex/protocol.py +297 -0
  12. package/jupyterlab_codex/runner.py +58 -15
  13. package/jupyterlab_codex/sessions.py +582 -97
  14. package/lib/codexChat.d.ts +13 -0
  15. package/lib/codexChat.js +2506 -0
  16. package/lib/codexChat.js.map +1 -0
  17. package/lib/codexChatAttachmentDedup.d.ts +10 -0
  18. package/lib/codexChatAttachmentDedup.js +35 -0
  19. package/lib/codexChatAttachmentDedup.js.map +1 -0
  20. package/lib/codexChatAttachmentLimit.d.ts +18 -0
  21. package/lib/codexChatAttachmentLimit.js +50 -0
  22. package/lib/codexChatAttachmentLimit.js.map +1 -0
  23. package/lib/codexChatAttachmentState.d.ts +15 -0
  24. package/lib/codexChatAttachmentState.js +16 -0
  25. package/lib/codexChatAttachmentState.js.map +1 -0
  26. package/lib/codexChatDocumentUtils.d.ts +70 -0
  27. package/lib/codexChatDocumentUtils.js +506 -0
  28. package/lib/codexChatDocumentUtils.js.map +1 -0
  29. package/lib/codexChatFormatting.d.ts +11 -0
  30. package/lib/codexChatFormatting.js +83 -0
  31. package/lib/codexChatFormatting.js.map +1 -0
  32. package/lib/codexChatNotice.d.ts +3 -0
  33. package/lib/codexChatNotice.js +74 -0
  34. package/lib/codexChatNotice.js.map +1 -0
  35. package/lib/codexChatPersistence.d.ts +35 -0
  36. package/lib/codexChatPersistence.js +158 -0
  37. package/lib/codexChatPersistence.js.map +1 -0
  38. package/lib/codexChatPrimitives.d.ts +44 -0
  39. package/lib/codexChatPrimitives.js +156 -0
  40. package/lib/codexChatPrimitives.js.map +1 -0
  41. package/lib/codexChatRender.d.ts +24 -0
  42. package/lib/codexChatRender.js +293 -0
  43. package/lib/codexChatRender.js.map +1 -0
  44. package/lib/codexChatSessionFactory.d.ts +15 -0
  45. package/lib/codexChatSessionFactory.js +45 -0
  46. package/lib/codexChatSessionFactory.js.map +1 -0
  47. package/lib/codexChatSessionKey.d.ts +3 -0
  48. package/lib/codexChatSessionKey.js +14 -0
  49. package/lib/codexChatSessionKey.js.map +1 -0
  50. package/lib/codexChatStorage.d.ts +4 -0
  51. package/lib/codexChatStorage.js +37 -0
  52. package/lib/codexChatStorage.js.map +1 -0
  53. package/lib/codexSessionResolver.d.ts +12 -0
  54. package/lib/codexSessionResolver.js +38 -0
  55. package/lib/codexSessionResolver.js.map +1 -0
  56. package/lib/handlers/activitySummarizer.d.ts +15 -0
  57. package/lib/handlers/activitySummarizer.js +327 -0
  58. package/lib/handlers/activitySummarizer.js.map +1 -0
  59. package/lib/handlers/codexMessageTypes.d.ts +30 -0
  60. package/lib/handlers/codexMessageTypes.js +2 -0
  61. package/lib/handlers/codexMessageTypes.js.map +1 -0
  62. package/lib/handlers/codexMessageUtils.d.ts +46 -0
  63. package/lib/handlers/codexMessageUtils.js +144 -0
  64. package/lib/handlers/codexMessageUtils.js.map +1 -0
  65. package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
  66. package/lib/handlers/handleCodexSocketMessage.js +78 -0
  67. package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
  68. package/lib/handlers/sessionSyncHandler.d.ts +34 -0
  69. package/lib/handlers/sessionSyncHandler.js +181 -0
  70. package/lib/handlers/sessionSyncHandler.js.map +1 -0
  71. package/lib/hooks/useCodexSocket.d.ts +15 -0
  72. package/lib/hooks/useCodexSocket.js +84 -0
  73. package/lib/hooks/useCodexSocket.js.map +1 -0
  74. package/lib/index.js +1 -1
  75. package/lib/index.js.map +1 -1
  76. package/lib/panel.d.ts +1 -11
  77. package/lib/panel.js +1 -2815
  78. package/lib/panel.js.map +1 -1
  79. package/lib/protocol.d.ts +235 -0
  80. package/lib/protocol.js +278 -0
  81. package/lib/protocol.js.map +1 -0
  82. package/package.json +13 -3
  83. package/playwright.config.cjs +27 -0
  84. package/playwright.unit.config.cjs +19 -0
  85. package/pyproject.toml +1 -1
  86. package/release.sh +52 -14
  87. package/scripts/run_playwright_e2e.sh +96 -0
  88. package/scripts/run_playwright_freeze_repro.sh +58 -0
  89. package/scripts/run_playwright_queue_repro.sh +60 -0
  90. package/scripts/run_playwright_repro.sh +55 -0
  91. package/src/codexChat.tsx +3914 -0
  92. package/src/codexChatAttachmentDedup.ts +47 -0
  93. package/src/codexChatAttachmentLimit.ts +81 -0
  94. package/src/codexChatAttachmentState.ts +37 -0
  95. package/src/codexChatDocumentUtils.ts +644 -0
  96. package/src/codexChatFormatting.ts +94 -0
  97. package/src/codexChatNotice.ts +95 -0
  98. package/src/codexChatPersistence.ts +191 -0
  99. package/src/codexChatPrimitives.tsx +446 -0
  100. package/src/codexChatRender.tsx +376 -0
  101. package/src/codexChatSessionFactory.ts +79 -0
  102. package/src/codexChatSessionKey.ts +16 -0
  103. package/src/codexChatStorage.ts +36 -0
  104. package/src/codexSessionResolver.ts +56 -0
  105. package/src/handlers/activitySummarizer.ts +369 -0
  106. package/src/handlers/codexMessageTypes.ts +34 -0
  107. package/src/handlers/codexMessageUtils.ts +217 -0
  108. package/src/handlers/handleCodexSocketMessage.ts +204 -0
  109. package/src/handlers/sessionSyncHandler.ts +308 -0
  110. package/src/hooks/useCodexSocket.ts +109 -0
  111. package/src/index.ts +1 -1
  112. package/src/panel.tsx +1 -4184
  113. package/src/protocol.ts +582 -0
  114. package/style/index.css +480 -11
  115. package/test-results/.last-run.json +4 -0
  116. package/test.py +0 -0
  117. package/tests/e2e/cell-output-error-tail.spec.js +156 -0
  118. package/tests/e2e/codex-ui-test-helpers.js +138 -0
  119. package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
  120. package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
  121. package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
  122. package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
  123. package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
  124. package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
  125. package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
  126. package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
  127. package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
  128. package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
  129. package/tests/e2e/mock-codex-cli-flood.py +127 -0
  130. package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
  131. package/tests/e2e/mock-codex-cli.py +95 -0
  132. package/tests/e2e/queue-multitab-repro.spec.js +189 -0
  133. package/tests/test_handlers.py +116 -0
  134. package/tests/test_protocol.py +169 -0
  135. package/tests/test_session_store_limits.py +50 -0
  136. package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
  137. package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
  138. package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
  139. package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
  140. package/tests/unit/codexChatLimit.spec.ts +18 -0
  141. package/tests/unit/codexChatNotice.spec.ts +45 -0
  142. package/tests/unit/codexChatPersistence.spec.ts +199 -0
  143. package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
  144. package/tests/unit/codexChatSessionKey.spec.ts +18 -0
  145. package/tests/unit/codexMessageUtils.spec.ts +89 -0
  146. package/tests/unit/codexSessionResolver.spec.ts +92 -0
  147. package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
  148. package/tsconfig.tsbuildinfo +1 -1
  149. package/webpack.config.js +6 -0
  150. package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
  151. package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
  152. package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
  153. /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
@@ -0,0 +1,446 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ type PortalAlign = 'left' | 'right';
5
+ type PortalRole = 'dialog' | 'menu' | 'tooltip';
6
+
7
+ type PortalMenuProps = {
8
+ open: boolean;
9
+ anchorRef: React.RefObject<HTMLElement>;
10
+ popoverRef: React.RefObject<HTMLDivElement>;
11
+ className?: string;
12
+ role?: PortalRole;
13
+ align?: PortalAlign;
14
+ ariaLabel?: string;
15
+ constrainHeightToViewport?: boolean;
16
+ viewportMargin?: number;
17
+ onMouseEnter?: () => void;
18
+ onMouseLeave?: () => void;
19
+ children: React.ReactNode;
20
+ };
21
+
22
+ export function PlusIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
23
+ return (
24
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
25
+ <path
26
+ d="M12 5v14M5 12h14"
27
+ stroke="currentColor"
28
+ strokeWidth="2"
29
+ strokeLinecap="round"
30
+ />
31
+ </svg>
32
+ );
33
+ }
34
+
35
+ export function ArrowUpIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
36
+ return (
37
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
38
+ <path
39
+ d="M12 19V5m0 0l-7 7m7-7l7 7"
40
+ stroke="currentColor"
41
+ strokeWidth="2"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ />
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export function ArrowDownIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
50
+ return (
51
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
52
+ <path
53
+ d="M12 5v14m0 0l-7-7m7 7l7-7"
54
+ stroke="currentColor"
55
+ strokeWidth="2"
56
+ strokeLinecap="round"
57
+ strokeLinejoin="round"
58
+ />
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ export function StopIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
64
+ return (
65
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
66
+ <rect x="7" y="7" width="10" height="10" rx="2.2" stroke="currentColor" strokeWidth="2" />
67
+ </svg>
68
+ );
69
+ }
70
+
71
+ export function ImageIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
72
+ return (
73
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
74
+ <rect x="3" y="5" width="18" height="14" rx="2.2" stroke="currentColor" strokeWidth="2" />
75
+ <circle cx="8.5" cy="10" r="1.5" fill="currentColor" />
76
+ <path
77
+ d="M21 16l-5.2-5.2a1 1 0 0 0-1.4 0L8.2 17 6 14.8a1 1 0 0 0-1.4 0L3 16.4"
78
+ stroke="currentColor"
79
+ strokeWidth="2"
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ />
83
+ </svg>
84
+ );
85
+ }
86
+
87
+ export function GearIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
88
+ return (
89
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
90
+ <path
91
+ d="M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4Z"
92
+ stroke="currentColor"
93
+ strokeWidth="2"
94
+ />
95
+ <path
96
+ d="M19.4 12a7.5 7.5 0 0 0-.1-1l2-1.6-1.9-3.2-2.4 1a8 8 0 0 0-1.8-1l-.4-2.5H9.2l-.4 2.5a8 8 0 0 0-1.8 1l-2.4-1-1.9 3.2 2 1.6a7.5 7.5 0 0 0 0 2l-2 1.6 1.9 3.2 2.4-1a8 8 0 0 0 1.8 1l.4 2.5h5.6l.4-2.5a8 8 0 0 0 1.8-1l2.4 1 1.9-3.2-2-1.6c.1-.3.1-.7.1-1Z"
97
+ stroke="currentColor"
98
+ strokeWidth="2"
99
+ strokeLinejoin="round"
100
+ />
101
+ </svg>
102
+ );
103
+ }
104
+
105
+ export function ChipIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
106
+ return (
107
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
108
+ <path
109
+ d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"
110
+ stroke="currentColor"
111
+ strokeWidth="2"
112
+ strokeLinecap="round"
113
+ />
114
+ <rect
115
+ x="7"
116
+ y="7"
117
+ width="10"
118
+ height="10"
119
+ rx="2.5"
120
+ stroke="currentColor"
121
+ strokeWidth="2"
122
+ />
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ export function CellAttachmentIcon(
128
+ props: React.SVGProps<SVGSVGElement> & { active?: boolean }
129
+ ): JSX.Element {
130
+ const { active = true, ...svgProps } = props;
131
+ return (
132
+ <svg viewBox="0 -960 960 960" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
133
+ <path
134
+ d="m384-336 56-57-87-87 87-87-56-57-144 144 144 144Zm192 0 144-144-144-144-56 57 87 87-87 87 56 57ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Z"
135
+ fill="currentColor"
136
+ opacity={active ? 0.92 : 0.74}
137
+ />
138
+ {!active && (
139
+ <path
140
+ d="M250-250 710-710"
141
+ stroke="currentColor"
142
+ strokeWidth="88"
143
+ strokeLinecap="round"
144
+ opacity="0.9"
145
+ />
146
+ )}
147
+ </svg>
148
+ );
149
+ }
150
+
151
+ export function GaugeIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
152
+ return (
153
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
154
+ <path
155
+ d="M5 16a7 7 0 1 1 14 0"
156
+ stroke="currentColor"
157
+ strokeWidth="2"
158
+ strokeLinecap="round"
159
+ />
160
+ <path
161
+ d="M12 13l3.5-3.5"
162
+ stroke="currentColor"
163
+ strokeWidth="2"
164
+ strokeLinecap="round"
165
+ />
166
+ <path
167
+ d="M5 16h14"
168
+ stroke="currentColor"
169
+ strokeWidth="2"
170
+ strokeLinecap="round"
171
+ />
172
+ </svg>
173
+ );
174
+ }
175
+
176
+ export function ReasoningEffortIcon(
177
+ props: React.SVGProps<SVGSVGElement> & { activeBars: number; isConfig?: boolean }
178
+ ): JSX.Element {
179
+ const { activeBars, isConfig = false, ...svgProps } = props;
180
+ if (isConfig && activeBars <= 0) {
181
+ return <GaugeIcon {...svgProps} />;
182
+ }
183
+ const bars = [
184
+ { x: 6, top: 14 },
185
+ { x: 10, top: 10 },
186
+ { x: 14, top: 6 },
187
+ { x: 18, top: 2 }
188
+ ];
189
+ return (
190
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
191
+ {bars.map((bar, idx) => (
192
+ <path
193
+ key={idx}
194
+ d={`M${bar.x} 20V${bar.top}`}
195
+ stroke="currentColor"
196
+ strokeWidth="2.6"
197
+ strokeLinecap="round"
198
+ opacity={idx < activeBars ? 1 : 0.25}
199
+ />
200
+ ))}
201
+ </svg>
202
+ );
203
+ }
204
+
205
+ export function BatteryIcon(
206
+ props: React.SVGProps<SVGSVGElement> & { level?: number | null }
207
+ ): JSX.Element {
208
+ const { level, ...svgProps } = props;
209
+ const clamped =
210
+ typeof level === 'number' && Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : null;
211
+ const innerWidth = 14;
212
+ const fillWidth = clamped == null ? 0 : Math.max(0, Math.round(innerWidth * clamped));
213
+ const dashed = clamped == null;
214
+ return (
215
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
216
+ <rect
217
+ x="2"
218
+ y="7"
219
+ width="18"
220
+ height="10"
221
+ rx="2.5"
222
+ stroke="currentColor"
223
+ strokeWidth="2"
224
+ strokeDasharray={dashed ? '4 2' : undefined}
225
+ />
226
+ <rect x="21" y="10" width="2" height="4" rx="1" fill="currentColor" opacity={0.85} />
227
+ {fillWidth > 0 && (
228
+ <rect
229
+ x="4"
230
+ y="9"
231
+ width={fillWidth}
232
+ height="6"
233
+ rx="1.4"
234
+ fill="currentColor"
235
+ opacity={0.9}
236
+ />
237
+ )}
238
+ </svg>
239
+ );
240
+ }
241
+
242
+ export function ContextWindowIcon(
243
+ props: React.SVGProps<SVGSVGElement> & { level?: number | null }
244
+ ): JSX.Element {
245
+ const { level, ...svgProps } = props;
246
+ const clamped =
247
+ typeof level === 'number' && Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : null;
248
+ const fillRatio = clamped == null ? 0.35 : Math.max(0.05, clamped);
249
+ const barX = 4;
250
+ const barY = 4;
251
+ const barW = 16;
252
+ const barH = 16;
253
+ const fillW = Math.round(barW * fillRatio);
254
+ return (
255
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
256
+ <rect
257
+ x={barX}
258
+ y={barY}
259
+ width={barW}
260
+ height={barH}
261
+ rx="3.4"
262
+ stroke="currentColor"
263
+ strokeWidth="2"
264
+ />
265
+ <rect x={barX + 2} y={barY + 2} width={fillW} height={barH - 4} rx="1.4" fill="currentColor" opacity="0.85" />
266
+ </svg>
267
+ );
268
+ }
269
+
270
+ export function ShieldIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
271
+ return (
272
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
273
+ <path
274
+ d="M12 2.8 19 5.9v6.2c0 5-3 9.2-7 9.9-4-.7-7-4.9-7-9.9V5.9l7-3.1Z"
275
+ stroke="currentColor"
276
+ strokeWidth="2"
277
+ strokeLinejoin="round"
278
+ />
279
+ </svg>
280
+ );
281
+ }
282
+
283
+ export function CheckIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
284
+ return (
285
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
286
+ <path
287
+ d="M20 6 9 17l-5-5"
288
+ stroke="currentColor"
289
+ strokeWidth="2"
290
+ strokeLinecap="round"
291
+ strokeLinejoin="round"
292
+ />
293
+ </svg>
294
+ );
295
+ }
296
+
297
+ export function FileIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
298
+ return (
299
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
300
+ <path
301
+ d="M8 3h6l5 5v10.5A2.5 2.5 0 0 1 16.5 21h-9A2.5 2.5 0 0 1 5 18.5v-13A2.5 2.5 0 0 1 7.5 3H8Z"
302
+ stroke="currentColor"
303
+ strokeWidth="2"
304
+ strokeLinejoin="round"
305
+ />
306
+ <path
307
+ d="M14 3v5h5"
308
+ stroke="currentColor"
309
+ strokeWidth="2"
310
+ strokeLinecap="round"
311
+ strokeLinejoin="round"
312
+ />
313
+ </svg>
314
+ );
315
+ }
316
+
317
+ export function XIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
318
+ return (
319
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
320
+ <path
321
+ d="M18 6 6 18M6 6l12 12"
322
+ stroke="currentColor"
323
+ strokeWidth="2"
324
+ strokeLinecap="round"
325
+ strokeLinejoin="round"
326
+ />
327
+ </svg>
328
+ );
329
+ }
330
+
331
+ export function PortalMenu(props: PortalMenuProps): JSX.Element | null {
332
+ const {
333
+ open,
334
+ anchorRef,
335
+ popoverRef,
336
+ className,
337
+ role = 'dialog',
338
+ align = 'left',
339
+ ariaLabel,
340
+ constrainHeightToViewport = false,
341
+ viewportMargin,
342
+ onMouseEnter,
343
+ onMouseLeave,
344
+ children
345
+ } = props;
346
+ const [style, setStyle] = React.useState<React.CSSProperties>(() => ({
347
+ left: 0,
348
+ top: 0,
349
+ visibility: 'hidden'
350
+ }));
351
+
352
+ React.useLayoutEffect(() => {
353
+ if (!open) {
354
+ return;
355
+ }
356
+
357
+ const update = () => {
358
+ const anchor = anchorRef.current;
359
+ const popover = popoverRef.current;
360
+ if (!anchor || !popover) {
361
+ return;
362
+ }
363
+
364
+ const margin = Math.max(0, Math.floor(viewportMargin ?? 8));
365
+ const offset = 10;
366
+ const viewportW = window.visualViewport?.width ?? window.innerWidth;
367
+ const viewportH = window.visualViewport?.height ?? window.innerHeight;
368
+ const viewportLeft = window.visualViewport?.offsetLeft ?? 0;
369
+ const viewportTop = window.visualViewport?.offsetTop ?? 0;
370
+ const bubbleEl = anchor.closest('.jp-CodexChat-message');
371
+ let bubbleMarginY = 0;
372
+ if (bubbleEl && bubbleEl instanceof HTMLElement) {
373
+ const bubbleStyle = window.getComputedStyle(bubbleEl);
374
+ const bubbleMarginTop = Number.parseFloat(bubbleStyle.marginTop) || 0;
375
+ const bubbleMarginBottom = Number.parseFloat(bubbleStyle.marginBottom) || 0;
376
+ bubbleMarginY = Math.max(0, Math.max(bubbleMarginTop, bubbleMarginBottom));
377
+ }
378
+ const viewportEdgePadding = margin + Math.round(bubbleMarginY);
379
+ const anchorRect = anchor.getBoundingClientRect();
380
+ const popRect = popover.getBoundingClientRect();
381
+ const maxHeight = Math.max(80, Math.floor(viewportH - viewportEdgePadding * 2));
382
+ const effectiveHeight = constrainHeightToViewport
383
+ ? Math.min(popRect.height, maxHeight)
384
+ : popRect.height;
385
+
386
+ let left = align === 'right' ? anchorRect.right - popRect.width : anchorRect.left;
387
+ left = clamp(left, viewportLeft + margin, Math.max(viewportLeft + margin, viewportLeft + viewportW - popRect.width - margin));
388
+
389
+ let top = anchorRect.top - offset - effectiveHeight;
390
+ const belowTop = anchorRect.bottom + offset;
391
+ const minTop = viewportTop + viewportEdgePadding;
392
+ const maxBottom = viewportTop + viewportH;
393
+ if (top < minTop && belowTop + effectiveHeight + viewportEdgePadding <= maxBottom) {
394
+ top = belowTop;
395
+ }
396
+ top = clamp(top, minTop, Math.max(minTop, maxBottom - effectiveHeight - viewportEdgePadding));
397
+
398
+ setStyle({
399
+ left: Math.round(left),
400
+ top: Math.round(top),
401
+ ...(constrainHeightToViewport
402
+ ? {
403
+ maxHeight: `${maxHeight}px`,
404
+ overflowY: 'auto',
405
+ overflowX: 'auto'
406
+ }
407
+ : {}),
408
+ visibility: 'visible'
409
+ });
410
+ };
411
+
412
+ const raf = window.requestAnimationFrame(update);
413
+ const onResize = () => update();
414
+ const onScroll = () => update();
415
+ window.addEventListener('resize', onResize);
416
+ window.addEventListener('scroll', onScroll, true);
417
+ return () => {
418
+ window.cancelAnimationFrame(raf);
419
+ window.removeEventListener('resize', onResize);
420
+ window.removeEventListener('scroll', onScroll, true);
421
+ };
422
+ }, [open, anchorRef, popoverRef, align, constrainHeightToViewport, viewportMargin]);
423
+
424
+ if (!open) {
425
+ return null;
426
+ }
427
+
428
+ return createPortal(
429
+ <div
430
+ ref={popoverRef}
431
+ className={`jp-CodexMenu jp-CodexMenuPortal${className ? ` ${className}` : ''}`}
432
+ role={role}
433
+ aria-label={ariaLabel}
434
+ style={style}
435
+ onMouseEnter={onMouseEnter}
436
+ onMouseLeave={onMouseLeave}
437
+ >
438
+ {children}
439
+ </div>,
440
+ document.body
441
+ );
442
+ }
443
+
444
+ function clamp(value: number, min: number, max: number): number {
445
+ return Math.min(max, Math.max(min, value));
446
+ }