jupyterlab-codex-sidebar 0.1.5 → 0.1.7

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 (41) hide show
  1. package/.jupyterlab-playwright.log +43 -0
  2. package/README.md +19 -3
  3. package/jupyterlab_codex/labextension/package.json +2 -2
  4. package/jupyterlab_codex/labextension/static/737.58bcf09100c9bc7bd90d.js +1 -0
  5. package/jupyterlab_codex/labextension/static/{remoteEntry.c1e865f207776f7f24ff.js → remoteEntry.34af8a0df422b4d029c3.js} +1 -1
  6. package/jupyterlab_codex/sessions.py +1 -1
  7. package/lib/codexChat.js +148 -52
  8. package/lib/codexChat.js.map +1 -1
  9. package/lib/codexChatAttachmentLimit.d.ts +12 -2
  10. package/lib/codexChatAttachmentLimit.js +43 -30
  11. package/lib/codexChatAttachmentLimit.js.map +1 -1
  12. package/lib/codexChatAttachmentState.d.ts +15 -0
  13. package/lib/codexChatAttachmentState.js +16 -0
  14. package/lib/codexChatAttachmentState.js.map +1 -0
  15. package/lib/codexChatDocumentUtils.d.ts +4 -1
  16. package/lib/codexChatDocumentUtils.js +71 -19
  17. package/lib/codexChatDocumentUtils.js.map +1 -1
  18. package/lib/codexChatPrimitives.d.ts +4 -1
  19. package/lib/codexChatPrimitives.js +4 -0
  20. package/lib/codexChatPrimitives.js.map +1 -1
  21. package/package.json +1 -1
  22. package/playwright.config.cjs +4 -1
  23. package/pyproject.toml +1 -1
  24. package/src/codexChat.tsx +234 -75
  25. package/src/codexChatAttachmentLimit.ts +59 -33
  26. package/src/codexChatAttachmentState.ts +37 -0
  27. package/src/codexChatDocumentUtils.ts +89 -21
  28. package/src/codexChatPrimitives.tsx +25 -1
  29. package/style/index.css +96 -40
  30. package/test-results/.last-run.json +4 -0
  31. package/test.py +0 -0
  32. package/tests/e2e/cell-output-error-tail.spec.js +165 -0
  33. package/tests/e2e/codex-ui-test-helpers.js +138 -0
  34. package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
  35. package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
  36. package/tests/e2e/mock-codex-cli-prompt-echo.py +92 -0
  37. package/tests/unit/codexChatAttachmentLimit.spec.ts +33 -8
  38. package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
  39. package/tests/unit/codexChatDocumentUtils.spec.ts +78 -0
  40. package/tsconfig.tsbuildinfo +1 -1
  41. package/jupyterlab_codex/labextension/static/855.d20f6158cd81bb4c9056.js +0 -1
@@ -272,7 +272,7 @@ class SessionStore:
272
272
  parts.append("")
273
273
 
274
274
  include_selection = mode in {"ipynb", "jupytext_py", "plain_py"}
275
- include_cell_output = mode == "ipynb"
275
+ include_cell_output = mode in {"ipynb", "jupytext_py"}
276
276
 
277
277
  if include_selection and selection:
278
278
  parts.append("Current Cell Content:")
package/lib/codexChat.js CHANGED
@@ -12,10 +12,11 @@ import { STORAGE_KEY_SESSION_THREADS, STORAGE_KEY_SESSION_THREADS_EVENT, buildSe
12
12
  import { createSession as createBaseSession, createThreadResetSession as createBaseThreadResetSession } from './codexChatSessionFactory';
13
13
  import { resolveCurrentSessionKey, resolveSessionKey } from './codexChatSessionKey';
14
14
  import { resolveMessageSessionKey as resolveMessageSessionKeyForMessage } from './codexSessionResolver';
15
- import { MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS, captureDocumentViewState, findDocumentWidgetByPath, getActiveCellOutput, getActiveCellText, getActiveDocumentWidget, getDocumentContext, getSelectedContext, getSelectedTextFromActiveCell, getSelectedTextFromFileEditor, getSupportedDocumentPath, normalizeSelectionPreviewText, restoreDocumentViewState, toCellOutputPreview, toFallbackSelectionPreview, } from './codexChatDocumentUtils';
15
+ import { MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS, captureDocumentViewState, findDocumentWidgetByPath, getActiveCellOutput, getActiveCellText, getActiveDocumentWidget, getDocumentContext, getSelectedContext, getSelectedTextFromActiveCell, getSelectedTextFromFileEditor, getSupportedDocumentPath, isNotebookWidget, normalizeSelectionPreviewText, restoreDocumentViewState, toCellOutputPreview, toMessageSelectionPreview, } from './codexChatDocumentUtils';
16
+ import { resolveCellAttachmentState } from './codexChatAttachmentState';
16
17
  import { buildActiveCellOutputSignature, buildActiveCellSelectionSignature, isDuplicateActiveCellAttachmentSignature, makeActiveCellAttachmentDedupKey } from './codexChatAttachmentDedup';
17
- import { buildAttachmentTruncationNotice, limitActiveCellAttachmentPayload } from './codexChatAttachmentLimit';
18
- import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ContextWindowIcon, FileIcon, GearIcon, ImageIcon, PlusIcon, PortalMenu, ReasoningEffortIcon, ShieldIcon, StopIcon, XIcon, BatteryIcon } from './codexChatPrimitives';
18
+ import { buildAttachmentTruncationNotice, limitActiveCellAttachmentPayload, resolveSentAttachmentTruncation } from './codexChatAttachmentLimit';
19
+ import { ArrowDownIcon, ArrowUpIcon, BatteryIcon, CellAttachmentIcon, CheckIcon, ContextWindowIcon, FileIcon, GearIcon, ImageIcon, PlusIcon, PortalMenu, ReasoningEffortIcon, ShieldIcon, StopIcon, XIcon } from './codexChatPrimitives';
19
20
  import { hasStoredValue, safeLocalStorageGet, safeLocalStorageRemove, safeLocalStorageSet } from './codexChatStorage';
20
21
  const truncateEnd = truncateEndShared;
21
22
  export class CodexPanel extends ReactWidget {
@@ -93,7 +94,8 @@ const SELECTION_PREVIEWS_STORAGE_KEY = 'jupyterlab-codex:selection-previews';
93
94
  const MAX_IMAGE_ATTACHMENTS = 4;
94
95
  const MAX_IMAGE_ATTACHMENT_BYTES = 4 * 1024 * 1024; // Avoid huge WebSocket payloads.
95
96
  const MAX_IMAGE_ATTACHMENT_TOTAL_BYTES = 6 * 1024 * 1024;
96
- const MAX_ACTIVE_CELL_ATTACHMENT_TOTAL_CHARS = 4000;
97
+ const MAX_ACTIVE_CELL_SELECTION_CHARS = 4000;
98
+ const MAX_ACTIVE_CELL_OUTPUT_CHARS = 20000;
97
99
  const MAX_STORED_SELECTION_PREVIEW_THREADS = 80;
98
100
  const MAX_STORED_SELECTION_PREVIEW_MESSAGES_PER_THREAD = 10;
99
101
  const MAX_SESSION_MESSAGES = 100;
@@ -444,7 +446,7 @@ function CodexChat(props) {
444
446
  const [autoSaveBeforeSend, setAutoSaveBeforeSend] = useState(() => readStoredAutoSave());
445
447
  const [includeActiveCell, setIncludeActiveCell] = useState(() => readStoredIncludeActiveCell());
446
448
  const [includeActiveCellOutput, setIncludeActiveCellOutput] = useState(() => readStoredIncludeActiveCellOutput());
447
- const [excludeCellAttachmentForNextSend, setExcludeCellAttachmentForNextSend] = useState(false);
449
+ const [currentDocumentIsNotebookEditor, setCurrentDocumentIsNotebookEditor] = useState(false);
448
450
  const [notifyOnDone, setNotifyOnDone] = useState(() => readStoredNotifyOnDone());
449
451
  const [notifyOnDoneMinSeconds, setNotifyOnDoneMinSeconds] = useState(() => readStoredNotifyOnDoneMinSeconds());
450
452
  const [settingsOpen, setSettingsOpen] = useState(() => readStoredSettingsOpen());
@@ -463,7 +465,9 @@ function CodexChat(props) {
463
465
  const [reasoningMenuOpen, setReasoningMenuOpen] = useState(false);
464
466
  const [usagePopoverOpen, setUsagePopoverOpen] = useState(false);
465
467
  const [permissionMenuOpen, setPermissionMenuOpen] = useState(false);
468
+ const [contextPopoverOpen, setContextPopoverOpen] = useState(false);
466
469
  const [isPlainPyRunInProgress, setIsPlainPyRunInProgress] = useState(false);
470
+ const [cellAttachmentPopoverOpen, setCellAttachmentPopoverOpen] = useState(false);
467
471
  const [selectionPopover, setSelectionPopover] = useState(null);
468
472
  const storedSelectionPreviewsRef = useRef(readStoredSelectionPreviewsByThread());
469
473
  const previousSessionThreadIdsRef = useRef(new Map());
@@ -486,6 +490,7 @@ function CodexChat(props) {
486
490
  const reasoningMenuWrapRef = useRef(null);
487
491
  const usageMenuWrapRef = useRef(null);
488
492
  const permissionMenuWrapRef = useRef(null);
493
+ const contextMenuWrapRef = useRef(null);
489
494
  const modelBtnRef = useRef(null);
490
495
  const modelPopoverRef = useRef(null);
491
496
  const reasoningBtnRef = useRef(null);
@@ -494,7 +499,13 @@ function CodexChat(props) {
494
499
  const usagePopoverRef = useRef(null);
495
500
  const permissionBtnRef = useRef(null);
496
501
  const permissionPopoverRef = useRef(null);
502
+ const contextBtnRef = useRef(null);
503
+ const contextPopoverRef = useRef(null);
497
504
  const plainPyRunSessionKeyRef = useRef('');
505
+ const cellAttachmentAnchorRef = useRef(null);
506
+ const cellAttachmentPopoverRef = useRef(null);
507
+ const cellAttachmentPopoverCloseTimerRef = useRef(null);
508
+ const contextPopoverCloseTimerRef = useRef(null);
498
509
  const selectionPopoverAnchorRef = useRef(null);
499
510
  const selectionPopoverRef = useRef(null);
500
511
  const notebookLabelRef = useRef(null);
@@ -643,12 +654,6 @@ function CodexChat(props) {
643
654
  useEffect(() => {
644
655
  persistIncludeActiveCellOutput(includeActiveCellOutput);
645
656
  }, [includeActiveCellOutput]);
646
- useEffect(() => {
647
- // Reset one-time exclusion when the base setting is turned off.
648
- if (!includeActiveCell && excludeCellAttachmentForNextSend) {
649
- setExcludeCellAttachmentForNextSend(false);
650
- }
651
- }, [includeActiveCell, excludeCellAttachmentForNextSend]);
652
657
  useEffect(() => {
653
658
  persistCommandPath(commandPath);
654
659
  }, [commandPath]);
@@ -671,7 +676,7 @@ function CodexChat(props) {
671
676
  persistNotifyOnDoneMinSeconds(normalized);
672
677
  }, [notifyOnDoneMinSeconds]);
673
678
  useEffect(() => {
674
- if (!modelMenuOpen && !reasoningMenuOpen && !usagePopoverOpen && !permissionMenuOpen) {
679
+ if (!modelMenuOpen && !reasoningMenuOpen && !usagePopoverOpen && !permissionMenuOpen && !contextPopoverOpen) {
675
680
  return;
676
681
  }
677
682
  const onPointerDown = (event) => {
@@ -683,24 +688,29 @@ function CodexChat(props) {
683
688
  const inReasoning = reasoningMenuWrapRef.current?.contains(target) ?? false;
684
689
  const inUsage = usageMenuWrapRef.current?.contains(target) ?? false;
685
690
  const inPermission = permissionMenuWrapRef.current?.contains(target) ?? false;
691
+ const inContext = contextMenuWrapRef.current?.contains(target) ?? false;
686
692
  const inModelPopover = modelPopoverRef.current?.contains(target) ?? false;
687
693
  const inReasoningPopover = reasoningPopoverRef.current?.contains(target) ?? false;
688
694
  const inUsagePopover = usagePopoverRef.current?.contains(target) ?? false;
689
695
  const inPermissionPopover = permissionPopoverRef.current?.contains(target) ?? false;
696
+ const inContextPopover = contextPopoverRef.current?.contains(target) ?? false;
690
697
  if (inModel ||
691
698
  inReasoning ||
692
699
  inUsage ||
693
700
  inPermission ||
701
+ inContext ||
694
702
  inModelPopover ||
695
703
  inReasoningPopover ||
696
704
  inUsagePopover ||
697
- inPermissionPopover) {
705
+ inPermissionPopover ||
706
+ inContextPopover) {
698
707
  return;
699
708
  }
700
709
  setModelMenuOpen(false);
701
710
  setReasoningMenuOpen(false);
702
711
  setUsagePopoverOpen(false);
703
712
  setPermissionMenuOpen(false);
713
+ setContextPopoverOpen(false);
704
714
  };
705
715
  const onKeyDown = (event) => {
706
716
  if (event.key !== 'Escape') {
@@ -711,6 +721,7 @@ function CodexChat(props) {
711
721
  setReasoningMenuOpen(false);
712
722
  setUsagePopoverOpen(false);
713
723
  setPermissionMenuOpen(false);
724
+ setContextPopoverOpen(false);
714
725
  };
715
726
  window.addEventListener('pointerdown', onPointerDown, true);
716
727
  window.addEventListener('keydown', onKeyDown);
@@ -718,7 +729,7 @@ function CodexChat(props) {
718
729
  window.removeEventListener('pointerdown', onPointerDown, true);
719
730
  window.removeEventListener('keydown', onKeyDown);
720
731
  };
721
- }, [modelMenuOpen, reasoningMenuOpen, usagePopoverOpen, permissionMenuOpen]);
732
+ }, [modelMenuOpen, reasoningMenuOpen, usagePopoverOpen, permissionMenuOpen, contextPopoverOpen]);
722
733
  useEffect(() => {
723
734
  if (!selectionPopover) {
724
735
  return;
@@ -755,6 +766,66 @@ function CodexChat(props) {
755
766
  setSelectionPopover(null);
756
767
  selectionPopoverAnchorRef.current = null;
757
768
  }
769
+ function clearCellAttachmentPopoverCloseTimer() {
770
+ if (cellAttachmentPopoverCloseTimerRef.current !== null) {
771
+ window.clearTimeout(cellAttachmentPopoverCloseTimerRef.current);
772
+ cellAttachmentPopoverCloseTimerRef.current = null;
773
+ }
774
+ }
775
+ function clearContextPopoverCloseTimer() {
776
+ if (contextPopoverCloseTimerRef.current !== null) {
777
+ window.clearTimeout(contextPopoverCloseTimerRef.current);
778
+ contextPopoverCloseTimerRef.current = null;
779
+ }
780
+ }
781
+ function openCellAttachmentPopover() {
782
+ clearCellAttachmentPopoverCloseTimer();
783
+ if (!showCellAttachmentBadge) {
784
+ setCellAttachmentPopoverOpen(false);
785
+ return;
786
+ }
787
+ setCellAttachmentPopoverOpen(true);
788
+ }
789
+ function scheduleCloseCellAttachmentPopover() {
790
+ clearCellAttachmentPopoverCloseTimer();
791
+ cellAttachmentPopoverCloseTimerRef.current = window.setTimeout(() => {
792
+ setCellAttachmentPopoverOpen(false);
793
+ cellAttachmentPopoverCloseTimerRef.current = null;
794
+ }, 90);
795
+ }
796
+ function openContextPopover() {
797
+ clearContextPopoverCloseTimer();
798
+ if (!hasContextUsageSnapshot) {
799
+ setContextPopoverOpen(false);
800
+ return;
801
+ }
802
+ setContextPopoverOpen(true);
803
+ }
804
+ function scheduleCloseContextPopover() {
805
+ clearContextPopoverCloseTimer();
806
+ contextPopoverCloseTimerRef.current = window.setTimeout(() => {
807
+ setContextPopoverOpen(false);
808
+ contextPopoverCloseTimerRef.current = null;
809
+ }, 90);
810
+ }
811
+ function handleContextPopoverBlur(event) {
812
+ const nextFocused = event.relatedTarget;
813
+ const inAnchor = nextFocused ? contextMenuWrapRef.current?.contains(nextFocused) ?? false : false;
814
+ const inPopover = nextFocused ? contextPopoverRef.current?.contains(nextFocused) ?? false : false;
815
+ if (inAnchor || inPopover) {
816
+ return;
817
+ }
818
+ scheduleCloseContextPopover();
819
+ }
820
+ function handleCellAttachmentBlur(event) {
821
+ const nextFocused = event.relatedTarget;
822
+ const inAnchor = nextFocused ? cellAttachmentAnchorRef.current?.contains(nextFocused) ?? false : false;
823
+ const inPopover = nextFocused ? cellAttachmentPopoverRef.current?.contains(nextFocused) ?? false : false;
824
+ if (inAnchor || inPopover) {
825
+ return;
826
+ }
827
+ scheduleCloseCellAttachmentPopover();
828
+ }
758
829
  function toggleSelectionPopover(messageId, preview, event) {
759
830
  if (!messageId) {
760
831
  return;
@@ -932,6 +1003,7 @@ function CodexChat(props) {
932
1003
  setModelMenuOpen(false);
933
1004
  setReasoningMenuOpen(false);
934
1005
  setPermissionMenuOpen(false);
1006
+ setContextPopoverOpen(false);
935
1007
  }
936
1008
  async function updateNotifyOnDone(enabled) {
937
1009
  if (!enabled) {
@@ -1428,6 +1500,8 @@ function CodexChat(props) {
1428
1500
  if (activeWidget) {
1429
1501
  activeDocumentWidgetRef.current = activeWidget;
1430
1502
  }
1503
+ const nextIsNotebookEditor = isNotebookWidget(activeWidget);
1504
+ setCurrentDocumentIsNotebookEditor(prev => (prev === nextIsNotebookEditor ? prev : nextIsNotebookEditor));
1431
1505
  const path = getSupportedDocumentPath(activeWidget);
1432
1506
  const sessionKey = resolveSessionKey(path);
1433
1507
  const previousKey = currentNotebookSessionKeyRef.current;
@@ -1472,23 +1546,6 @@ function CodexChat(props) {
1472
1546
  props.notebooks.currentChanged.disconnect(updateNotebook);
1473
1547
  };
1474
1548
  }, [props.app, props.notebooks]);
1475
- useEffect(() => {
1476
- const onActiveCellChanged = (_tracker, _cell) => {
1477
- if (!includeActiveCell || !excludeCellAttachmentForNextSend) {
1478
- return;
1479
- }
1480
- const currentNotebookWidget = props.notebooks.currentWidget;
1481
- const currentNotebookWidgetPath = getSupportedDocumentPath(currentNotebookWidget);
1482
- if (!currentNotebookWidgetPath || currentNotebookWidgetPath !== currentNotebookPathRef.current) {
1483
- return;
1484
- }
1485
- setExcludeCellAttachmentForNextSend(false);
1486
- };
1487
- props.notebooks.activeCellChanged.connect(onActiveCellChanged);
1488
- return () => {
1489
- props.notebooks.activeCellChanged.disconnect(onActiveCellChanged);
1490
- };
1491
- }, [props.notebooks, includeActiveCell, excludeCellAttachmentForNextSend]);
1492
1549
  useEffect(() => {
1493
1550
  const onStorage = (event) => {
1494
1551
  if (event.key !== STORAGE_KEY_SESSION_THREADS_EVENT || !event.newValue) {
@@ -1935,8 +1992,7 @@ function CodexChat(props) {
1935
1992
  const selectedTextForContext = selectedContext?.text || '';
1936
1993
  let includeSelectionKey = false;
1937
1994
  let selection = '';
1938
- const includeActiveCellForNextSend = includeActiveCell && !excludeCellAttachmentForNextSend;
1939
- if (includeActiveCellForNextSend) {
1995
+ if (includeActiveCell) {
1940
1996
  if (notebookMode === 'plain_py') {
1941
1997
  const selectedText = selectedTextForContext || getSelectedTextFromActiveCell(activeWidget) || getSelectedTextFromFileEditor(activeWidget);
1942
1998
  if (selectedText) {
@@ -1950,21 +2006,23 @@ function CodexChat(props) {
1950
2006
  selectedTextForContext || getActiveCellText(activeWidget) || getSelectedTextFromFileEditor(activeWidget);
1951
2007
  }
1952
2008
  }
1953
- const includeCellOutputKey = includeActiveCellForNextSend && includeActiveCellOutput && notebookMode === 'ipynb';
2009
+ const includeCellOutputKey = includeActiveCell &&
2010
+ includeActiveCellOutput &&
2011
+ (notebookMode === 'ipynb' || notebookMode === 'jupytext_py');
1954
2012
  const cellOutputRaw = includeCellOutputKey ? getActiveCellOutput(activeWidget) : '';
1955
- const attachmentLimit = limitActiveCellAttachmentPayload(includeSelectionKey ? selection : '', includeCellOutputKey ? cellOutputRaw : '', MAX_ACTIVE_CELL_ATTACHMENT_TOTAL_CHARS);
2013
+ const attachmentLimit = limitActiveCellAttachmentPayload(includeSelectionKey ? selection : '', includeCellOutputKey ? cellOutputRaw : '', MAX_ACTIVE_CELL_SELECTION_CHARS, MAX_ACTIVE_CELL_OUTPUT_CHARS);
1956
2014
  const selectionForAttachment = includeSelectionKey ? attachmentLimit.selection : '';
1957
2015
  const cellOutputForAttachment = includeCellOutputKey ? attachmentLimit.cellOutput : '';
1958
2016
  const includeSelectionKeyAfterLimit = Boolean(selectionForAttachment);
1959
2017
  const includeCellOutputKeyAfterLimit = Boolean(cellOutputForAttachment);
1960
2018
  const messageSelectionPreview = includeSelectionKeyAfterLimit
1961
- ? toFallbackSelectionPreview(activeWidget, notebookMode, selectionForAttachment)
2019
+ ? toMessageSelectionPreview(selectedContext, activeWidget, notebookMode, selectionForAttachment)
1962
2020
  : undefined;
1963
2021
  const messageCellOutputPreview = includeCellOutputKeyAfterLimit
1964
2022
  ? toCellOutputPreview(selectedContext, activeWidget, notebookMode, cellOutputForAttachment)
1965
2023
  : undefined;
1966
- const shouldDeduplicateSelection = includeActiveCellForNextSend && includeSelectionKeyAfterLimit;
1967
- const shouldDeduplicateCellOutput = includeActiveCellForNextSend && includeCellOutputKeyAfterLimit;
2024
+ const shouldDeduplicateSelection = includeActiveCell && includeSelectionKeyAfterLimit;
2025
+ const shouldDeduplicateCellOutput = includeActiveCell && includeCellOutputKeyAfterLimit;
1968
2026
  const activeCellAttachmentDedupKey = makeActiveCellAttachmentDedupKey(sessionKey, session.threadId);
1969
2027
  const previousActiveCellSignatures = lastActiveCellAttachmentSignatureRef.current.get(activeCellAttachmentDedupKey);
1970
2028
  const activeCellSelectionSignature = shouldDeduplicateSelection
@@ -1987,6 +2045,12 @@ function CodexChat(props) {
1987
2045
  isDuplicateActiveCellAttachmentSignature(previousActiveCellSignatures?.cellOutputSignature, activeCellOutputSignature);
1988
2046
  const includeSelectionKeyForSend = includeSelectionKeyAfterLimit && !hasDuplicateSelectionAttachment;
1989
2047
  const includeCellOutputKeyForSend = includeCellOutputKeyAfterLimit && !hasDuplicateCellOutputAttachment;
2048
+ const sentAttachmentTruncation = resolveSentAttachmentTruncation({
2049
+ includeSelection: includeSelectionKeyForSend,
2050
+ includeCellOutput: includeCellOutputKeyForSend,
2051
+ selectionTruncated: attachmentLimit.selectionTruncated,
2052
+ cellOutputTruncated: attachmentLimit.cellOutputTruncated
2053
+ });
1990
2054
  const messageSelectionPreviewForSend = hasDuplicateSelectionAttachment ? undefined : messageSelectionPreview;
1991
2055
  const messageCellOutputPreviewForSend = hasDuplicateCellOutputAttachment ? undefined : messageCellOutputPreview;
1992
2056
  const messageContextPreview = messageSelectionPreviewForSend || messageCellOutputPreviewForSend
@@ -2030,8 +2094,8 @@ function CodexChat(props) {
2030
2094
  sandbox: sandboxForSend,
2031
2095
  ...(includeSelectionKeyForSend ? { selection: selectionForAttachment } : {}),
2032
2096
  ...(includeCellOutputKeyForSend ? { cellOutput: cellOutputForAttachment } : {}),
2033
- ...(attachmentLimit.selectionTruncated ? { selectionTruncated: true } : {}),
2034
- ...(attachmentLimit.cellOutputTruncated ? { cellOutputTruncated: true } : {}),
2097
+ ...(sentAttachmentTruncation.selectionTruncated ? { selectionTruncated: true } : {}),
2098
+ ...(sentAttachmentTruncation.cellOutputTruncated ? { cellOutputTruncated: true } : {}),
2035
2099
  ...(images ? { images } : {}),
2036
2100
  ...(messageSelectionPreviewForSend ? { uiSelectionPreview: messageSelectionPreviewForSend } : {}),
2037
2101
  ...(messageCellOutputPreviewForSend ? { uiCellOutputPreview: messageCellOutputPreviewForSend } : {})
@@ -2057,7 +2121,7 @@ function CodexChat(props) {
2057
2121
  appendStoredSelectionPreviewEntry(session.threadId, content, messageContextPreview);
2058
2122
  const imageCount = images ? images.length : 0;
2059
2123
  const showReadOnlyWarning = sandboxForSend === 'read-only';
2060
- const attachmentTruncationNotice = buildAttachmentTruncationNotice(attachmentLimit.selectionTruncated, attachmentLimit.cellOutputTruncated, MAX_ACTIVE_CELL_ATTACHMENT_TOTAL_CHARS);
2124
+ const attachmentTruncationNotice = buildAttachmentTruncationNotice(sentAttachmentTruncation.selectionTruncated, sentAttachmentTruncation.cellOutputTruncated, MAX_ACTIVE_CELL_SELECTION_CHARS, MAX_ACTIVE_CELL_OUTPUT_CHARS);
2061
2125
  if (notebookMode === 'plain_py' || notebookMode === 'jupytext_py') {
2062
2126
  plainPyRunSessionKeyRef.current = sessionKey;
2063
2127
  setIsPlainPyRunInProgress(true);
@@ -2156,13 +2220,36 @@ function CodexChat(props) {
2156
2220
  const displayPath = currentNotebookPath
2157
2221
  ? currentNotebookPath.split('/').pop() || 'Untitled'
2158
2222
  : 'No notebook';
2159
- const includeActiveCellForNextSend = includeActiveCell && !excludeCellAttachmentForNextSend;
2160
2223
  const composerNotebookMode = currentSession?.notebookMode ?? inferNotebookModeFromPath(currentNotebookPath);
2161
- const includeCellOutputForNextSend = includeActiveCellForNextSend && includeActiveCellOutput && composerNotebookMode === 'ipynb';
2162
- const showCellAttachmentBadge = includeActiveCellForNextSend &&
2163
- composerNotebookMode !== 'plain_py' &&
2164
- currentNotebookPath.length > 0 &&
2165
- currentSession?.pairedOk !== false;
2224
+ const cellAttachmentState = resolveCellAttachmentState({
2225
+ includeActiveCell,
2226
+ includeActiveCellOutput,
2227
+ notebookMode: composerNotebookMode,
2228
+ isNotebookEditor: currentDocumentIsNotebookEditor,
2229
+ currentNotebookPath,
2230
+ pairedOk: currentSession?.pairedOk
2231
+ });
2232
+ const includeCellOutputForNextSend = cellAttachmentState.outputEnabled;
2233
+ const showCellAttachmentBadge = cellAttachmentState.showBadge;
2234
+ const cellAttachmentContentEnabled = cellAttachmentState.contentEnabled;
2235
+ const cellAttachmentOutputEnabled = cellAttachmentState.outputEnabled;
2236
+ useEffect(() => {
2237
+ if (showCellAttachmentBadge) {
2238
+ return;
2239
+ }
2240
+ setCellAttachmentPopoverOpen(false);
2241
+ clearCellAttachmentPopoverCloseTimer();
2242
+ }, [showCellAttachmentBadge]);
2243
+ useEffect(() => {
2244
+ return () => {
2245
+ clearCellAttachmentPopoverCloseTimer();
2246
+ };
2247
+ }, []);
2248
+ useEffect(() => {
2249
+ return () => {
2250
+ clearContextPopoverCloseTimer();
2251
+ };
2252
+ }, []);
2166
2253
  const trimmedInput = input.trim();
2167
2254
  const canSend = status !== 'disconnected' &&
2168
2255
  currentNotebookPath.length > 0 &&
@@ -2244,6 +2331,13 @@ function CodexChat(props) {
2244
2331
  ? `${Math.round(clampNumber(contextUsedPercent, 0, 100))}%`
2245
2332
  : 'Unknown';
2246
2333
  const hasContextUsageSnapshot = rateLimits?.contextWindow != null;
2334
+ useEffect(() => {
2335
+ if (hasContextUsageSnapshot) {
2336
+ return;
2337
+ }
2338
+ setContextPopoverOpen(false);
2339
+ clearContextPopoverCloseTimer();
2340
+ }, [hasContextUsageSnapshot]);
2247
2341
  useLayoutEffect(() => {
2248
2342
  const target = notebookLabelRef.current;
2249
2343
  if (!target) {
@@ -2286,6 +2380,7 @@ function CodexChat(props) {
2286
2380
  setReasoningMenuOpen(false);
2287
2381
  setUsagePopoverOpen(false);
2288
2382
  setPermissionMenuOpen(false);
2383
+ setContextPopoverOpen(false);
2289
2384
  }, className: `jp-CodexHeaderBtn jp-CodexHeaderBtn-icon${settingsOpen ? ' is-active' : ''}`, "aria-label": "Settings", "aria-expanded": settingsOpen, title: "Settings", children: _jsx(GearIcon, { width: 16, height: 16 }) })] })] }), currentSession?.pairedOk === false && (_jsxs("div", { className: "jp-CodexPairingNotice", role: "status", "aria-live": "polite", children: [_jsx("div", { className: "jp-CodexPairingNotice-title", children: "Jupytext pairing required" }), _jsx("div", { className: "jp-CodexPairingNotice-body", children: currentSession.pairedMessage ||
2290
2385
  'This notebook must be paired (.ipynb ↔ .py) via Jupytext to enable running.' })] }))] }), _jsx("div", { className: "jp-CodexChat-body", children: _jsxs("div", { className: "jp-CodexChat-messages", ref: scrollRef, onScroll: onScrollMessages, children: [status === 'disconnected' && !isReconnecting && (_jsxs("div", { className: "jp-CodexChat-message jp-CodexChat-system jp-CodexChat-reconnectNotice", children: [_jsx("div", { className: "jp-CodexChat-role", children: "system" }), _jsx("div", { className: "jp-CodexChat-text", children: "Codex connection was lost. Reconnect to continue." }), _jsx("button", { type: "button", className: "jp-CodexReconnectBtn", onClick: () => reconnectSocket(), disabled: isReconnecting, "aria-label": isReconnecting ? 'Codex reconnecting' : 'Reconnect to Codex', title: isReconnecting ? 'Attempting to reconnect...' : 'Reconnect to Codex', children: isReconnecting ? 'Connecting...' : 'Reconnect' })] })), messages.length === 0 && (_jsxs("div", { className: "jp-CodexChat-message jp-CodexChat-system", children: [_jsx("div", { className: "jp-CodexChat-role", children: "system" }), _jsx("div", { className: "jp-CodexChat-text", children: "Select a notebook, then start a conversation." })] })), messages.map(entry => {
2291
2386
  if (entry.kind === 'text') {
@@ -2322,9 +2417,7 @@ function CodexChat(props) {
2322
2417
  return (_jsxs("details", { className: activityClassName, role: "status", "aria-live": "polite", children: [_jsx("summary", { className: "jp-CodexActivitySummary", children: summaryContent }), _jsx("div", { className: "jp-CodexActivityBody", children: _jsx("pre", { className: "jp-CodexActivityCode", children: _jsx("code", { children: trimmedDetail }) }) })] }, entry.id));
2323
2418
  }
2324
2419
  return (_jsx("div", { className: activityClassName, role: "status", "aria-live": "polite", children: _jsx("div", { className: "jp-CodexActivitySummary jp-CodexActivitySummaryStatic", children: summaryContent }) }, entry.id));
2325
- }), status === 'running' && (_jsx("div", { className: `jp-CodexChat-loading${progressKind === 'reasoning' ? ' is-reasoning' : ''}`, "aria-label": progressKind === 'reasoning' ? 'Reasoning' : 'Running', children: _jsxs("div", { className: "jp-CodexChat-loading-dots", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }) })), _jsx("div", { ref: endRef })] }) }), _jsx(PortalMenu, { open: Boolean(selectionPopover), anchorRef: selectionPopoverAnchorRef, popoverRef: selectionPopoverRef, className: "jp-CodexChat-selectionPopover", ariaLabel: "Message context", constrainHeightToViewport: true, viewportMargin: 20, role: "dialog", align: "right", children: selectionPopover && (_jsxs("div", { className: "jp-CodexChat-selectionCard", role: "note", "aria-label": "Message context", children: [selectionPopover.preview.selectionPreview && (_jsxs("div", { className: "jp-CodexChat-contextSection", children: [_jsx("div", { className: "jp-CodexChat-selectionMeta", children: selectionPopover.preview.selectionPreview.locationLabel }), _jsx(SelectionPreviewCode, { code: selectionPopover.preview.selectionPreview.previewText })] })), selectionPopover.preview.cellOutputPreview && (_jsxs("div", { className: "jp-CodexChat-contextSection", children: [_jsx("div", { className: "jp-CodexChat-selectionMeta", children: selectionPopover.preview.cellOutputPreview.locationLabel }), _jsx(SelectionPreviewCode, { code: selectionPopover.preview.cellOutputPreview.previewText })] }))] })) }), _jsxs("div", { className: "jp-CodexChat-input", children: [_jsx("div", { className: `jp-CodexJumpBar${isAtBottom ? '' : ' is-visible'}`, children: _jsx("button", { type: "button", className: "jp-CodexJumpToLatestBtn", onClick: scrollToBottom, "aria-label": "Jump to latest", "aria-hidden": isAtBottom, tabIndex: isAtBottom ? -1 : 0, title: "Jump to latest", children: _jsx(ArrowDownIcon, { width: 20, height: 20 }) }) }), _jsxs("div", { className: "jp-CodexComposer", children: [_jsx("div", { className: `jp-CodexComposer-cellAttachmentWrap${showCellAttachmentBadge ? ' is-visible' : ''}`, "aria-hidden": !showCellAttachmentBadge, children: _jsxs("div", { className: "jp-CodexComposer-cellAttachment", role: "group", "aria-label": "Pending active-cell attachment", title: includeCellOutputForNextSend
2326
- ? 'Active cell and output will be attached on next send.'
2327
- : 'Active cell will be attached on next send.', children: [_jsx("span", { className: "jp-CodexComposer-cellAttachmentLabel", children: "Cell Attached" }), _jsx("button", { type: "button", className: "jp-CodexComposer-cellAttachmentRemove", onClick: () => setExcludeCellAttachmentForNextSend(true), "aria-label": "Do not attach cell on next send", title: "Do not attach cell on next send", disabled: !showCellAttachmentBadge, tabIndex: showCellAttachmentBadge ? 0 : -1, children: _jsx(XIcon, { width: 10, height: 10 }) })] }) }), _jsx("textarea", { ref: composerTextareaRef, value: input, onChange: e => {
2420
+ }), status === 'running' && (_jsx("div", { className: `jp-CodexChat-loading${progressKind === 'reasoning' ? ' is-reasoning' : ''}`, "aria-label": progressKind === 'reasoning' ? 'Reasoning' : 'Running', children: _jsxs("div", { className: "jp-CodexChat-loading-dots", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }) })), _jsx("div", { ref: endRef })] }) }), _jsx(PortalMenu, { open: Boolean(selectionPopover), anchorRef: selectionPopoverAnchorRef, popoverRef: selectionPopoverRef, className: "jp-CodexChat-selectionPopover", ariaLabel: "Message context", constrainHeightToViewport: true, viewportMargin: 20, role: "dialog", align: "right", children: selectionPopover && (_jsxs("div", { className: "jp-CodexChat-selectionCard", role: "note", "aria-label": "Message context", children: [selectionPopover.preview.selectionPreview && (_jsxs("div", { className: "jp-CodexChat-contextSection", children: [_jsx("div", { className: "jp-CodexChat-selectionMeta", children: selectionPopover.preview.selectionPreview.locationLabel }), _jsx(SelectionPreviewCode, { code: selectionPopover.preview.selectionPreview.previewText })] })), selectionPopover.preview.cellOutputPreview && (_jsxs("div", { className: "jp-CodexChat-contextSection", children: [_jsx("div", { className: "jp-CodexChat-selectionMeta", children: selectionPopover.preview.cellOutputPreview.locationLabel }), _jsx(SelectionPreviewCode, { code: selectionPopover.preview.cellOutputPreview.previewText })] }))] })) }), _jsx(PortalMenu, { open: cellAttachmentPopoverOpen && showCellAttachmentBadge, anchorRef: cellAttachmentAnchorRef, popoverRef: cellAttachmentPopoverRef, className: "jp-CodexCellAttachmentPopoverMenu", ariaLabel: "Cell attachment details", role: "dialog", align: "left", onMouseEnter: openCellAttachmentPopover, onMouseLeave: scheduleCloseCellAttachmentPopover, children: _jsxs("div", { className: "jp-CodexCellAttachmentPopoverCard", role: "note", "aria-label": "Cell attachment details", children: [_jsx("div", { className: "jp-CodexCellAttachmentPopoverTitle", children: "Attach On Next Send" }), _jsxs("div", { className: "jp-CodexCellAttachmentPopoverRow", children: [_jsx("span", { children: "Current cell content" }), _jsx("span", { className: `jp-CodexCellAttachmentDot ${cellAttachmentContentEnabled ? 'is-on' : 'is-off'}`, "aria-label": cellAttachmentContentEnabled ? 'Attached' : 'Not attached', title: cellAttachmentContentEnabled ? 'Attached' : 'Not attached' })] }), _jsxs("div", { className: "jp-CodexCellAttachmentPopoverRow", children: [_jsx("span", { children: "Current cell output" }), _jsx("span", { className: `jp-CodexCellAttachmentDot ${cellAttachmentOutputEnabled ? 'is-on' : 'is-off'}`, "aria-label": cellAttachmentOutputEnabled ? 'Attached' : 'Not attached', title: cellAttachmentOutputEnabled ? 'Attached' : 'Not attached' })] })] }) }), _jsxs("div", { className: "jp-CodexChat-input", children: [_jsx("div", { className: `jp-CodexJumpBar${isAtBottom ? '' : ' is-visible'}`, children: _jsx("button", { type: "button", className: "jp-CodexJumpToLatestBtn", onClick: scrollToBottom, "aria-label": "Jump to latest", "aria-hidden": isAtBottom, tabIndex: isAtBottom ? -1 : 0, title: "Jump to latest", children: _jsx(ArrowDownIcon, { width: 20, height: 20 }) }) }), _jsxs("div", { className: "jp-CodexComposer", children: [_jsx("div", { className: `jp-CodexCellAttachmentWrap jp-CodexComposer-cellAttachmentWrap${showCellAttachmentBadge ? ' is-visible' : ''}`, ref: cellAttachmentAnchorRef, "aria-hidden": !showCellAttachmentBadge, onMouseEnter: openCellAttachmentPopover, onMouseLeave: scheduleCloseCellAttachmentPopover, onFocusCapture: openCellAttachmentPopover, onBlurCapture: handleCellAttachmentBlur, children: _jsx("div", { role: "group", "aria-label": "Active-cell attachment", children: _jsxs("button", { type: "button", className: `jp-CodexComposer-cellAttachment${cellAttachmentContentEnabled ? '' : ' is-off'}`, onClick: () => setIncludeActiveCell(value => !value), "aria-pressed": cellAttachmentContentEnabled, "aria-label": cellAttachmentContentEnabled ? 'Disable active-cell attachment' : 'Enable active-cell attachment', title: cellAttachmentContentEnabled ? 'Disable active-cell attachment' : 'Enable active-cell attachment', disabled: !showCellAttachmentBadge, tabIndex: showCellAttachmentBadge ? 0 : -1, children: [_jsx(CellAttachmentIcon, { active: cellAttachmentContentEnabled, width: 15, height: 15 }), _jsx("span", { className: "jp-CodexComposer-cellAttachmentLabel", children: "Cell Attatch" })] }) }) }), _jsx("textarea", { ref: composerTextareaRef, value: input, onChange: e => {
2328
2421
  updateInput(e.currentTarget.value);
2329
2422
  // Resize using the current target so typing feels immediate.
2330
2423
  window.requestAnimationFrame(() => autosizeComposerTextarea(e.currentTarget));
@@ -2354,6 +2447,7 @@ function CodexChat(props) {
2354
2447
  setReasoningMenuOpen(false);
2355
2448
  setUsagePopoverOpen(false);
2356
2449
  setPermissionMenuOpen(false);
2450
+ setContextPopoverOpen(false);
2357
2451
  }, disabled: status === 'running', "aria-label": `Model: ${selectedModelLabel}`, "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, title: `Model: ${selectedModelLabel}`, children: _jsx("span", { className: "jp-CodexModelBtn-label", children: selectedModelLabel }) }) }), _jsxs(PortalMenu, { open: modelMenuOpen, anchorRef: modelBtnRef, popoverRef: modelPopoverRef, role: "menu", ariaLabel: "Model", align: "left", children: [modelOptions.length === 0 && (_jsx("div", { className: "jp-CodexMenuItem", children: "No models available" })), modelOptions.map(option => {
2358
2452
  const inferred = activeModelOption === '__config__' && autoModel && modelOptions.some(option => option.value === autoModel)
2359
2453
  ? autoModel
@@ -2368,6 +2462,7 @@ function CodexChat(props) {
2368
2462
  setModelMenuOpen(false);
2369
2463
  setUsagePopoverOpen(false);
2370
2464
  setPermissionMenuOpen(false);
2465
+ setContextPopoverOpen(false);
2371
2466
  }, disabled: status === 'running', "aria-label": `Reasoning: ${selectedReasoningLabel}`, "aria-haspopup": "menu", "aria-expanded": reasoningMenuOpen, title: `Reasoning: ${selectedReasoningLabel}`, children: _jsx(ReasoningEffortIcon, { isConfig: activeReasoningEffort === '__config__' && !autoReasoningEffort, activeBars: getReasoningEffortBars((activeReasoningEffort === '__config__' && autoReasoningEffort
2372
2467
  ? autoReasoningEffort
2373
2468
  : activeReasoningEffort), reasoningOptions), width: 17, height: 17 }) }) }), _jsxs(PortalMenu, { open: reasoningMenuOpen, anchorRef: reasoningBtnRef, popoverRef: reasoningPopoverRef, role: "menu", ariaLabel: "Reasoning", align: "left", children: [reasoningOptions.length === 0 && (_jsx("div", { className: "jp-CodexMenuItem", children: "No reasoning options" })), reasoningOptions.map(option => {
@@ -2382,14 +2477,15 @@ function CodexChat(props) {
2382
2477
  setModelMenuOpen(false);
2383
2478
  setReasoningMenuOpen(false);
2384
2479
  setUsagePopoverOpen(false);
2480
+ setContextPopoverOpen(false);
2385
2481
  }, disabled: status === 'running', "aria-label": `Permission: ${selectedSandboxLabel}`, "aria-haspopup": "menu", "aria-expanded": permissionMenuOpen, title: `Permission: ${selectedSandboxLabel}`, children: _jsx(ShieldIcon, { width: 17, height: 17 }) }) }), _jsx(PortalMenu, { open: permissionMenuOpen, anchorRef: permissionBtnRef, popoverRef: permissionPopoverRef, role: "menu", ariaLabel: "Permissions", align: "right", children: SANDBOX_OPTIONS.map(option => (_jsxs("button", { type: "button", className: `jp-CodexMenuItem ${activeSandboxMode === option.value ? 'is-active' : ''}`, onClick: () => {
2386
2482
  setCurrentSessionSandboxMode(option.value);
2387
2483
  setPermissionMenuOpen(false);
2388
- }, children: [_jsx("span", { className: "jp-CodexMenuItemLabel", children: option.label }), activeSandboxMode === option.value && _jsx(CheckIcon, { className: "jp-CodexMenuCheck", width: 16, height: 16 })] }, option.value))) }), hasContextUsageSnapshot && (_jsxs("div", { className: "jp-CodexContextWrap", children: [_jsx("button", { type: "button", className: `jp-CodexIconBtn jp-CodexContextBtn${usageIsStale ? ' is-stale' : ''}`, "aria-label": contextUsedTokens == null || contextLeftTokens == null
2484
+ }, children: [_jsx("span", { className: "jp-CodexMenuItemLabel", children: option.label }), activeSandboxMode === option.value && _jsx(CheckIcon, { className: "jp-CodexMenuCheck", width: 16, height: 16 })] }, option.value))) }), hasContextUsageSnapshot && (_jsxs("div", { className: "jp-CodexContextWrap", ref: contextMenuWrapRef, onMouseEnter: openContextPopover, onMouseLeave: scheduleCloseContextPopover, onFocusCapture: openContextPopover, onBlurCapture: handleContextPopoverBlur, children: [_jsx("button", { type: "button", className: `jp-CodexIconBtn jp-CodexContextBtn${usageIsStale ? ' is-stale' : ''}`, ref: contextBtnRef, "aria-label": contextUsedTokens == null || contextLeftTokens == null
2389
2485
  ? 'Context window usage unavailable'
2390
2486
  : `Context window: used ${contextUsedLabel} tokens, left ${contextLeftLabel} tokens`, title: contextUsedTokens == null || contextLeftTokens == null
2391
2487
  ? 'Context window usage unavailable'
2392
- : `Used ${contextUsedLabel} / left ${contextLeftLabel}`, children: _jsx(ContextWindowIcon, { level: contextLevel, width: 20, height: 20 }) }), _jsxs("div", { className: "jp-CodexContextPopover", role: "tooltip", children: [_jsx("div", { className: "jp-CodexContextPopoverTitle", children: "Context window" }), _jsxs("div", { className: "jp-CodexContextPopoverRow", children: [_jsx("span", { children: "Used" }), _jsx("strong", { children: contextUsedLabel })] }), _jsxs("div", { className: "jp-CodexContextPopoverRow", children: [_jsx("span", { children: "Left" }), _jsx("strong", { children: contextLeftLabel })] }), _jsx("div", { className: "jp-CodexContextPopoverMeta", children: contextWindowTokens == null
2488
+ : `Used ${contextUsedLabel} / left ${contextLeftLabel}`, children: _jsx(ContextWindowIcon, { level: contextLevel, width: 20, height: 20 }) }), _jsxs(PortalMenu, { open: contextPopoverOpen, anchorRef: contextBtnRef, popoverRef: contextPopoverRef, className: "jp-CodexContextPopover", role: "tooltip", ariaLabel: "Context window", align: "right", onMouseEnter: openContextPopover, onMouseLeave: scheduleCloseContextPopover, children: [_jsx("div", { className: "jp-CodexContextPopoverTitle", children: "Context window" }), _jsxs("div", { className: "jp-CodexContextPopoverRow", children: [_jsx("span", { children: "Used" }), _jsx("strong", { children: contextUsedLabel })] }), _jsxs("div", { className: "jp-CodexContextPopoverRow", children: [_jsx("span", { children: "Left" }), _jsx("strong", { children: contextLeftLabel })] }), _jsx("div", { className: "jp-CodexContextPopoverMeta", children: contextWindowTokens == null
2393
2489
  ? 'Window size unavailable'
2394
2490
  : `Window: ${contextWindowLabel} tokens (${contextUsedPercentLabel} used)` })] })] }))] }), _jsx("div", { className: "jp-CodexComposer-toolbarRight", children: _jsx("button", { type: "button", className: `jp-CodexSendBtn${sendButtonMode === 'stop' ? ' is-stop' : ''}`, onClick: () => {
2395
2491
  if (sendButtonMode === 'stop') {