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,376 @@
1
+ import React, { memo, useEffect, useMemo, useState } from 'react';
2
+ import DOMPurify from 'dompurify';
3
+ import hljs from 'highlight.js/lib/core';
4
+ import bashLang from 'highlight.js/lib/languages/bash';
5
+ import cssLang from 'highlight.js/lib/languages/css';
6
+ import diffLang from 'highlight.js/lib/languages/diff';
7
+ import javascriptLang from 'highlight.js/lib/languages/javascript';
8
+ import jsonLang from 'highlight.js/lib/languages/json';
9
+ import markdownLang from 'highlight.js/lib/languages/markdown';
10
+ import pythonLang from 'highlight.js/lib/languages/python';
11
+ import sqlLang from 'highlight.js/lib/languages/sql';
12
+ import typescriptLang from 'highlight.js/lib/languages/typescript';
13
+ import xmlLang from 'highlight.js/lib/languages/xml';
14
+ import yamlLang from 'highlight.js/lib/languages/yaml';
15
+ import { formatSelectionPreviewTextForDisplay } from './codexChatDocumentUtils';
16
+
17
+ hljs.registerLanguage('bash', bashLang);
18
+ hljs.registerLanguage('css', cssLang);
19
+ hljs.registerLanguage('diff', diffLang);
20
+ hljs.registerLanguage('javascript', javascriptLang);
21
+ hljs.registerLanguage('json', jsonLang);
22
+ hljs.registerLanguage('markdown', markdownLang);
23
+ hljs.registerLanguage('python', pythonLang);
24
+ hljs.registerLanguage('sql', sqlLang);
25
+ hljs.registerLanguage('typescript', typescriptLang);
26
+ hljs.registerLanguage('xml', xmlLang);
27
+ hljs.registerLanguage('yaml', yamlLang);
28
+
29
+ type MarkdownParser = {
30
+ parse: (markdown: string) => string;
31
+ };
32
+
33
+ let markdownParser: MarkdownParser | null = null;
34
+ let markdownParserPromise: Promise<MarkdownParser> | null = null;
35
+
36
+ async function ensureMarkdownParser(): Promise<MarkdownParser> {
37
+ if (markdownParser) {
38
+ return markdownParser;
39
+ }
40
+ if (!markdownParserPromise) {
41
+ markdownParserPromise = Promise.all([import('marked'), import('marked-katex-extension')])
42
+ .then(([markedModule, katexModule]) => {
43
+ const marked = markedModule.marked;
44
+ const markedKatex = katexModule.default;
45
+ marked.use(
46
+ markedKatex({
47
+ throwOnError: false
48
+ })
49
+ );
50
+ const parser: MarkdownParser = {
51
+ parse(markdown: string): string {
52
+ return marked.parse(normalizeMathDelimiters(markdown), {
53
+ gfm: true,
54
+ breaks: true
55
+ }) as string;
56
+ }
57
+ };
58
+ markdownParser = parser;
59
+ return parser;
60
+ })
61
+ .catch(error => {
62
+ markdownParserPromise = null;
63
+ throw error;
64
+ });
65
+ }
66
+ return markdownParserPromise;
67
+ }
68
+
69
+ type MessageBlock =
70
+ | { kind: 'text'; text: string }
71
+ | { kind: 'code'; lang: string; code: string };
72
+
73
+ async function copyToClipboard(text: string): Promise<boolean> {
74
+ if (!text) {
75
+ return false;
76
+ }
77
+ try {
78
+ await navigator.clipboard.writeText(text);
79
+ return true;
80
+ } catch {
81
+ try {
82
+ const textarea = document.createElement('textarea');
83
+ textarea.value = text;
84
+ textarea.setAttribute('readonly', 'true');
85
+ textarea.style.position = 'fixed';
86
+ textarea.style.left = '-9999px';
87
+ document.body.appendChild(textarea);
88
+ textarea.select();
89
+ const ok = document.execCommand('copy');
90
+ document.body.removeChild(textarea);
91
+ return ok;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ }
97
+
98
+ export async function blobToDataUrl(blob: Blob): Promise<string> {
99
+ return await new Promise((resolve, reject) => {
100
+ const reader = new FileReader();
101
+ reader.onload = () => resolve(String(reader.result || ''));
102
+ reader.onerror = () => reject(reader.error || new Error('Failed to read blob'));
103
+ reader.readAsDataURL(blob);
104
+ });
105
+ }
106
+
107
+ export function splitFencedCodeBlocks(text: string): MessageBlock[] {
108
+ const blocks: MessageBlock[] = [];
109
+ let rest = text;
110
+
111
+ while (rest.length > 0) {
112
+ const fenceStart = rest.indexOf('```');
113
+ if (fenceStart === -1) {
114
+ blocks.push({ kind: 'text', text: rest });
115
+ break;
116
+ }
117
+
118
+ if (fenceStart > 0) {
119
+ blocks.push({ kind: 'text', text: rest.slice(0, fenceStart) });
120
+ }
121
+
122
+ const afterFence = rest.slice(fenceStart + 3);
123
+ const fenceEnd = afterFence.indexOf('```');
124
+ if (fenceEnd === -1) {
125
+ blocks.push({ kind: 'text', text: rest.slice(fenceStart) });
126
+ break;
127
+ }
128
+
129
+ const raw = afterFence.slice(0, fenceEnd);
130
+ rest = afterFence.slice(fenceEnd + 3);
131
+
132
+ let lang = '';
133
+ let code = raw;
134
+ const firstNewline = raw.indexOf('\n');
135
+ if (firstNewline !== -1) {
136
+ const candidate = raw.slice(0, firstNewline).trim();
137
+ if (candidate.length > 0 && candidate.length <= 20 && /^[A-Za-z0-9+#_.-]+$/.test(candidate)) {
138
+ lang = candidate;
139
+ code = raw.slice(firstNewline + 1);
140
+ }
141
+ }
142
+ blocks.push({ kind: 'code', lang, code: code.replace(/\n$/, '') });
143
+ }
144
+
145
+ return blocks.filter(block => (block.kind === 'text' ? block.text.length > 0 : true));
146
+ }
147
+
148
+ function escapeHtml(value: string): string {
149
+ return value
150
+ .replace(/&/g, '&amp;')
151
+ .replace(/</g, '&lt;')
152
+ .replace(/>/g, '&gt;')
153
+ .replace(/\"/g, '&quot;')
154
+ .replace(/'/g, '&#39;');
155
+ }
156
+
157
+ function normalizeHighlightLanguage(lang: string): string {
158
+ const normalized = lang.trim().toLowerCase();
159
+ if (!normalized) {
160
+ return '';
161
+ }
162
+ const aliasMap: Record<string, string> = {
163
+ js: 'javascript',
164
+ jsx: 'javascript',
165
+ mjs: 'javascript',
166
+ cjs: 'javascript',
167
+ ts: 'typescript',
168
+ tsx: 'typescript',
169
+ py: 'python',
170
+ sh: 'bash',
171
+ shell: 'bash',
172
+ zsh: 'bash',
173
+ yml: 'yaml',
174
+ 'c++': 'cpp',
175
+ 'c#': 'csharp',
176
+ 'objective-c': 'objectivec',
177
+ objc: 'objectivec'
178
+ };
179
+ return aliasMap[normalized] ?? normalized;
180
+ }
181
+
182
+ function inferHighlightLanguage(code: string): string {
183
+ const source = code.trim();
184
+ if (!source) {
185
+ return '';
186
+ }
187
+ const pythonSignals = [
188
+ /^\s*from\s+[A-Za-z0-9_.]+\s+import\s+/m,
189
+ /^\s*import\s+[A-Za-z0-9_.]+/m,
190
+ /^\s*def\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/m,
191
+ /^\s*class\s+[A-Za-z_][A-Za-z0-9_]*\s*(\(|:)/m,
192
+ /\bNone\b|\bTrue\b|\bFalse\b/,
193
+ /:\s*$/m
194
+ ];
195
+ if (pythonSignals.some(pattern => pattern.test(source))) {
196
+ return 'python';
197
+ }
198
+ return '';
199
+ }
200
+
201
+ export function renderHighlightedCodeToSafeHtml(code: string, lang: string): string {
202
+ const fallback = escapeHtml(code);
203
+ let highlighted = fallback;
204
+ try {
205
+ const normalizedLang = normalizeHighlightLanguage(lang);
206
+ if (normalizedLang && hljs.getLanguage(normalizedLang)) {
207
+ highlighted = hljs.highlight(code, { language: normalizedLang, ignoreIllegals: true }).value;
208
+ } else {
209
+ const inferredLang = inferHighlightLanguage(code);
210
+ if (inferredLang && hljs.getLanguage(inferredLang)) {
211
+ highlighted = hljs.highlight(code, { language: inferredLang, ignoreIllegals: true }).value;
212
+ } else {
213
+ highlighted = hljs.highlightAuto(code).value;
214
+ }
215
+ }
216
+ } catch {
217
+ highlighted = fallback;
218
+ }
219
+ try {
220
+ return DOMPurify.sanitize(highlighted, { USE_PROFILES: { html: true } });
221
+ } catch {
222
+ return escapeHtml(highlighted);
223
+ }
224
+ }
225
+
226
+ export function renderMarkdownToSafeHtml(markdown: string): string {
227
+ if (!markdown) {
228
+ return '';
229
+ }
230
+ const parser = markdownParser;
231
+ if (!parser) {
232
+ return escapeHtml(markdown).replace(/\n/g, '<br />');
233
+ }
234
+ let html = '';
235
+ try {
236
+ html = parser.parse(markdown);
237
+ } catch {
238
+ return escapeHtml(markdown).replace(/\n/g, '<br />');
239
+ }
240
+ try {
241
+ return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
242
+ } catch {
243
+ return escapeHtml(markdown).replace(/\n/g, '<br />');
244
+ }
245
+ }
246
+
247
+ function normalizeMathDelimiters(markdown: string): string {
248
+ const normalizedEscapedBlock = markdown.replace(
249
+ /(^|[^\\])\\\[([\s\S]*?)\\\]/g,
250
+ (_match, prefix: string, body: string) => `${prefix}\n$$\n${body}\n$$\n`
251
+ );
252
+
253
+ const normalizedEscapedInline = normalizedEscapedBlock.replace(
254
+ /(^|[^\\])\\\((.+?)\\\)/g,
255
+ (_match, prefix: string, body: string) => `${prefix}$${body}$`
256
+ );
257
+
258
+ return normalizedEscapedInline.replace(
259
+ /(^|\n)[ \t]*\[\s*\n([\s\S]*?)\n[ \t]*\](?=\n|$)/g,
260
+ (_match, prefix: string, body: string) => {
261
+ const trimmed = body.trim();
262
+ if (!looksLikeLatexMath(trimmed)) {
263
+ return `${prefix}[\n${body}\n]`;
264
+ }
265
+ return `${prefix}\n$$\n${trimmed}\n$$\n`;
266
+ }
267
+ );
268
+ }
269
+
270
+ function looksLikeLatexMath(value: string): boolean {
271
+ if (!value) {
272
+ return false;
273
+ }
274
+ return /\\[A-Za-z]+/.test(value) || /[_^{}]/.test(value);
275
+ }
276
+
277
+ export function StatusPill(props: { status: 'disconnected' | 'ready' | 'running' }): JSX.Element {
278
+ const label =
279
+ props.status === 'disconnected' ? 'Disconnected' : props.status === 'running' ? 'Running' : 'Ready';
280
+ return (
281
+ <span
282
+ className={`jp-CodexStatusPill jp-CodexStatusPill-${props.status}`}
283
+ role="img"
284
+ aria-label={label}
285
+ title={label}
286
+ >
287
+ <span className="jp-CodexStatusPill-dot" aria-hidden="true" />
288
+ </span>
289
+ );
290
+ }
291
+
292
+ const CodeBlock = memo(function CodeBlock(props: { lang: string; code: string; canCopy: boolean }): JSX.Element {
293
+ const [copied, setCopied] = useState(false);
294
+ const highlightedCode = useMemo(
295
+ () => renderHighlightedCodeToSafeHtml(props.code, props.lang),
296
+ [props.code, props.lang]
297
+ );
298
+
299
+ async function onCopy(): Promise<void> {
300
+ const ok = await copyToClipboard(props.code);
301
+ if (!ok) {
302
+ return;
303
+ }
304
+ setCopied(true);
305
+ window.setTimeout(() => setCopied(false), 900);
306
+ }
307
+
308
+ return (
309
+ <div className="jp-CodexCodeBlockWrap">
310
+ <div className="jp-CodexCodeBlockBar">
311
+ <span className="jp-CodexCodeBlockMeta">
312
+ {props.lang ? <span className="jp-CodexCodeBlockLang">{props.lang}</span> : <span />}
313
+ </span>
314
+ {props.canCopy && (
315
+ <button
316
+ className="jp-CodexBtn jp-CodexBtn-ghost jp-CodexBtn-xs jp-CodexCodeBlockCopyBtn"
317
+ onClick={() => void onCopy()}
318
+ >
319
+ {copied ? 'Copied' : 'Copy'}
320
+ </button>
321
+ )}
322
+ </div>
323
+ <pre className="jp-CodexCodeBlock">
324
+ <code className="hljs" dangerouslySetInnerHTML={{ __html: highlightedCode }} />
325
+ </pre>
326
+ </div>
327
+ );
328
+ });
329
+
330
+ CodeBlock.displayName = 'CodeBlock';
331
+
332
+ export const MessageText = memo(function MessageText(props: { text: string; canCopyCode?: boolean }): JSX.Element {
333
+ const [, setMarkdownParserVersion] = useState(0);
334
+
335
+ useEffect(() => {
336
+ let disposed = false;
337
+ void ensureMarkdownParser()
338
+ .then(() => {
339
+ if (!disposed) {
340
+ setMarkdownParserVersion(version => version + 1);
341
+ }
342
+ })
343
+ .catch(() => undefined);
344
+ return () => {
345
+ disposed = true;
346
+ };
347
+ }, []);
348
+
349
+ const blocks = splitFencedCodeBlocks(props.text);
350
+ return (
351
+ <div className="jp-CodexChat-text">
352
+ {blocks.map((block, idx) => {
353
+ if (block.kind === 'code') {
354
+ return <CodeBlock key={idx} lang={block.lang} code={block.code} canCopy={Boolean(props.canCopyCode)} />;
355
+ }
356
+ const html = renderMarkdownToSafeHtml(block.text);
357
+ return <div key={idx} className="jp-CodexMarkdown" dangerouslySetInnerHTML={{ __html: html }} />;
358
+ })}
359
+ </div>
360
+ );
361
+ }, (a, b) => a.text === b.text && Boolean(a.canCopyCode) === Boolean(b.canCopyCode));
362
+
363
+ MessageText.displayName = 'MessageText';
364
+
365
+ export function SelectionPreviewCode(props: { code: string }): JSX.Element {
366
+ const displayCode = formatSelectionPreviewTextForDisplay(props.code);
367
+ const highlightedCode = useMemo(
368
+ () => renderHighlightedCodeToSafeHtml(displayCode, ''),
369
+ [displayCode]
370
+ );
371
+ return (
372
+ <pre className="jp-CodexCodeBlock jp-CodexChat-selectionCode">
373
+ <code className="hljs" dangerouslySetInnerHTML={{ __html: highlightedCode }} />
374
+ </pre>
375
+ );
376
+ }
@@ -0,0 +1,79 @@
1
+ export type TextRole = 'user' | 'assistant' | 'system';
2
+
3
+ export type SessionCreateOptions = {
4
+ threadId?: string;
5
+ reuseStoredThread?: boolean;
6
+ sessionKey?: string;
7
+ };
8
+
9
+ export type SessionFactoryDefaults<TModelOption extends string, TReasoningOption extends string, TSandboxMode extends string> = {
10
+ readStoredThreadId: (path: string, sessionKey: string) => string;
11
+ readDefaultModelOption: () => TModelOption;
12
+ readDefaultReasoningEffortOption: () => TReasoningOption;
13
+ readDefaultSandboxModeOption: () => TSandboxMode;
14
+ normalizeSystemText: (role: TextRole, text: string) => string;
15
+ };
16
+
17
+ export function createSession<TSession, TModelOption extends string, TReasoningOption extends string, TSandboxMode extends string>(
18
+ path: string,
19
+ intro: string,
20
+ options: SessionCreateOptions | undefined,
21
+ defaults: SessionFactoryDefaults<TModelOption, TReasoningOption, TSandboxMode>
22
+ ): TSession {
23
+ const generateId = () =>
24
+ typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
25
+ ? crypto.randomUUID()
26
+ : `${Date.now()}-${Math.floor(Math.random() * 0x100000000).toString(16)}`;
27
+
28
+ const defaultIntro = 'Session started';
29
+ const systemText = defaults.normalizeSystemText('system', intro || defaultIntro);
30
+ const requestedThreadId = (options?.threadId || '').trim();
31
+ const storedThreadId = options?.reuseStoredThread === false ? '' : defaults.readStoredThreadId(path, options?.sessionKey || '');
32
+ const threadId = requestedThreadId || storedThreadId || generateId();
33
+
34
+ return {
35
+ threadId,
36
+ runState: 'ready',
37
+ activeRunId: null,
38
+ runStartedAt: null,
39
+ progress: '',
40
+ progressKind: '',
41
+ messages: [
42
+ {
43
+ kind: 'text',
44
+ id: generateId(),
45
+ role: 'system',
46
+ text: systemText
47
+ }
48
+ ],
49
+ pairedOk: null,
50
+ pairedPath: '',
51
+ pairedOsPath: '',
52
+ pairedMessage: '',
53
+ notebookMode: null,
54
+ selectedModelOption: defaults.readDefaultModelOption(),
55
+ selectedReasoningEffort: defaults.readDefaultReasoningEffortOption(),
56
+ selectedSandboxMode: defaults.readDefaultSandboxModeOption(),
57
+ effectiveSandboxMode: null,
58
+ conversationMode: 'resume'
59
+ } as unknown as TSession;
60
+ }
61
+
62
+ export function createThreadResetSession<TSession, TModelOption extends string, TReasoningOption extends string, TSandboxMode extends string>(
63
+ path: string,
64
+ sessionKey: string,
65
+ threadId: string,
66
+ defaults: SessionFactoryDefaults<TModelOption, TReasoningOption, TSandboxMode>
67
+ ): TSession {
68
+ const time = new Date().toLocaleTimeString();
69
+ return createSession<TSession, TModelOption, TReasoningOption, TSandboxMode>(
70
+ path,
71
+ `Session started (${time})`,
72
+ {
73
+ threadId,
74
+ reuseStoredThread: false,
75
+ sessionKey
76
+ },
77
+ defaults
78
+ );
79
+ }
@@ -0,0 +1,16 @@
1
+ export function makeSessionKey(path: string): string {
2
+ const normalizedPath = (path || '').trim();
3
+ if (!normalizedPath) {
4
+ return '';
5
+ }
6
+ return normalizedPath;
7
+ }
8
+
9
+ export function resolveSessionKey(path: string): string {
10
+ return makeSessionKey(path || '');
11
+ }
12
+
13
+ export function resolveCurrentSessionKey(path: string): string {
14
+ return resolveSessionKey(path);
15
+ }
16
+
@@ -0,0 +1,36 @@
1
+ export function safeLocalStorageGet(key: string): string | null {
2
+ if (typeof window === 'undefined') {
3
+ return null;
4
+ }
5
+ try {
6
+ return window.localStorage.getItem(key);
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ export function hasStoredValue(key: string): boolean {
13
+ return safeLocalStorageGet(key) !== null;
14
+ }
15
+
16
+ export function safeLocalStorageSet(key: string, value: string): void {
17
+ if (typeof window === 'undefined') {
18
+ return;
19
+ }
20
+ try {
21
+ window.localStorage.setItem(key, value);
22
+ } catch {
23
+ // Ignore storage failures; settings still work for current session.
24
+ }
25
+ }
26
+
27
+ export function safeLocalStorageRemove(key: string): void {
28
+ if (typeof window === 'undefined') {
29
+ return;
30
+ }
31
+ try {
32
+ window.localStorage.removeItem(key);
33
+ } catch {
34
+ // Ignore storage failures; settings still work for current session.
35
+ }
36
+ }
@@ -0,0 +1,56 @@
1
+ import { makeSessionKey } from './codexChatSessionKey';
2
+
3
+ export type MessageLike = {
4
+ notebookPath?: unknown;
5
+ sessionContextKey?: unknown;
6
+ runId?: unknown;
7
+ };
8
+
9
+ export type ResolveMessageSessionKeyParams = {
10
+ message: MessageLike;
11
+ runToSessionKey: Map<string, string>;
12
+ activeSessionKeyByPath: Map<string, string>;
13
+ currentSessionKey: string;
14
+ };
15
+
16
+ export function resolveMessageSessionKey(params: ResolveMessageSessionKeyParams): string {
17
+ const { message, runToSessionKey, activeSessionKeyByPath, currentSessionKey } = params;
18
+ const messagePath = typeof message?.notebookPath === 'string' ? message.notebookPath : '';
19
+ const sessionContextKey = typeof message?.sessionContextKey === 'string' ? message.sessionContextKey : '';
20
+ if (sessionContextKey) {
21
+ return sessionContextKey;
22
+ }
23
+
24
+ const runId = typeof message?.runId === 'string' ? message.runId : '';
25
+ if (runId) {
26
+ const mapped = runToSessionKey.get(runId);
27
+ if (mapped) {
28
+ return mapped;
29
+ }
30
+ }
31
+
32
+ if (messagePath) {
33
+ if (runId) {
34
+ const activeSessionKey = activeSessionKeyByPath.get(messagePath);
35
+ if (activeSessionKey) {
36
+ runToSessionKey.set(runId, activeSessionKey);
37
+ return activeSessionKey;
38
+ }
39
+ }
40
+ const activeSessionKey = activeSessionKeyByPath.get(messagePath);
41
+ if (activeSessionKey) {
42
+ return activeSessionKey;
43
+ }
44
+ return makeSessionKey(messagePath);
45
+ }
46
+
47
+ if (runId) {
48
+ const mapped = runToSessionKey.get(runId);
49
+ if (mapped) {
50
+ return mapped;
51
+ }
52
+ }
53
+
54
+ return currentSessionKey;
55
+ }
56
+