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.
- package/.claude/settings.local.json +9 -0
- package/.github/workflows/unit-tests.yml +27 -0
- package/.jupyterlab-playwright.log +0 -0
- package/README.md +83 -9
- package/docs/images/codex-sidebar-screenshot.png +0 -0
- package/jupyterlab_codex/handlers.py +938 -297
- package/jupyterlab_codex/labextension/package.json +13 -3
- package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
- package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
- package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
- package/jupyterlab_codex/protocol.py +297 -0
- package/jupyterlab_codex/runner.py +58 -15
- package/jupyterlab_codex/sessions.py +582 -97
- package/lib/codexChat.d.ts +13 -0
- package/lib/codexChat.js +2506 -0
- package/lib/codexChat.js.map +1 -0
- package/lib/codexChatAttachmentDedup.d.ts +10 -0
- package/lib/codexChatAttachmentDedup.js +35 -0
- package/lib/codexChatAttachmentDedup.js.map +1 -0
- package/lib/codexChatAttachmentLimit.d.ts +18 -0
- package/lib/codexChatAttachmentLimit.js +50 -0
- package/lib/codexChatAttachmentLimit.js.map +1 -0
- package/lib/codexChatAttachmentState.d.ts +15 -0
- package/lib/codexChatAttachmentState.js +16 -0
- package/lib/codexChatAttachmentState.js.map +1 -0
- package/lib/codexChatDocumentUtils.d.ts +70 -0
- package/lib/codexChatDocumentUtils.js +506 -0
- package/lib/codexChatDocumentUtils.js.map +1 -0
- package/lib/codexChatFormatting.d.ts +11 -0
- package/lib/codexChatFormatting.js +83 -0
- package/lib/codexChatFormatting.js.map +1 -0
- package/lib/codexChatNotice.d.ts +3 -0
- package/lib/codexChatNotice.js +74 -0
- package/lib/codexChatNotice.js.map +1 -0
- package/lib/codexChatPersistence.d.ts +35 -0
- package/lib/codexChatPersistence.js +158 -0
- package/lib/codexChatPersistence.js.map +1 -0
- package/lib/codexChatPrimitives.d.ts +44 -0
- package/lib/codexChatPrimitives.js +156 -0
- package/lib/codexChatPrimitives.js.map +1 -0
- package/lib/codexChatRender.d.ts +24 -0
- package/lib/codexChatRender.js +293 -0
- package/lib/codexChatRender.js.map +1 -0
- package/lib/codexChatSessionFactory.d.ts +15 -0
- package/lib/codexChatSessionFactory.js +45 -0
- package/lib/codexChatSessionFactory.js.map +1 -0
- package/lib/codexChatSessionKey.d.ts +3 -0
- package/lib/codexChatSessionKey.js +14 -0
- package/lib/codexChatSessionKey.js.map +1 -0
- package/lib/codexChatStorage.d.ts +4 -0
- package/lib/codexChatStorage.js +37 -0
- package/lib/codexChatStorage.js.map +1 -0
- package/lib/codexSessionResolver.d.ts +12 -0
- package/lib/codexSessionResolver.js +38 -0
- package/lib/codexSessionResolver.js.map +1 -0
- package/lib/handlers/activitySummarizer.d.ts +15 -0
- package/lib/handlers/activitySummarizer.js +327 -0
- package/lib/handlers/activitySummarizer.js.map +1 -0
- package/lib/handlers/codexMessageTypes.d.ts +30 -0
- package/lib/handlers/codexMessageTypes.js +2 -0
- package/lib/handlers/codexMessageTypes.js.map +1 -0
- package/lib/handlers/codexMessageUtils.d.ts +46 -0
- package/lib/handlers/codexMessageUtils.js +144 -0
- package/lib/handlers/codexMessageUtils.js.map +1 -0
- package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
- package/lib/handlers/handleCodexSocketMessage.js +78 -0
- package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
- package/lib/handlers/sessionSyncHandler.d.ts +34 -0
- package/lib/handlers/sessionSyncHandler.js +181 -0
- package/lib/handlers/sessionSyncHandler.js.map +1 -0
- package/lib/hooks/useCodexSocket.d.ts +15 -0
- package/lib/hooks/useCodexSocket.js +84 -0
- package/lib/hooks/useCodexSocket.js.map +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/panel.d.ts +1 -11
- package/lib/panel.js +1 -2815
- package/lib/panel.js.map +1 -1
- package/lib/protocol.d.ts +235 -0
- package/lib/protocol.js +278 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +13 -3
- package/playwright.config.cjs +27 -0
- package/playwright.unit.config.cjs +19 -0
- package/pyproject.toml +1 -1
- package/release.sh +52 -14
- package/scripts/run_playwright_e2e.sh +96 -0
- package/scripts/run_playwright_freeze_repro.sh +58 -0
- package/scripts/run_playwright_queue_repro.sh +60 -0
- package/scripts/run_playwright_repro.sh +55 -0
- package/src/codexChat.tsx +3914 -0
- package/src/codexChatAttachmentDedup.ts +47 -0
- package/src/codexChatAttachmentLimit.ts +81 -0
- package/src/codexChatAttachmentState.ts +37 -0
- package/src/codexChatDocumentUtils.ts +644 -0
- package/src/codexChatFormatting.ts +94 -0
- package/src/codexChatNotice.ts +95 -0
- package/src/codexChatPersistence.ts +191 -0
- package/src/codexChatPrimitives.tsx +446 -0
- package/src/codexChatRender.tsx +376 -0
- package/src/codexChatSessionFactory.ts +79 -0
- package/src/codexChatSessionKey.ts +16 -0
- package/src/codexChatStorage.ts +36 -0
- package/src/codexSessionResolver.ts +56 -0
- package/src/handlers/activitySummarizer.ts +369 -0
- package/src/handlers/codexMessageTypes.ts +34 -0
- package/src/handlers/codexMessageUtils.ts +217 -0
- package/src/handlers/handleCodexSocketMessage.ts +204 -0
- package/src/handlers/sessionSyncHandler.ts +308 -0
- package/src/hooks/useCodexSocket.ts +109 -0
- package/src/index.ts +1 -1
- package/src/panel.tsx +1 -4184
- package/src/protocol.ts +582 -0
- package/style/index.css +480 -11
- package/test-results/.last-run.json +4 -0
- package/test.py +0 -0
- package/tests/e2e/cell-output-error-tail.spec.js +156 -0
- package/tests/e2e/codex-ui-test-helpers.js +138 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
- package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
- package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
- package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
- package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
- package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
- package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
- package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
- package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
- package/tests/e2e/mock-codex-cli-flood.py +127 -0
- package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
- package/tests/e2e/mock-codex-cli.py +95 -0
- package/tests/e2e/queue-multitab-repro.spec.js +189 -0
- package/tests/test_handlers.py +116 -0
- package/tests/test_protocol.py +169 -0
- package/tests/test_session_store_limits.py +50 -0
- package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
- package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
- package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
- package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
- package/tests/unit/codexChatLimit.spec.ts +18 -0
- package/tests/unit/codexChatNotice.spec.ts +45 -0
- package/tests/unit/codexChatPersistence.spec.ts +199 -0
- package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
- package/tests/unit/codexChatSessionKey.spec.ts +18 -0
- package/tests/unit/codexMessageUtils.spec.ts +89 -0
- package/tests/unit/codexSessionResolver.spec.ts +92 -0
- package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/webpack.config.js +6 -0
- package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
- package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
- package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
- /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
package/src/panel.tsx
CHANGED
|
@@ -1,4184 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import { ReactWidget, Dialog, showDialog } from '@jupyterlab/apputils';
|
|
4
|
-
import { INotebookTracker } from '@jupyterlab/notebook';
|
|
5
|
-
import { ServerConnection } from '@jupyterlab/services';
|
|
6
|
-
import { URLExt } from '@jupyterlab/coreutils';
|
|
7
|
-
import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
8
|
-
import { Message } from '@lumino/messaging';
|
|
9
|
-
import { marked } from 'marked';
|
|
10
|
-
import markedKatex from 'marked-katex-extension';
|
|
11
|
-
import DOMPurify from 'dompurify';
|
|
12
|
-
import hljs from 'highlight.js/lib/common';
|
|
13
|
-
|
|
14
|
-
marked.use(
|
|
15
|
-
markedKatex({
|
|
16
|
-
throwOnError: false
|
|
17
|
-
})
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
function PlusIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
21
|
-
return (
|
|
22
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
23
|
-
<path
|
|
24
|
-
d="M12 5v14M5 12h14"
|
|
25
|
-
stroke="currentColor"
|
|
26
|
-
strokeWidth="2"
|
|
27
|
-
strokeLinecap="round"
|
|
28
|
-
/>
|
|
29
|
-
</svg>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ArrowUpIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
34
|
-
return (
|
|
35
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
36
|
-
<path
|
|
37
|
-
d="M12 19V5m0 0l-7 7m7-7l7 7"
|
|
38
|
-
stroke="currentColor"
|
|
39
|
-
strokeWidth="2"
|
|
40
|
-
strokeLinecap="round"
|
|
41
|
-
strokeLinejoin="round"
|
|
42
|
-
/>
|
|
43
|
-
</svg>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function ArrowDownIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
48
|
-
return (
|
|
49
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
50
|
-
<path
|
|
51
|
-
d="M12 5v14m0 0l-7-7m7 7l7-7"
|
|
52
|
-
stroke="currentColor"
|
|
53
|
-
strokeWidth="2"
|
|
54
|
-
strokeLinecap="round"
|
|
55
|
-
strokeLinejoin="round"
|
|
56
|
-
/>
|
|
57
|
-
</svg>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function StopIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
62
|
-
return (
|
|
63
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
64
|
-
<rect x="7" y="7" width="10" height="10" rx="2.2" stroke="currentColor" strokeWidth="2" />
|
|
65
|
-
</svg>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function ImageIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
70
|
-
return (
|
|
71
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
72
|
-
<rect x="3" y="5" width="18" height="14" rx="2.2" stroke="currentColor" strokeWidth="2" />
|
|
73
|
-
<circle cx="8.5" cy="10" r="1.5" fill="currentColor" />
|
|
74
|
-
<path
|
|
75
|
-
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"
|
|
76
|
-
stroke="currentColor"
|
|
77
|
-
strokeWidth="2"
|
|
78
|
-
strokeLinecap="round"
|
|
79
|
-
strokeLinejoin="round"
|
|
80
|
-
/>
|
|
81
|
-
</svg>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function GearIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
86
|
-
return (
|
|
87
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
88
|
-
<path
|
|
89
|
-
d="M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4Z"
|
|
90
|
-
stroke="currentColor"
|
|
91
|
-
strokeWidth="2"
|
|
92
|
-
/>
|
|
93
|
-
<path
|
|
94
|
-
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"
|
|
95
|
-
stroke="currentColor"
|
|
96
|
-
strokeWidth="2"
|
|
97
|
-
strokeLinejoin="round"
|
|
98
|
-
/>
|
|
99
|
-
</svg>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function ChipIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
104
|
-
return (
|
|
105
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
106
|
-
<path
|
|
107
|
-
d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"
|
|
108
|
-
stroke="currentColor"
|
|
109
|
-
strokeWidth="2"
|
|
110
|
-
strokeLinecap="round"
|
|
111
|
-
/>
|
|
112
|
-
<rect
|
|
113
|
-
x="7"
|
|
114
|
-
y="7"
|
|
115
|
-
width="10"
|
|
116
|
-
height="10"
|
|
117
|
-
rx="2.5"
|
|
118
|
-
stroke="currentColor"
|
|
119
|
-
strokeWidth="2"
|
|
120
|
-
/>
|
|
121
|
-
</svg>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function GaugeIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
126
|
-
return (
|
|
127
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
128
|
-
<path
|
|
129
|
-
d="M5 16a7 7 0 1 1 14 0"
|
|
130
|
-
stroke="currentColor"
|
|
131
|
-
strokeWidth="2"
|
|
132
|
-
strokeLinecap="round"
|
|
133
|
-
/>
|
|
134
|
-
<path
|
|
135
|
-
d="M12 13l3.5-3.5"
|
|
136
|
-
stroke="currentColor"
|
|
137
|
-
strokeWidth="2"
|
|
138
|
-
strokeLinecap="round"
|
|
139
|
-
/>
|
|
140
|
-
<path
|
|
141
|
-
d="M5 16h14"
|
|
142
|
-
stroke="currentColor"
|
|
143
|
-
strokeWidth="2"
|
|
144
|
-
strokeLinecap="round"
|
|
145
|
-
/>
|
|
146
|
-
</svg>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function ReasoningEffortIcon(
|
|
151
|
-
props: React.SVGProps<SVGSVGElement> & {
|
|
152
|
-
effort: ReasoningOptionValue;
|
|
153
|
-
effortOptions?: readonly ReasoningOption[];
|
|
154
|
-
}
|
|
155
|
-
): JSX.Element {
|
|
156
|
-
const { effort, effortOptions = [], ...svgProps } = props;
|
|
157
|
-
if (effort === '__config__') {
|
|
158
|
-
return <GaugeIcon {...svgProps} />;
|
|
159
|
-
}
|
|
160
|
-
const activeBars = getReasoningEffortBars(effort, effortOptions);
|
|
161
|
-
|
|
162
|
-
const bars = [
|
|
163
|
-
{ x: 6, top: 14 },
|
|
164
|
-
{ x: 10, top: 10 },
|
|
165
|
-
{ x: 14, top: 6 },
|
|
166
|
-
{ x: 18, top: 2 }
|
|
167
|
-
];
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
|
|
171
|
-
{bars.map((bar, idx) => (
|
|
172
|
-
<path
|
|
173
|
-
key={idx}
|
|
174
|
-
d={`M${bar.x} 20V${bar.top}`}
|
|
175
|
-
stroke="currentColor"
|
|
176
|
-
strokeWidth="2.6"
|
|
177
|
-
strokeLinecap="round"
|
|
178
|
-
opacity={idx < activeBars ? 1 : 0.25}
|
|
179
|
-
/>
|
|
180
|
-
))}
|
|
181
|
-
</svg>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function BatteryIcon(
|
|
186
|
-
props: React.SVGProps<SVGSVGElement> & { level?: number | null }
|
|
187
|
-
): JSX.Element {
|
|
188
|
-
const { level, ...svgProps } = props;
|
|
189
|
-
const clamped =
|
|
190
|
-
typeof level === 'number' && Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : null;
|
|
191
|
-
const innerWidth = 14;
|
|
192
|
-
const fillWidth = clamped == null ? 0 : Math.max(0, Math.round(innerWidth * clamped));
|
|
193
|
-
const dashed = clamped == null;
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...svgProps}>
|
|
197
|
-
<rect
|
|
198
|
-
x="2"
|
|
199
|
-
y="7"
|
|
200
|
-
width="18"
|
|
201
|
-
height="10"
|
|
202
|
-
rx="2.5"
|
|
203
|
-
stroke="currentColor"
|
|
204
|
-
strokeWidth="2"
|
|
205
|
-
strokeDasharray={dashed ? '4 2' : undefined}
|
|
206
|
-
/>
|
|
207
|
-
<rect x="21" y="10" width="2" height="4" rx="1" fill="currentColor" opacity={0.85} />
|
|
208
|
-
{fillWidth > 0 && (
|
|
209
|
-
<rect x="4" y="9" width={fillWidth} height="6" rx="1.4" fill="currentColor" opacity={0.9} />
|
|
210
|
-
)}
|
|
211
|
-
</svg>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function ShieldIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
216
|
-
return (
|
|
217
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
218
|
-
<path
|
|
219
|
-
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"
|
|
220
|
-
stroke="currentColor"
|
|
221
|
-
strokeWidth="2"
|
|
222
|
-
strokeLinejoin="round"
|
|
223
|
-
/>
|
|
224
|
-
</svg>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function PortalMenu(props: {
|
|
229
|
-
open: boolean;
|
|
230
|
-
anchorRef: React.RefObject<HTMLElement>;
|
|
231
|
-
popoverRef: React.RefObject<HTMLDivElement>;
|
|
232
|
-
className?: string;
|
|
233
|
-
role?: 'dialog' | 'menu';
|
|
234
|
-
align?: 'left' | 'right';
|
|
235
|
-
ariaLabel?: string;
|
|
236
|
-
onMouseEnter?: () => void;
|
|
237
|
-
onMouseLeave?: () => void;
|
|
238
|
-
children: React.ReactNode;
|
|
239
|
-
}): JSX.Element | null {
|
|
240
|
-
const {
|
|
241
|
-
open,
|
|
242
|
-
anchorRef,
|
|
243
|
-
popoverRef,
|
|
244
|
-
className,
|
|
245
|
-
role = 'dialog',
|
|
246
|
-
align = 'left',
|
|
247
|
-
ariaLabel,
|
|
248
|
-
onMouseEnter,
|
|
249
|
-
onMouseLeave,
|
|
250
|
-
children
|
|
251
|
-
} = props;
|
|
252
|
-
const [style, setStyle] = useState<React.CSSProperties>(() => ({
|
|
253
|
-
left: 0,
|
|
254
|
-
top: 0,
|
|
255
|
-
visibility: 'hidden'
|
|
256
|
-
}));
|
|
257
|
-
|
|
258
|
-
useLayoutEffect(() => {
|
|
259
|
-
if (!open) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const update = () => {
|
|
264
|
-
const anchor = anchorRef.current;
|
|
265
|
-
const popover = popoverRef.current;
|
|
266
|
-
if (!anchor || !popover) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const margin = 8;
|
|
271
|
-
const offset = 10;
|
|
272
|
-
const viewportW = window.innerWidth;
|
|
273
|
-
const viewportH = window.innerHeight;
|
|
274
|
-
const anchorRect = anchor.getBoundingClientRect();
|
|
275
|
-
const popRect = popover.getBoundingClientRect();
|
|
276
|
-
|
|
277
|
-
// Default: above, aligned to the anchor.
|
|
278
|
-
let left = align === 'right' ? anchorRect.right - popRect.width : anchorRect.left;
|
|
279
|
-
left = clampNumber(left, margin, Math.max(margin, viewportW - popRect.width - margin));
|
|
280
|
-
|
|
281
|
-
let top = anchorRect.top - offset - popRect.height;
|
|
282
|
-
const belowTop = anchorRect.bottom + offset;
|
|
283
|
-
if (top < margin && belowTop + popRect.height + margin <= viewportH) {
|
|
284
|
-
top = belowTop;
|
|
285
|
-
}
|
|
286
|
-
top = clampNumber(top, margin, Math.max(margin, viewportH - popRect.height - margin));
|
|
287
|
-
|
|
288
|
-
setStyle({
|
|
289
|
-
left: Math.round(left),
|
|
290
|
-
top: Math.round(top),
|
|
291
|
-
visibility: 'visible'
|
|
292
|
-
});
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const raf = window.requestAnimationFrame(update);
|
|
296
|
-
const onResize = () => update();
|
|
297
|
-
const onScroll = () => update();
|
|
298
|
-
window.addEventListener('resize', onResize);
|
|
299
|
-
// Capture scroll events from any scroll container.
|
|
300
|
-
window.addEventListener('scroll', onScroll, true);
|
|
301
|
-
return () => {
|
|
302
|
-
window.cancelAnimationFrame(raf);
|
|
303
|
-
window.removeEventListener('resize', onResize);
|
|
304
|
-
window.removeEventListener('scroll', onScroll, true);
|
|
305
|
-
};
|
|
306
|
-
}, [open, anchorRef, popoverRef]);
|
|
307
|
-
|
|
308
|
-
if (!open) {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return createPortal(
|
|
313
|
-
<div
|
|
314
|
-
ref={popoverRef}
|
|
315
|
-
className={`jp-CodexMenu jp-CodexMenuPortal${className ? ` ${className}` : ''}`}
|
|
316
|
-
role={role}
|
|
317
|
-
aria-label={ariaLabel}
|
|
318
|
-
style={style}
|
|
319
|
-
onMouseEnter={onMouseEnter}
|
|
320
|
-
onMouseLeave={onMouseLeave}
|
|
321
|
-
>
|
|
322
|
-
{children}
|
|
323
|
-
</div>,
|
|
324
|
-
document.body
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function CheckIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
329
|
-
return (
|
|
330
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
331
|
-
<path
|
|
332
|
-
d="M20 6 9 17l-5-5"
|
|
333
|
-
stroke="currentColor"
|
|
334
|
-
strokeWidth="2"
|
|
335
|
-
strokeLinecap="round"
|
|
336
|
-
strokeLinejoin="round"
|
|
337
|
-
/>
|
|
338
|
-
</svg>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function XIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|
343
|
-
return (
|
|
344
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" {...props}>
|
|
345
|
-
<path
|
|
346
|
-
d="M18 6 6 18M6 6l12 12"
|
|
347
|
-
stroke="currentColor"
|
|
348
|
-
strokeWidth="2"
|
|
349
|
-
strokeLinecap="round"
|
|
350
|
-
strokeLinejoin="round"
|
|
351
|
-
/>
|
|
352
|
-
</svg>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export class CodexPanel extends ReactWidget {
|
|
357
|
-
private _notebooks: INotebookTracker;
|
|
358
|
-
|
|
359
|
-
constructor(notebooks: INotebookTracker) {
|
|
360
|
-
super();
|
|
361
|
-
this._notebooks = notebooks;
|
|
362
|
-
this.addClass('jp-CodexPanel');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
render(): JSX.Element {
|
|
366
|
-
return <CodexChat notebooks={this._notebooks} />;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
protected onAfterAttach(msg: Message): void {
|
|
370
|
-
super.onAfterAttach(msg);
|
|
371
|
-
this.focusComposer();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
protected onActivateRequest(msg: Message): void {
|
|
375
|
-
super.onActivateRequest(msg);
|
|
376
|
-
this.focusComposer();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
private focusComposer(): void {
|
|
380
|
-
window.setTimeout(() => {
|
|
381
|
-
const textarea = this.node.querySelector<HTMLTextAreaElement>('.jp-CodexComposer textarea');
|
|
382
|
-
if (!textarea || textarea.disabled) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
textarea.focus({ preventScroll: true });
|
|
386
|
-
const cursor = textarea.value.length;
|
|
387
|
-
textarea.setSelectionRange(cursor, cursor);
|
|
388
|
-
}, 0);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
type TextRole = 'user' | 'assistant' | 'system';
|
|
393
|
-
type ChatAttachments = {
|
|
394
|
-
images?: number;
|
|
395
|
-
};
|
|
396
|
-
type ChatEntry =
|
|
397
|
-
| {
|
|
398
|
-
kind: 'text';
|
|
399
|
-
id: string;
|
|
400
|
-
role: TextRole;
|
|
401
|
-
text: string;
|
|
402
|
-
attachments?: ChatAttachments;
|
|
403
|
-
}
|
|
404
|
-
| {
|
|
405
|
-
kind: 'run-divider';
|
|
406
|
-
id: string;
|
|
407
|
-
elapsedMs: number;
|
|
408
|
-
}
|
|
409
|
-
| {
|
|
410
|
-
kind: 'activity';
|
|
411
|
-
id: string;
|
|
412
|
-
item: ActivityItem;
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
type PendingImageAttachment = {
|
|
416
|
-
id: string;
|
|
417
|
-
file: File;
|
|
418
|
-
previewUrl: string;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
type RunState = 'ready' | 'running';
|
|
422
|
-
type PanelStatus = 'disconnected' | RunState;
|
|
423
|
-
type ProgressKind = '' | 'reasoning';
|
|
424
|
-
type ActivityPhase = 'started' | 'completed' | '';
|
|
425
|
-
type ActivityCategory = 'reasoning' | 'command' | 'file' | 'tool' | 'event';
|
|
426
|
-
type ActivityItem = {
|
|
427
|
-
id: string;
|
|
428
|
-
ts: number;
|
|
429
|
-
category: ActivityCategory;
|
|
430
|
-
phase: ActivityPhase;
|
|
431
|
-
title: string;
|
|
432
|
-
detail: string;
|
|
433
|
-
raw: string;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
type RateLimitWindowSnapshot = {
|
|
437
|
-
usedPercent: number | null;
|
|
438
|
-
windowMinutes: number | null;
|
|
439
|
-
resetsAt: number | null;
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
type CodexRateLimitsSnapshot = {
|
|
443
|
-
updatedAt: string | null;
|
|
444
|
-
primary: RateLimitWindowSnapshot | null;
|
|
445
|
-
secondary: RateLimitWindowSnapshot | null;
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
type ModelCatalogEntry = {
|
|
449
|
-
model: string;
|
|
450
|
-
displayName: string;
|
|
451
|
-
reasoningEfforts?: string[];
|
|
452
|
-
defaultReasoningEffort?: string;
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
type CodexChatProps = {
|
|
456
|
-
notebooks: INotebookTracker;
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
type CliDefaultsSnapshot = {
|
|
460
|
-
model: string | null;
|
|
461
|
-
reasoningEffort: string | null;
|
|
462
|
-
availableModels?: ModelCatalogEntry[];
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
type NotebookSession = {
|
|
466
|
-
threadId: string;
|
|
467
|
-
messages: ChatEntry[];
|
|
468
|
-
runState: RunState;
|
|
469
|
-
activeRunId: string | null;
|
|
470
|
-
runStartedAt: number | null;
|
|
471
|
-
progress: string;
|
|
472
|
-
progressKind: ProgressKind;
|
|
473
|
-
pairedOk: boolean | null;
|
|
474
|
-
pairedPath: string;
|
|
475
|
-
pairedOsPath: string;
|
|
476
|
-
pairedMessage: string;
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
type SessionThreadSyncEvent = {
|
|
480
|
-
kind: 'new-thread';
|
|
481
|
-
sessionKey: string;
|
|
482
|
-
notebookPath: string;
|
|
483
|
-
threadId: string;
|
|
484
|
-
source: string;
|
|
485
|
-
id: string;
|
|
486
|
-
issuedAt: number;
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
type ModelOptionValue = '__config__' | string;
|
|
490
|
-
type ModelOption = {
|
|
491
|
-
label: string;
|
|
492
|
-
value: string;
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const FALLBACK_MODEL_OPTIONS: ModelOption[] = [];
|
|
496
|
-
type ReasoningOption = {
|
|
497
|
-
label: string;
|
|
498
|
-
value: string;
|
|
499
|
-
};
|
|
500
|
-
type ReasoningOptionValue = '__config__' | string;
|
|
501
|
-
const MAX_REASONING_EFFORT_BARS = 4;
|
|
502
|
-
const SANDBOX_OPTIONS = [
|
|
503
|
-
{ label: 'Default permission', value: 'workspace-write' },
|
|
504
|
-
{ label: 'Full access', value: 'danger-full-access' }
|
|
505
|
-
] as const;
|
|
506
|
-
type SandboxMode = (typeof SANDBOX_OPTIONS)[number]['value'];
|
|
507
|
-
const AUTO_SAVE_STORAGE_KEY = 'jupyterlab-codex:auto-save-before-send';
|
|
508
|
-
const MODEL_STORAGE_KEY = 'jupyterlab-codex:model';
|
|
509
|
-
const COMMAND_PATH_STORAGE_KEY = 'jupyterlab-codex:command-path';
|
|
510
|
-
const REASONING_STORAGE_KEY = 'jupyterlab-codex:reasoning-effort';
|
|
511
|
-
const SANDBOX_MODE_STORAGE_KEY = 'jupyterlab-codex:sandbox-mode';
|
|
512
|
-
const SETTINGS_OPEN_STORAGE_KEY = 'jupyterlab-codex:settings-open';
|
|
513
|
-
const NOTIFY_ON_DONE_STORAGE_KEY = 'jupyterlab-codex:notify-on-done';
|
|
514
|
-
const NOTIFY_ON_DONE_MIN_SECONDS_STORAGE_KEY = 'jupyterlab-codex:notify-on-done-min-seconds';
|
|
515
|
-
const INCLUDE_ACTIVE_CELL_STORAGE_KEY = 'jupyterlab-codex:include-active-cell';
|
|
516
|
-
const INCLUDE_ACTIVE_CELL_OUTPUT_STORAGE_KEY = 'jupyterlab-codex:include-active-cell-output';
|
|
517
|
-
const SESSION_THREADS_STORAGE_KEY = 'jupyterlab-codex:session-threads';
|
|
518
|
-
const SESSION_THREADS_EVENT_KEY = 'jupyterlab-codex:session-threads:event';
|
|
519
|
-
const DELETE_ALL_PENDING_KEY = 'jupyterlab-codex:delete-all-pending';
|
|
520
|
-
const SESSION_KEY_SEPARATOR = '\u0000';
|
|
521
|
-
|
|
522
|
-
const MAX_IMAGE_ATTACHMENTS = 4;
|
|
523
|
-
const MAX_IMAGE_ATTACHMENT_BYTES = 4 * 1024 * 1024; // Avoid huge WebSocket payloads.
|
|
524
|
-
const MAX_IMAGE_ATTACHMENT_TOTAL_BYTES = 6 * 1024 * 1024;
|
|
525
|
-
|
|
526
|
-
function findModelLabel(model: string, options: readonly ModelOption[]): string {
|
|
527
|
-
const match = options.find(option => option.value === model);
|
|
528
|
-
return match ? match.label : truncateMiddle(model, 32);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function coerceReasoningEffort(value: unknown): string {
|
|
532
|
-
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function coerceReasoningText(value: unknown): string {
|
|
536
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function coerceReasoningEffortEntry(value: unknown): string {
|
|
540
|
-
if (typeof value === 'string') {
|
|
541
|
-
return coerceReasoningText(value);
|
|
542
|
-
}
|
|
543
|
-
if (value && typeof value === 'object' && 'reasoningEffort' in (value as Record<string, unknown>)) {
|
|
544
|
-
return coerceReasoningText((value as { reasoningEffort?: unknown }).reasoningEffort);
|
|
545
|
-
}
|
|
546
|
-
return '';
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function readModelCatalog(rawModels: unknown): ModelCatalogEntry[] {
|
|
550
|
-
if (!Array.isArray(rawModels)) {
|
|
551
|
-
return [];
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const seen = new Set<string>();
|
|
555
|
-
const catalog: ModelCatalogEntry[] = [];
|
|
556
|
-
for (const item of rawModels) {
|
|
557
|
-
if (!item || typeof item !== 'object') {
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
const rawModel = (item as { model?: unknown }).model;
|
|
561
|
-
const rawDisplayName = (item as { displayName?: unknown }).displayName;
|
|
562
|
-
const rawDefaultReasoningEffort = (item as { defaultReasoningEffort?: unknown }).defaultReasoningEffort;
|
|
563
|
-
const rawReasoningEfforts =
|
|
564
|
-
(item as { reasoningEfforts?: unknown }).reasoningEfforts ??
|
|
565
|
-
(item as { supportedReasoningEfforts?: unknown }).supportedReasoningEfforts;
|
|
566
|
-
if (typeof rawModel !== 'string') {
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
const model = rawModel.trim();
|
|
570
|
-
if (!model || seen.has(model)) {
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
seen.add(model);
|
|
574
|
-
|
|
575
|
-
const displayName = typeof rawDisplayName === 'string' && rawDisplayName.trim() ? rawDisplayName.trim() : model;
|
|
576
|
-
const defaultReasoningEffort = coerceReasoningEffortEntry(rawDefaultReasoningEffort);
|
|
577
|
-
const reasoningEfforts = Array.isArray(rawReasoningEfforts)
|
|
578
|
-
? rawReasoningEfforts.reduce<string[]>((acc, effortCandidate: unknown) => {
|
|
579
|
-
const effort = coerceReasoningEffortEntry(effortCandidate);
|
|
580
|
-
if (!effort || acc.includes(effort)) {
|
|
581
|
-
return acc;
|
|
582
|
-
}
|
|
583
|
-
acc.push(effort);
|
|
584
|
-
return acc;
|
|
585
|
-
}, [])
|
|
586
|
-
: [];
|
|
587
|
-
catalog.push({
|
|
588
|
-
model,
|
|
589
|
-
displayName,
|
|
590
|
-
...(reasoningEfforts.length > 0 ? { reasoningEfforts } : {}),
|
|
591
|
-
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return catalog;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function readModelOptions(rawModels: unknown): ModelOption[] {
|
|
599
|
-
const catalog = readModelCatalog(rawModels);
|
|
600
|
-
return catalog.map(entry => ({ label: entry.displayName, value: entry.model }));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function getReasoningEffortBars(
|
|
604
|
-
effort: string,
|
|
605
|
-
effortOptions: readonly ReasoningOption[]
|
|
606
|
-
): number {
|
|
607
|
-
if (!effort || effort === '__config__') {
|
|
608
|
-
return 0;
|
|
609
|
-
}
|
|
610
|
-
if (effortOptions.length <= 0) {
|
|
611
|
-
return 0;
|
|
612
|
-
}
|
|
613
|
-
const index = effortOptions.findIndex(option => option.value === effort);
|
|
614
|
-
if (index < 0) {
|
|
615
|
-
return 0;
|
|
616
|
-
}
|
|
617
|
-
if (effortOptions.length === 1) {
|
|
618
|
-
return 1;
|
|
619
|
-
}
|
|
620
|
-
const scale = MAX_REASONING_EFFORT_BARS - 1;
|
|
621
|
-
return Math.max(
|
|
622
|
-
1,
|
|
623
|
-
Math.min(
|
|
624
|
-
MAX_REASONING_EFFORT_BARS,
|
|
625
|
-
Math.floor((index * scale) / (effortOptions.length - 1)) + 1
|
|
626
|
-
)
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function buildReasoningOptions(rawModels: unknown, selectedModel: string): ReasoningOption[] {
|
|
631
|
-
const catalog = readModelCatalog(rawModels);
|
|
632
|
-
const normalizedModel = selectedModel.trim();
|
|
633
|
-
const modelByName = catalog.find(item => item.model === normalizedModel);
|
|
634
|
-
const reasons =
|
|
635
|
-
modelByName?.reasoningEfforts && modelByName.reasoningEfforts.length > 0
|
|
636
|
-
? modelByName.reasoningEfforts
|
|
637
|
-
: modelByName?.defaultReasoningEffort
|
|
638
|
-
? [modelByName.defaultReasoningEffort]
|
|
639
|
-
: catalog.flatMap(item => item.reasoningEfforts ?? []);
|
|
640
|
-
|
|
641
|
-
const deduped = new Map<string, ReasoningOption>();
|
|
642
|
-
for (const reason of reasons) {
|
|
643
|
-
const label = coerceReasoningEffortEntry(reason);
|
|
644
|
-
const normalized = coerceReasoningEffort(label);
|
|
645
|
-
if (!normalized || deduped.has(normalized)) {
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
deduped.set(normalized, { value: normalized, label: label || normalized });
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return Array.from(deduped.values());
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function findReasoningLabel(value: string, options: readonly ReasoningOption[]): string {
|
|
655
|
-
const match = options.find(option => option.value === value);
|
|
656
|
-
if (match) {
|
|
657
|
-
return match.label;
|
|
658
|
-
}
|
|
659
|
-
const raw = coerceReasoningText(value);
|
|
660
|
-
if (!raw) {
|
|
661
|
-
return 'Reasoning';
|
|
662
|
-
}
|
|
663
|
-
return raw;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function isKnownSandboxMode(value: string): value is SandboxMode {
|
|
667
|
-
return SANDBOX_OPTIONS.some(option => option.value === value);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function makeSessionKey(path: string): string {
|
|
671
|
-
const normalizedPath = (path || '').trim();
|
|
672
|
-
if (!normalizedPath) {
|
|
673
|
-
return '';
|
|
674
|
-
}
|
|
675
|
-
return normalizedPath;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function createSessionEventId(): string {
|
|
679
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
680
|
-
return crypto.randomUUID();
|
|
681
|
-
}
|
|
682
|
-
return `${Date.now()}-${Math.floor(Math.random() * 0x100000000).toString(16)}`;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function parseSessionKey(sessionKey: string): { path: string } {
|
|
686
|
-
if (!sessionKey) {
|
|
687
|
-
return { path: '' };
|
|
688
|
-
}
|
|
689
|
-
const separatorIndex = sessionKey.indexOf(SESSION_KEY_SEPARATOR);
|
|
690
|
-
if (separatorIndex < 0) {
|
|
691
|
-
return { path: sessionKey };
|
|
692
|
-
}
|
|
693
|
-
return { path: sessionKey.slice(0, separatorIndex) };
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function readStoredSessionThreads(): Record<string, string> {
|
|
697
|
-
const raw = safeLocalStorageGet(SESSION_THREADS_STORAGE_KEY);
|
|
698
|
-
if (!raw) {
|
|
699
|
-
return {};
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
const parsed = JSON.parse(raw);
|
|
703
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
704
|
-
return {};
|
|
705
|
-
}
|
|
706
|
-
const result: Record<string, string> = {};
|
|
707
|
-
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
708
|
-
if (!key || typeof value !== 'string') {
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
const threadId = value.trim();
|
|
712
|
-
if (!threadId) {
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
result[key] = threadId;
|
|
716
|
-
}
|
|
717
|
-
return result;
|
|
718
|
-
} catch {
|
|
719
|
-
return {};
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function getStoredSessionThreadCount(): number {
|
|
724
|
-
const mapping = readStoredSessionThreads();
|
|
725
|
-
const uniquePaths = new Set<string>();
|
|
726
|
-
for (const key of Object.keys(mapping)) {
|
|
727
|
-
const { path } = parseSessionKey(key);
|
|
728
|
-
if (path) {
|
|
729
|
-
uniquePaths.add(path);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
return uniquePaths.size;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function readStoredThreadId(path: string, sessionKey: string): string {
|
|
736
|
-
const normalizedPath = path.trim();
|
|
737
|
-
const normalizedSessionKey = sessionKey || '';
|
|
738
|
-
if (!normalizedSessionKey) {
|
|
739
|
-
return '';
|
|
740
|
-
}
|
|
741
|
-
const mapping = readStoredSessionThreads();
|
|
742
|
-
const exactMatch = mapping[normalizedSessionKey];
|
|
743
|
-
if (exactMatch) {
|
|
744
|
-
return exactMatch;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (!normalizedPath) {
|
|
748
|
-
return '';
|
|
749
|
-
}
|
|
750
|
-
for (const [key, threadId] of Object.entries(mapping)) {
|
|
751
|
-
if (!threadId) {
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
const parsed = parseSessionKey(key);
|
|
755
|
-
if (parsed.path === normalizedPath) {
|
|
756
|
-
return threadId;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return '';
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function persistStoredSessionThreads(sessions: Map<string, NotebookSession>): void {
|
|
763
|
-
const mapping: Record<string, string> = {};
|
|
764
|
-
for (const [sessionKey, session] of sessions) {
|
|
765
|
-
if (!sessionKey || !session?.threadId) {
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
mapping[sessionKey] = session.threadId;
|
|
769
|
-
}
|
|
770
|
-
try {
|
|
771
|
-
safeLocalStorageSet(SESSION_THREADS_STORAGE_KEY, JSON.stringify(mapping));
|
|
772
|
-
} catch {
|
|
773
|
-
// Ignore storage failures; in-memory sessions still work.
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function coerceSessionThreadSyncEvent(value: string): SessionThreadSyncEvent | null {
|
|
778
|
-
if (!value) {
|
|
779
|
-
return null;
|
|
780
|
-
}
|
|
781
|
-
let raw: unknown;
|
|
782
|
-
try {
|
|
783
|
-
raw = JSON.parse(value);
|
|
784
|
-
} catch {
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
787
|
-
if (!raw || typeof raw !== 'object') {
|
|
788
|
-
return null;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const event = raw as Record<string, unknown>;
|
|
792
|
-
const sessionKey = typeof event.sessionKey === 'string' ? event.sessionKey.trim() : '';
|
|
793
|
-
const notebookPath = typeof event.notebookPath === 'string' ? event.notebookPath.trim() : '';
|
|
794
|
-
const threadId = typeof event.threadId === 'string' ? event.threadId.trim() : '';
|
|
795
|
-
const source = typeof event.source === 'string' ? event.source.trim() : '';
|
|
796
|
-
const id = typeof event.id === 'string' ? event.id.trim() : '';
|
|
797
|
-
if (!sessionKey || !notebookPath || !threadId || !id || event.kind !== 'new-thread') {
|
|
798
|
-
return null;
|
|
799
|
-
}
|
|
800
|
-
const issuedAt = typeof event.issuedAt === 'number' && Number.isFinite(event.issuedAt) ? event.issuedAt : Date.now();
|
|
801
|
-
return { kind: 'new-thread', sessionKey, notebookPath, threadId, source, id, issuedAt };
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function coerceSessionHistory(
|
|
805
|
-
raw: any
|
|
806
|
-
): Array<{
|
|
807
|
-
role: TextRole;
|
|
808
|
-
content: string;
|
|
809
|
-
}> {
|
|
810
|
-
if (!Array.isArray(raw)) {
|
|
811
|
-
return [];
|
|
812
|
-
}
|
|
813
|
-
const result: Array<{ role: TextRole; content: string }> = [];
|
|
814
|
-
for (const item of raw) {
|
|
815
|
-
if (!item || typeof item !== 'object') {
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
const role = (item as any).role;
|
|
819
|
-
const content = (item as any).content;
|
|
820
|
-
if ((role !== 'user' && role !== 'assistant' && role !== 'system') || typeof content !== 'string') {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
result.push({ role, content });
|
|
824
|
-
}
|
|
825
|
-
return result;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function createSession(
|
|
829
|
-
path: string,
|
|
830
|
-
intro: string,
|
|
831
|
-
options?: {
|
|
832
|
-
threadId?: string;
|
|
833
|
-
reuseStoredThread?: boolean;
|
|
834
|
-
sessionKey?: string;
|
|
835
|
-
}
|
|
836
|
-
): NotebookSession {
|
|
837
|
-
const defaultIntro = 'Session started';
|
|
838
|
-
const systemText = normalizeSystemText('system', intro || defaultIntro);
|
|
839
|
-
const requestedThreadId = (options?.threadId || '').trim();
|
|
840
|
-
const storedThreadId = options?.reuseStoredThread === false ? '' : readStoredThreadId(path, options?.sessionKey || '');
|
|
841
|
-
const threadId = requestedThreadId || storedThreadId || crypto.randomUUID();
|
|
842
|
-
return {
|
|
843
|
-
threadId,
|
|
844
|
-
runState: 'ready',
|
|
845
|
-
activeRunId: null,
|
|
846
|
-
runStartedAt: null,
|
|
847
|
-
progress: '',
|
|
848
|
-
progressKind: '',
|
|
849
|
-
messages: [
|
|
850
|
-
{
|
|
851
|
-
kind: 'text',
|
|
852
|
-
id: crypto.randomUUID(),
|
|
853
|
-
role: 'system',
|
|
854
|
-
text: systemText
|
|
855
|
-
}
|
|
856
|
-
],
|
|
857
|
-
pairedOk: null,
|
|
858
|
-
pairedPath: '',
|
|
859
|
-
pairedOsPath: '',
|
|
860
|
-
pairedMessage: '',
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function createThreadResetSession(path: string, sessionKey: string, threadId: string): NotebookSession {
|
|
865
|
-
const time = new Date().toLocaleTimeString();
|
|
866
|
-
return createSession(path, `Session started (${time})`, {
|
|
867
|
-
threadId,
|
|
868
|
-
reuseStoredThread: false,
|
|
869
|
-
sessionKey
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function safeLocalStorageGet(key: string): string | null {
|
|
874
|
-
if (typeof window === 'undefined') {
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
try {
|
|
878
|
-
return window.localStorage.getItem(key);
|
|
879
|
-
} catch {
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function hasStoredValue(key: string): boolean {
|
|
885
|
-
return safeLocalStorageGet(key) !== null;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function extractTrailingParenValue(text: string): { rest: string; value: string | null } {
|
|
889
|
-
const trimmed = text.trim();
|
|
890
|
-
if (!trimmed.endsWith(')')) {
|
|
891
|
-
return { rest: trimmed, value: null };
|
|
892
|
-
}
|
|
893
|
-
const start = trimmed.lastIndexOf('(');
|
|
894
|
-
if (start < 0) {
|
|
895
|
-
return { rest: trimmed, value: null };
|
|
896
|
-
}
|
|
897
|
-
const inner = trimmed.slice(start + 1, -1).trim();
|
|
898
|
-
if (!inner) {
|
|
899
|
-
return { rest: trimmed, value: null };
|
|
900
|
-
}
|
|
901
|
-
return { rest: trimmed.slice(0, start).trimEnd(), value: inner };
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function formatSessionStartedNotice(label: string, time: string | null): string {
|
|
905
|
-
return `${label}${time ? ` (${time})` : ''}`;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function normalizeSessionStartedNotice(text: string): string | null {
|
|
909
|
-
const raw = text.trim();
|
|
910
|
-
|
|
911
|
-
// Korean legacy messages (normalize to English UI text)
|
|
912
|
-
if (raw.startsWith('세션 시작')) {
|
|
913
|
-
const { rest, value } = extractTrailingParenValue(raw);
|
|
914
|
-
if (rest.startsWith('세션 시작')) {
|
|
915
|
-
return formatSessionStartedNotice('Session started', value);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// English (case-insensitive)
|
|
920
|
-
const lower = raw.toLowerCase();
|
|
921
|
-
if (lower.startsWith('session started')) {
|
|
922
|
-
const { rest, value } = extractTrailingParenValue(raw);
|
|
923
|
-
if (rest.toLowerCase().startsWith('session started')) {
|
|
924
|
-
return formatSessionStartedNotice('Session started', value);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return null;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function normalizeSystemText(role: TextRole, text: string): string {
|
|
932
|
-
if (role !== 'system') {
|
|
933
|
-
return text;
|
|
934
|
-
}
|
|
935
|
-
return normalizeSessionStartedNotice(text) ?? text;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function isSessionStartNotice(text: string): boolean {
|
|
939
|
-
if (normalizeSessionStartedNotice(text) !== null) {
|
|
940
|
-
return true;
|
|
941
|
-
}
|
|
942
|
-
const trimmed = text.trimStart();
|
|
943
|
-
if (trimmed.startsWith('새 스레드 시작:')) {
|
|
944
|
-
return true;
|
|
945
|
-
}
|
|
946
|
-
const lower = trimmed.toLowerCase();
|
|
947
|
-
return (
|
|
948
|
-
lower.startsWith('session start:') ||
|
|
949
|
-
lower.startsWith('new thread started:') ||
|
|
950
|
-
lower.startsWith('new thread started')
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function safeLocalStorageSet(key: string, value: string): void {
|
|
955
|
-
if (typeof window === 'undefined') {
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
try {
|
|
959
|
-
window.localStorage.setItem(key, value);
|
|
960
|
-
} catch {
|
|
961
|
-
// Ignore storage errors; settings still work for current session.
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function safeLocalStorageRemove(key: string): void {
|
|
966
|
-
if (typeof window === 'undefined') {
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
try {
|
|
970
|
-
window.localStorage.removeItem(key);
|
|
971
|
-
} catch {
|
|
972
|
-
// Ignore storage errors; settings still work for current session.
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function markDeleteAllPending(): void {
|
|
977
|
-
safeLocalStorageSet(DELETE_ALL_PENDING_KEY, '1');
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function clearDeleteAllPending(): void {
|
|
981
|
-
safeLocalStorageRemove(DELETE_ALL_PENDING_KEY);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function hasDeleteAllPending(): boolean {
|
|
985
|
-
return safeLocalStorageGet(DELETE_ALL_PENDING_KEY) === '1';
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function readStoredModel(): string {
|
|
989
|
-
return safeLocalStorageGet(MODEL_STORAGE_KEY) ?? '';
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function readStoredCommandPath(): string {
|
|
993
|
-
return safeLocalStorageGet(COMMAND_PATH_STORAGE_KEY) ?? '';
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function readStoredAutoSave(): boolean {
|
|
997
|
-
return (safeLocalStorageGet(AUTO_SAVE_STORAGE_KEY) ?? 'true') !== 'false';
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function readStoredIncludeActiveCell(): boolean {
|
|
1001
|
-
return (safeLocalStorageGet(INCLUDE_ACTIVE_CELL_STORAGE_KEY) ?? 'true') !== 'false';
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function readStoredIncludeActiveCellOutput(): boolean {
|
|
1005
|
-
return (safeLocalStorageGet(INCLUDE_ACTIVE_CELL_OUTPUT_STORAGE_KEY) ?? 'true') !== 'false';
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function readStoredReasoningEffort(): ReasoningOptionValue {
|
|
1009
|
-
try {
|
|
1010
|
-
const stored = safeLocalStorageGet(REASONING_STORAGE_KEY) ?? '';
|
|
1011
|
-
if (stored === '__config__') {
|
|
1012
|
-
return '__config__';
|
|
1013
|
-
}
|
|
1014
|
-
const normalized = stored.trim();
|
|
1015
|
-
return normalized ? (normalized as ReasoningOptionValue) : '__config__';
|
|
1016
|
-
} catch {
|
|
1017
|
-
return '__config__';
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function readStoredSandboxMode(): SandboxMode {
|
|
1022
|
-
const stored = safeLocalStorageGet(SANDBOX_MODE_STORAGE_KEY) ?? '';
|
|
1023
|
-
return isKnownSandboxMode(stored) ? stored : SANDBOX_OPTIONS[0].value;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function persistModel(model: string): void {
|
|
1027
|
-
safeLocalStorageSet(MODEL_STORAGE_KEY, model);
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
function persistCommandPath(commandPath: string): void {
|
|
1031
|
-
safeLocalStorageSet(COMMAND_PATH_STORAGE_KEY, commandPath);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function persistAutoSave(enabled: boolean): void {
|
|
1035
|
-
safeLocalStorageSet(AUTO_SAVE_STORAGE_KEY, enabled ? 'true' : 'false');
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function persistIncludeActiveCell(enabled: boolean): void {
|
|
1039
|
-
safeLocalStorageSet(INCLUDE_ACTIVE_CELL_STORAGE_KEY, enabled ? 'true' : 'false');
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function persistIncludeActiveCellOutput(enabled: boolean): void {
|
|
1043
|
-
safeLocalStorageSet(INCLUDE_ACTIVE_CELL_OUTPUT_STORAGE_KEY, enabled ? 'true' : 'false');
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
function persistReasoningEffort(value: ReasoningOptionValue): void {
|
|
1047
|
-
safeLocalStorageSet(REASONING_STORAGE_KEY, value);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function persistSandboxMode(value: SandboxMode): void {
|
|
1051
|
-
safeLocalStorageSet(SANDBOX_MODE_STORAGE_KEY, value);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function readStoredSettingsOpen(): boolean {
|
|
1055
|
-
return (safeLocalStorageGet(SETTINGS_OPEN_STORAGE_KEY) ?? 'false') === 'true';
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function persistSettingsOpen(enabled: boolean): void {
|
|
1059
|
-
safeLocalStorageSet(SETTINGS_OPEN_STORAGE_KEY, enabled ? 'true' : 'false');
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function readStoredNotifyOnDone(): boolean {
|
|
1063
|
-
return (safeLocalStorageGet(NOTIFY_ON_DONE_STORAGE_KEY) ?? 'true') === 'true';
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function persistNotifyOnDone(enabled: boolean): void {
|
|
1067
|
-
safeLocalStorageSet(NOTIFY_ON_DONE_STORAGE_KEY, enabled ? 'true' : 'false');
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function readStoredNotifyOnDoneMinSeconds(): number {
|
|
1071
|
-
const raw = safeLocalStorageGet(NOTIFY_ON_DONE_MIN_SECONDS_STORAGE_KEY);
|
|
1072
|
-
if (raw == null) {
|
|
1073
|
-
return 30;
|
|
1074
|
-
}
|
|
1075
|
-
const parsed = Number(raw);
|
|
1076
|
-
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : 0;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function persistNotifyOnDoneMinSeconds(seconds: number): void {
|
|
1080
|
-
safeLocalStorageSet(NOTIFY_ON_DONE_MIN_SECONDS_STORAGE_KEY, String(Math.max(0, Math.trunc(seconds))));
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function getBrowserNotificationPermission(): NotificationPermission | 'unsupported' {
|
|
1084
|
-
if (typeof window === 'undefined' || typeof window.Notification === 'undefined') {
|
|
1085
|
-
return 'unsupported';
|
|
1086
|
-
}
|
|
1087
|
-
return window.Notification.permission;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function truncateMiddle(value: string, max: number): string {
|
|
1091
|
-
if (value.length <= max) {
|
|
1092
|
-
return value;
|
|
1093
|
-
}
|
|
1094
|
-
const ellipsis = '...';
|
|
1095
|
-
if (max <= ellipsis.length) {
|
|
1096
|
-
return value.slice(0, max);
|
|
1097
|
-
}
|
|
1098
|
-
const head = Math.max(0, Math.floor((max - ellipsis.length) * 0.6));
|
|
1099
|
-
const tail = Math.max(0, max - head - ellipsis.length);
|
|
1100
|
-
return `${value.slice(0, head)}${ellipsis}${value.slice(value.length - tail)}`;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function safePreview(value: unknown, max = 220): string {
|
|
1104
|
-
if (value == null) {
|
|
1105
|
-
return '';
|
|
1106
|
-
}
|
|
1107
|
-
if (typeof value === 'string') {
|
|
1108
|
-
return truncateMiddle(value, max);
|
|
1109
|
-
}
|
|
1110
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1111
|
-
return String(value);
|
|
1112
|
-
}
|
|
1113
|
-
try {
|
|
1114
|
-
return truncateMiddle(JSON.stringify(value), max);
|
|
1115
|
-
} catch {
|
|
1116
|
-
return '';
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function coerceRateLimitWindow(raw: any): RateLimitWindowSnapshot | null {
|
|
1121
|
-
if (!raw || typeof raw !== 'object') {
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
|
-
const usedPercent = typeof raw.usedPercent === 'number' && Number.isFinite(raw.usedPercent) ? raw.usedPercent : null;
|
|
1125
|
-
const windowMinutes =
|
|
1126
|
-
typeof raw.windowMinutes === 'number' && Number.isFinite(raw.windowMinutes) ? Math.round(raw.windowMinutes) : null;
|
|
1127
|
-
const resetsAt = typeof raw.resetsAt === 'number' && Number.isFinite(raw.resetsAt) ? Math.round(raw.resetsAt) : null;
|
|
1128
|
-
return { usedPercent, windowMinutes, resetsAt };
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function coerceRateLimitsSnapshot(raw: any): CodexRateLimitsSnapshot | null {
|
|
1132
|
-
if (!raw || typeof raw !== 'object') {
|
|
1133
|
-
return null;
|
|
1134
|
-
}
|
|
1135
|
-
const updatedAt = typeof raw.updatedAt === 'string' && raw.updatedAt.trim() ? raw.updatedAt : null;
|
|
1136
|
-
return {
|
|
1137
|
-
updatedAt,
|
|
1138
|
-
primary: coerceRateLimitWindow(raw.primary),
|
|
1139
|
-
secondary: coerceRateLimitWindow(raw.secondary)
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function clampNumber(value: number, min: number, max: number): number {
|
|
1144
|
-
return Math.min(max, Math.max(min, value));
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
function percentLeftFromUsed(usedPercent: number | null): number | null {
|
|
1148
|
-
if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent)) {
|
|
1149
|
-
return null;
|
|
1150
|
-
}
|
|
1151
|
-
return Math.round(clampNumber(100 - usedPercent, 0, 100));
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function safeParseDateMs(value: string | null): number | null {
|
|
1155
|
-
if (!value) {
|
|
1156
|
-
return null;
|
|
1157
|
-
}
|
|
1158
|
-
const ms = Date.parse(value);
|
|
1159
|
-
return Number.isFinite(ms) ? ms : null;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function formatDurationShort(ms: number): string {
|
|
1163
|
-
if (!Number.isFinite(ms)) {
|
|
1164
|
-
return '0m';
|
|
1165
|
-
}
|
|
1166
|
-
const totalMinutes = Math.max(0, Math.floor(ms / 60000));
|
|
1167
|
-
const days = Math.floor(totalMinutes / (60 * 24));
|
|
1168
|
-
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
1169
|
-
const minutes = totalMinutes % 60;
|
|
1170
|
-
if (days > 0) {
|
|
1171
|
-
return `${days}d ${hours}h`;
|
|
1172
|
-
}
|
|
1173
|
-
if (hours > 0) {
|
|
1174
|
-
return `${hours}h ${minutes}m`;
|
|
1175
|
-
}
|
|
1176
|
-
return `${minutes}m`;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function formatRunDuration(ms: number): string {
|
|
1180
|
-
if (!Number.isFinite(ms)) {
|
|
1181
|
-
return '0s';
|
|
1182
|
-
}
|
|
1183
|
-
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
1184
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1185
|
-
const seconds = totalSeconds % 60;
|
|
1186
|
-
if (minutes > 0) {
|
|
1187
|
-
return `${minutes}m ${seconds}s`;
|
|
1188
|
-
}
|
|
1189
|
-
return `${seconds}s`;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function formatResetsIn(resetsAtSec: number | null, nowMs: number): string {
|
|
1193
|
-
if (typeof resetsAtSec !== 'number' || !Number.isFinite(resetsAtSec)) {
|
|
1194
|
-
return 'Unknown';
|
|
1195
|
-
}
|
|
1196
|
-
const diffMs = resetsAtSec * 1000 - nowMs;
|
|
1197
|
-
if (diffMs <= 0) {
|
|
1198
|
-
return 'Overdue';
|
|
1199
|
-
}
|
|
1200
|
-
return formatDurationShort(diffMs);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function truncateEnd(text: string, max: number): string {
|
|
1204
|
-
if (text.length <= max) {
|
|
1205
|
-
return text;
|
|
1206
|
-
}
|
|
1207
|
-
if (max <= 3) {
|
|
1208
|
-
return text.slice(0, max);
|
|
1209
|
-
}
|
|
1210
|
-
return `${text.slice(0, max - 3)}...`;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function safeJsonPreview(value: any, maxChars = 6000): string {
|
|
1214
|
-
try {
|
|
1215
|
-
const text = JSON.stringify(value, null, 2);
|
|
1216
|
-
if (text.length <= maxChars) {
|
|
1217
|
-
return text;
|
|
1218
|
-
}
|
|
1219
|
-
return `${text.slice(0, Math.max(0, maxChars - 40))}\n... (truncated)`;
|
|
1220
|
-
} catch {
|
|
1221
|
-
return '';
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function humanizeToken(value: string): string {
|
|
1226
|
-
const input = (value || '').trim();
|
|
1227
|
-
if (!input) {
|
|
1228
|
-
return '';
|
|
1229
|
-
}
|
|
1230
|
-
return input
|
|
1231
|
-
.split(/[_-]+/)
|
|
1232
|
-
.filter(Boolean)
|
|
1233
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1234
|
-
.join(' ');
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function summarizeCodexEvent(payload: any): {
|
|
1238
|
-
progress: string;
|
|
1239
|
-
progressKind: ProgressKind;
|
|
1240
|
-
activity: Omit<ActivityItem, 'id' | 'ts'>;
|
|
1241
|
-
} {
|
|
1242
|
-
if (!payload || typeof payload !== 'object') {
|
|
1243
|
-
return {
|
|
1244
|
-
progress: 'Event',
|
|
1245
|
-
progressKind: '',
|
|
1246
|
-
activity: { category: 'event', phase: '', title: 'Event', detail: '', raw: safeJsonPreview(payload) }
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const type =
|
|
1251
|
-
(typeof payload.type === 'string' && payload.type) ||
|
|
1252
|
-
(typeof payload.event === 'string' && payload.event) ||
|
|
1253
|
-
'event';
|
|
1254
|
-
|
|
1255
|
-
const raw = safeJsonPreview(payload);
|
|
1256
|
-
|
|
1257
|
-
// Common Codex CLI shape: { type: "item.completed|item.started", item: { type: "...", ... } }
|
|
1258
|
-
if (
|
|
1259
|
-
(type === 'item.completed' || type === 'item.started') &&
|
|
1260
|
-
payload.item &&
|
|
1261
|
-
typeof payload.item === 'object'
|
|
1262
|
-
) {
|
|
1263
|
-
const item: any = payload.item;
|
|
1264
|
-
const itemType = typeof item.type === 'string' ? item.type : '';
|
|
1265
|
-
const toolName =
|
|
1266
|
-
(typeof item.tool_name === 'string' && item.tool_name) ||
|
|
1267
|
-
(typeof item.tool === 'string' && item.tool) ||
|
|
1268
|
-
(typeof item.name === 'string' && item.name) ||
|
|
1269
|
-
'';
|
|
1270
|
-
const path = (typeof item.path === 'string' && item.path) || (typeof item.filename === 'string' && item.filename) || '';
|
|
1271
|
-
const phase: ActivityPhase = type === 'item.started' ? 'started' : type === 'item.completed' ? 'completed' : '';
|
|
1272
|
-
|
|
1273
|
-
let category: ActivityCategory = 'event';
|
|
1274
|
-
let progressKind: ProgressKind = '';
|
|
1275
|
-
|
|
1276
|
-
if (itemType === 'reasoning') {
|
|
1277
|
-
category = 'reasoning';
|
|
1278
|
-
progressKind = 'reasoning';
|
|
1279
|
-
} else if (itemType === 'command_execution') {
|
|
1280
|
-
category = 'command';
|
|
1281
|
-
} else if (itemType === 'file_change') {
|
|
1282
|
-
category = 'file';
|
|
1283
|
-
} else if (toolName) {
|
|
1284
|
-
category = 'tool';
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
let title = '';
|
|
1288
|
-
let detail = '';
|
|
1289
|
-
|
|
1290
|
-
if (itemType === 'command_execution') {
|
|
1291
|
-
const commandField =
|
|
1292
|
-
item.command ?? item.cmd ?? item.shell_command ?? item.argv ?? item.args ?? item.commandLine ?? item.command_line;
|
|
1293
|
-
let commandHint = '';
|
|
1294
|
-
if (typeof commandField === 'string') {
|
|
1295
|
-
commandHint = commandField;
|
|
1296
|
-
} else if (
|
|
1297
|
-
Array.isArray(commandField) &&
|
|
1298
|
-
commandField.length > 0 &&
|
|
1299
|
-
commandField.every((token: any) => typeof token === 'string')
|
|
1300
|
-
) {
|
|
1301
|
-
commandHint = commandField.join(' ');
|
|
1302
|
-
} else {
|
|
1303
|
-
commandHint = safePreview(commandField);
|
|
1304
|
-
}
|
|
1305
|
-
const exitCode = typeof item.exit_code === 'number' ? item.exit_code : typeof item.exitCode === 'number' ? item.exitCode : null;
|
|
1306
|
-
title = phase === 'started' ? 'Running command' : phase === 'completed' ? 'Command finished' : 'Command';
|
|
1307
|
-
detail = commandHint || '';
|
|
1308
|
-
if (exitCode != null) {
|
|
1309
|
-
detail = detail ? `${detail}\n(exit ${exitCode})` : `(exit ${exitCode})`;
|
|
1310
|
-
}
|
|
1311
|
-
} else if (itemType === 'file_change') {
|
|
1312
|
-
title = phase === 'started' ? 'Update file' : phase === 'completed' ? 'File updated' : 'File change';
|
|
1313
|
-
detail = path || '';
|
|
1314
|
-
} else if (itemType === 'reasoning') {
|
|
1315
|
-
title = phase === 'started' ? 'Reasoning' : phase === 'completed' ? 'Reasoning step' : 'Reasoning';
|
|
1316
|
-
detail = toolName ? `(${toolName})` : '';
|
|
1317
|
-
} else {
|
|
1318
|
-
const base = humanizeToken(itemType) || humanizeToken(type) || 'Event';
|
|
1319
|
-
title = phase === 'started' ? `${base} started` : phase === 'completed' ? `${base} completed` : base;
|
|
1320
|
-
const detailParts: string[] = [];
|
|
1321
|
-
if (toolName) {
|
|
1322
|
-
detailParts.push(`tool: ${toolName}`);
|
|
1323
|
-
}
|
|
1324
|
-
if (path) {
|
|
1325
|
-
detailParts.push(`path: ${path}`);
|
|
1326
|
-
}
|
|
1327
|
-
detail = detailParts.join('\n');
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (!title) {
|
|
1331
|
-
title = humanizeToken(itemType) || 'Event';
|
|
1332
|
-
}
|
|
1333
|
-
detail = truncateEnd(detail, 1400);
|
|
1334
|
-
|
|
1335
|
-
const progressParts: string[] = [];
|
|
1336
|
-
if (title) {
|
|
1337
|
-
progressParts.push(title);
|
|
1338
|
-
}
|
|
1339
|
-
if (detail) {
|
|
1340
|
-
const oneLine = detail.replace(/\s+/g, ' ').trim();
|
|
1341
|
-
progressParts.push(truncateMiddle(oneLine, 200));
|
|
1342
|
-
}
|
|
1343
|
-
const progress = progressParts.join(': ') || humanizeToken(type) || 'Event';
|
|
1344
|
-
|
|
1345
|
-
return {
|
|
1346
|
-
progress,
|
|
1347
|
-
progressKind,
|
|
1348
|
-
activity: {
|
|
1349
|
-
category,
|
|
1350
|
-
phase,
|
|
1351
|
-
title,
|
|
1352
|
-
detail,
|
|
1353
|
-
raw
|
|
1354
|
-
}
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const label =
|
|
1359
|
-
(typeof payload.name === 'string' && payload.name) ||
|
|
1360
|
-
(typeof payload.label === 'string' && payload.label) ||
|
|
1361
|
-
(typeof payload.tool_name === 'string' && payload.tool_name) ||
|
|
1362
|
-
(typeof payload.tool === 'string' && payload.tool) ||
|
|
1363
|
-
'';
|
|
1364
|
-
if (label && label !== type) {
|
|
1365
|
-
const progress = `${humanizeToken(type) || type}: ${truncateMiddle(label, 120)}`;
|
|
1366
|
-
return {
|
|
1367
|
-
progress,
|
|
1368
|
-
progressKind: '',
|
|
1369
|
-
activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: label, raw }
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
const commandHint = safePreview(payload.command);
|
|
1374
|
-
if (commandHint) {
|
|
1375
|
-
const progress = `${humanizeToken(type) || type}: ${commandHint}`;
|
|
1376
|
-
return {
|
|
1377
|
-
progress,
|
|
1378
|
-
progressKind: '',
|
|
1379
|
-
activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: commandHint, raw }
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const pathHint = safePreview(payload.path);
|
|
1384
|
-
if (pathHint) {
|
|
1385
|
-
const progress = `${humanizeToken(type) || type}: ${pathHint}`;
|
|
1386
|
-
return {
|
|
1387
|
-
progress,
|
|
1388
|
-
progressKind: '',
|
|
1389
|
-
activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: pathHint, raw }
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
return {
|
|
1394
|
-
progress: humanizeToken(type) || type,
|
|
1395
|
-
progressKind: '',
|
|
1396
|
-
activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: '', raw }
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
function isNoiseCodexEvent(payload: any): boolean {
|
|
1401
|
-
if (!payload || typeof payload !== 'object') {
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
const type =
|
|
1405
|
-
(typeof payload.type === 'string' && payload.type) ||
|
|
1406
|
-
(typeof payload.event === 'string' && payload.event) ||
|
|
1407
|
-
'';
|
|
1408
|
-
// These are useful for debugging but not meaningful for end users.
|
|
1409
|
-
return type === 'thread.started' || type === 'turn.started' || type === 'turn.completed';
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
async function copyToClipboard(text: string): Promise<boolean> {
|
|
1413
|
-
if (!text) {
|
|
1414
|
-
return false;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
try {
|
|
1418
|
-
await navigator.clipboard.writeText(text);
|
|
1419
|
-
return true;
|
|
1420
|
-
} catch {
|
|
1421
|
-
// Fallback for older browser contexts.
|
|
1422
|
-
try {
|
|
1423
|
-
const textarea = document.createElement('textarea');
|
|
1424
|
-
textarea.value = text;
|
|
1425
|
-
textarea.setAttribute('readonly', 'true');
|
|
1426
|
-
textarea.style.position = 'fixed';
|
|
1427
|
-
textarea.style.left = '-9999px';
|
|
1428
|
-
document.body.appendChild(textarea);
|
|
1429
|
-
textarea.select();
|
|
1430
|
-
const ok = document.execCommand('copy');
|
|
1431
|
-
document.body.removeChild(textarea);
|
|
1432
|
-
return ok;
|
|
1433
|
-
} catch {
|
|
1434
|
-
return false;
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
async function blobToDataUrl(blob: Blob): Promise<string> {
|
|
1440
|
-
return await new Promise((resolve, reject) => {
|
|
1441
|
-
const reader = new FileReader();
|
|
1442
|
-
reader.onload = () => resolve(String(reader.result || ''));
|
|
1443
|
-
reader.onerror = () => reject(reader.error || new Error('Failed to read blob'));
|
|
1444
|
-
reader.readAsDataURL(blob);
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
type MessageBlock =
|
|
1449
|
-
| { kind: 'text'; text: string }
|
|
1450
|
-
| { kind: 'code'; lang: string; code: string };
|
|
1451
|
-
|
|
1452
|
-
function splitFencedCodeBlocks(text: string): MessageBlock[] {
|
|
1453
|
-
const blocks: MessageBlock[] = [];
|
|
1454
|
-
let rest = text;
|
|
1455
|
-
|
|
1456
|
-
while (rest.length > 0) {
|
|
1457
|
-
const fenceStart = rest.indexOf('```');
|
|
1458
|
-
if (fenceStart === -1) {
|
|
1459
|
-
blocks.push({ kind: 'text', text: rest });
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
if (fenceStart > 0) {
|
|
1464
|
-
blocks.push({ kind: 'text', text: rest.slice(0, fenceStart) });
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
const afterFence = rest.slice(fenceStart + 3);
|
|
1468
|
-
const fenceEnd = afterFence.indexOf('```');
|
|
1469
|
-
if (fenceEnd === -1) {
|
|
1470
|
-
// Unclosed fence; treat as plain text.
|
|
1471
|
-
blocks.push({ kind: 'text', text: rest.slice(fenceStart) });
|
|
1472
|
-
break;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
const raw = afterFence.slice(0, fenceEnd);
|
|
1476
|
-
rest = afterFence.slice(fenceEnd + 3);
|
|
1477
|
-
|
|
1478
|
-
let lang = '';
|
|
1479
|
-
let code = raw;
|
|
1480
|
-
const firstNewline = raw.indexOf('\n');
|
|
1481
|
-
if (firstNewline !== -1) {
|
|
1482
|
-
const candidate = raw.slice(0, firstNewline).trim();
|
|
1483
|
-
// Avoid interpreting arbitrary first lines as language labels.
|
|
1484
|
-
if (candidate.length > 0 && candidate.length <= 20 && /^[A-Za-z0-9+#_.-]+$/.test(candidate)) {
|
|
1485
|
-
lang = candidate;
|
|
1486
|
-
code = raw.slice(firstNewline + 1);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
blocks.push({ kind: 'code', lang, code: code.replace(/\n$/, '') });
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
return blocks.filter(block => (block.kind === 'text' ? block.text.length > 0 : true));
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function escapeHtml(value: string): string {
|
|
1497
|
-
return value
|
|
1498
|
-
.replace(/&/g, '&')
|
|
1499
|
-
.replace(/</g, '<')
|
|
1500
|
-
.replace(/>/g, '>')
|
|
1501
|
-
.replace(/\"/g, '"')
|
|
1502
|
-
.replace(/'/g, ''');
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
function normalizeHighlightLanguage(lang: string): string {
|
|
1506
|
-
const normalized = lang.trim().toLowerCase();
|
|
1507
|
-
if (!normalized) {
|
|
1508
|
-
return '';
|
|
1509
|
-
}
|
|
1510
|
-
const aliasMap: Record<string, string> = {
|
|
1511
|
-
js: 'javascript',
|
|
1512
|
-
jsx: 'javascript',
|
|
1513
|
-
mjs: 'javascript',
|
|
1514
|
-
cjs: 'javascript',
|
|
1515
|
-
ts: 'typescript',
|
|
1516
|
-
tsx: 'typescript',
|
|
1517
|
-
py: 'python',
|
|
1518
|
-
sh: 'bash',
|
|
1519
|
-
shell: 'bash',
|
|
1520
|
-
zsh: 'bash',
|
|
1521
|
-
yml: 'yaml',
|
|
1522
|
-
'c++': 'cpp',
|
|
1523
|
-
'c#': 'csharp',
|
|
1524
|
-
'objective-c': 'objectivec',
|
|
1525
|
-
objc: 'objectivec'
|
|
1526
|
-
};
|
|
1527
|
-
return aliasMap[normalized] ?? normalized;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function renderHighlightedCodeToSafeHtml(code: string, lang: string): string {
|
|
1531
|
-
const fallback = escapeHtml(code);
|
|
1532
|
-
let highlighted = fallback;
|
|
1533
|
-
try {
|
|
1534
|
-
const normalizedLang = normalizeHighlightLanguage(lang);
|
|
1535
|
-
if (normalizedLang && hljs.getLanguage(normalizedLang)) {
|
|
1536
|
-
highlighted = hljs.highlight(code, { language: normalizedLang, ignoreIllegals: true }).value;
|
|
1537
|
-
} else {
|
|
1538
|
-
highlighted = hljs.highlightAuto(code).value;
|
|
1539
|
-
}
|
|
1540
|
-
} catch {
|
|
1541
|
-
highlighted = fallback;
|
|
1542
|
-
}
|
|
1543
|
-
try {
|
|
1544
|
-
return DOMPurify.sanitize(highlighted, { USE_PROFILES: { html: true } });
|
|
1545
|
-
} catch {
|
|
1546
|
-
return highlighted;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function renderMarkdownToSafeHtml(markdown: string): string {
|
|
1551
|
-
if (!markdown) {
|
|
1552
|
-
return '';
|
|
1553
|
-
}
|
|
1554
|
-
const normalizedMarkdown = normalizeMathDelimiters(markdown);
|
|
1555
|
-
let html = '';
|
|
1556
|
-
try {
|
|
1557
|
-
html = marked.parse(normalizedMarkdown, {
|
|
1558
|
-
gfm: true,
|
|
1559
|
-
breaks: true
|
|
1560
|
-
}) as string;
|
|
1561
|
-
} catch {
|
|
1562
|
-
return escapeHtml(markdown).replace(/\n/g, '<br />');
|
|
1563
|
-
}
|
|
1564
|
-
try {
|
|
1565
|
-
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
|
|
1566
|
-
} catch {
|
|
1567
|
-
return html;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
function normalizeMathDelimiters(markdown: string): string {
|
|
1572
|
-
const normalizedEscapedBlock = markdown.replace(
|
|
1573
|
-
/(^|[^\\])\\\[([\s\S]*?)\\\]/g,
|
|
1574
|
-
(_match, prefix: string, body: string) => `${prefix}\n$$\n${body}\n$$\n`
|
|
1575
|
-
);
|
|
1576
|
-
|
|
1577
|
-
const normalizedEscapedInline = normalizedEscapedBlock.replace(
|
|
1578
|
-
/(^|[^\\])\\\((.+?)\\\)/g,
|
|
1579
|
-
(_match, prefix: string, body: string) => `${prefix}$${body}$`
|
|
1580
|
-
);
|
|
1581
|
-
|
|
1582
|
-
// Some model outputs arrive with bare bracket delimiters:
|
|
1583
|
-
// [
|
|
1584
|
-
// ...latex...
|
|
1585
|
-
// ]
|
|
1586
|
-
// Promote those to $$...$$ when the body looks like math.
|
|
1587
|
-
return normalizedEscapedInline.replace(
|
|
1588
|
-
/(^|\n)[ \t]*\[\s*\n([\s\S]*?)\n[ \t]*\](?=\n|$)/g,
|
|
1589
|
-
(_match, prefix: string, body: string) => {
|
|
1590
|
-
const trimmed = body.trim();
|
|
1591
|
-
if (!looksLikeLatexMath(trimmed)) {
|
|
1592
|
-
return `${prefix}[\n${body}\n]`;
|
|
1593
|
-
}
|
|
1594
|
-
return `${prefix}\n$$\n${trimmed}\n$$\n`;
|
|
1595
|
-
}
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function looksLikeLatexMath(value: string): boolean {
|
|
1600
|
-
if (!value) {
|
|
1601
|
-
return false;
|
|
1602
|
-
}
|
|
1603
|
-
return /\\[A-Za-z]+/.test(value) || /[_^{}]/.test(value);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
function StatusPill(props: { status: PanelStatus }): JSX.Element {
|
|
1607
|
-
// Visual: dot only. Keep an accessible label + tooltip for status.
|
|
1608
|
-
const label =
|
|
1609
|
-
props.status === 'disconnected' ? 'Disconnected' : props.status === 'running' ? 'Running' : 'Ready';
|
|
1610
|
-
return (
|
|
1611
|
-
<span
|
|
1612
|
-
className={`jp-CodexStatusPill jp-CodexStatusPill-${props.status}`}
|
|
1613
|
-
role="img"
|
|
1614
|
-
aria-label={label}
|
|
1615
|
-
title={label}
|
|
1616
|
-
>
|
|
1617
|
-
<span className="jp-CodexStatusPill-dot" aria-hidden="true" />
|
|
1618
|
-
</span>
|
|
1619
|
-
);
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
function CodeBlock(props: { lang: string; code: string; canCopy: boolean }): JSX.Element {
|
|
1623
|
-
const [copied, setCopied] = useState(false);
|
|
1624
|
-
const highlightedCode = useMemo(
|
|
1625
|
-
() => renderHighlightedCodeToSafeHtml(props.code, props.lang),
|
|
1626
|
-
[props.code, props.lang]
|
|
1627
|
-
);
|
|
1628
|
-
|
|
1629
|
-
async function onCopy(): Promise<void> {
|
|
1630
|
-
const ok = await copyToClipboard(props.code);
|
|
1631
|
-
if (!ok) {
|
|
1632
|
-
return;
|
|
1633
|
-
}
|
|
1634
|
-
setCopied(true);
|
|
1635
|
-
window.setTimeout(() => setCopied(false), 900);
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
return (
|
|
1639
|
-
<div className="jp-CodexCodeBlockWrap">
|
|
1640
|
-
<div className="jp-CodexCodeBlockBar">
|
|
1641
|
-
<span className="jp-CodexCodeBlockMeta">
|
|
1642
|
-
{props.lang ? <span className="jp-CodexCodeBlockLang">{props.lang}</span> : <span />}
|
|
1643
|
-
</span>
|
|
1644
|
-
{props.canCopy && (
|
|
1645
|
-
<button
|
|
1646
|
-
className="jp-CodexBtn jp-CodexBtn-ghost jp-CodexBtn-xs jp-CodexCodeBlockCopyBtn"
|
|
1647
|
-
onClick={() => void onCopy()}
|
|
1648
|
-
>
|
|
1649
|
-
{copied ? 'Copied' : 'Copy'}
|
|
1650
|
-
</button>
|
|
1651
|
-
)}
|
|
1652
|
-
</div>
|
|
1653
|
-
<pre className="jp-CodexCodeBlock">
|
|
1654
|
-
<code className="hljs" dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
|
1655
|
-
</pre>
|
|
1656
|
-
</div>
|
|
1657
|
-
);
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
function MessageText(props: { text: string; canCopyCode?: boolean }): JSX.Element {
|
|
1661
|
-
const blocks = splitFencedCodeBlocks(props.text);
|
|
1662
|
-
return (
|
|
1663
|
-
<div className="jp-CodexChat-text">
|
|
1664
|
-
{blocks.map((block, idx) => {
|
|
1665
|
-
if (block.kind === 'code') {
|
|
1666
|
-
return <CodeBlock key={idx} lang={block.lang} code={block.code} canCopy={Boolean(props.canCopyCode)} />;
|
|
1667
|
-
}
|
|
1668
|
-
const html = renderMarkdownToSafeHtml(block.text);
|
|
1669
|
-
return <div key={idx} className="jp-CodexMarkdown" dangerouslySetInnerHTML={{ __html: html }} />;
|
|
1670
|
-
})}
|
|
1671
|
-
</div>
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
function CodexChat(props: CodexChatProps): JSX.Element {
|
|
1676
|
-
const [sessions, setSessions] = useState<Map<string, NotebookSession>>(new Map());
|
|
1677
|
-
const sessionsRef = useRef<Map<string, NotebookSession>>(new Map());
|
|
1678
|
-
const [currentNotebookPath, setCurrentNotebookPath] = useState<string>('');
|
|
1679
|
-
const currentNotebookPathRef = useRef<string>('');
|
|
1680
|
-
const [currentNotebookSessionKey, setCurrentNotebookSessionKey] = useState<string>('');
|
|
1681
|
-
const currentNotebookSessionKeyRef = useRef<string>('');
|
|
1682
|
-
const [cliDefaults, setCliDefaults] = useState<CliDefaultsSnapshot>({ model: null, reasoningEffort: null });
|
|
1683
|
-
const [modelOptions, setModelOptions] = useState<ModelOption[]>(() => FALLBACK_MODEL_OPTIONS);
|
|
1684
|
-
const [modelOption, setModelOption] = useState<ModelOptionValue>(() => {
|
|
1685
|
-
const savedModel = readStoredModel();
|
|
1686
|
-
if (savedModel && savedModel !== '__config__' && savedModel !== '__custom__') {
|
|
1687
|
-
return savedModel;
|
|
1688
|
-
}
|
|
1689
|
-
return '__config__';
|
|
1690
|
-
});
|
|
1691
|
-
const [reasoningEffort, setReasoningEffort] = useState<ReasoningOptionValue>(() =>
|
|
1692
|
-
readStoredReasoningEffort()
|
|
1693
|
-
);
|
|
1694
|
-
const [sandboxMode, setSandboxMode] = useState<SandboxMode>(() => readStoredSandboxMode());
|
|
1695
|
-
const [commandPath, setCommandPath] = useState<string>(() => readStoredCommandPath());
|
|
1696
|
-
const [autoSaveBeforeSend, setAutoSaveBeforeSend] = useState<boolean>(() => readStoredAutoSave());
|
|
1697
|
-
const [includeActiveCell, setIncludeActiveCell] = useState<boolean>(() => readStoredIncludeActiveCell());
|
|
1698
|
-
const [includeActiveCellOutput, setIncludeActiveCellOutput] = useState<boolean>(() =>
|
|
1699
|
-
readStoredIncludeActiveCellOutput()
|
|
1700
|
-
);
|
|
1701
|
-
const [notifyOnDone, setNotifyOnDone] = useState<boolean>(() => readStoredNotifyOnDone());
|
|
1702
|
-
const [notifyOnDoneMinSeconds, setNotifyOnDoneMinSeconds] = useState<number>(() => readStoredNotifyOnDoneMinSeconds());
|
|
1703
|
-
const [settingsOpen, setSettingsOpen] = useState<boolean>(() => readStoredSettingsOpen());
|
|
1704
|
-
const [input, setInput] = useState('');
|
|
1705
|
-
const [pendingImages, setPendingImages] = useState<PendingImageAttachment[]>([]);
|
|
1706
|
-
const pendingImagesRef = useRef<PendingImageAttachment[]>([]);
|
|
1707
|
-
const inputRef = useRef<string>('');
|
|
1708
|
-
const inputDraftsRef = useRef<Map<string, string>>(new Map());
|
|
1709
|
-
const [socketConnected, setSocketConnected] = useState(false);
|
|
1710
|
-
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
1711
|
-
const [reconnectCounter, setReconnectCounter] = useState(0);
|
|
1712
|
-
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
1713
|
-
const [modelMenuOpen, setModelMenuOpen] = useState(false);
|
|
1714
|
-
const [reasoningMenuOpen, setReasoningMenuOpen] = useState(false);
|
|
1715
|
-
const [usagePopoverOpen, setUsagePopoverOpen] = useState(false);
|
|
1716
|
-
const [permissionMenuOpen, setPermissionMenuOpen] = useState(false);
|
|
1717
|
-
const [rateLimits, setRateLimits] = useState<CodexRateLimitsSnapshot | null>(null);
|
|
1718
|
-
const wsRef = useRef<WebSocket | null>(null);
|
|
1719
|
-
const runToSessionKeyRef = useRef<Map<string, string>>(new Map());
|
|
1720
|
-
const activeSessionKeyByPathRef = useRef<Map<string, string>>(new Map());
|
|
1721
|
-
const sessionThreadSyncIdRef = useRef<string>(createSessionEventId());
|
|
1722
|
-
const lastRateLimitsRefreshRef = useRef<number>(0);
|
|
1723
|
-
const pendingRefreshPathsRef = useRef<Set<string>>(new Set());
|
|
1724
|
-
const notifyOnDoneRef = useRef<boolean>(notifyOnDone);
|
|
1725
|
-
const notifyOnDoneMinSecondsRef = useRef<number>(notifyOnDoneMinSeconds);
|
|
1726
|
-
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
1727
|
-
const endRef = useRef<HTMLDivElement | null>(null);
|
|
1728
|
-
const modelMenuWrapRef = useRef<HTMLDivElement | null>(null);
|
|
1729
|
-
const reasoningMenuWrapRef = useRef<HTMLDivElement | null>(null);
|
|
1730
|
-
const usageMenuWrapRef = useRef<HTMLDivElement | null>(null);
|
|
1731
|
-
const permissionMenuWrapRef = useRef<HTMLDivElement | null>(null);
|
|
1732
|
-
const modelBtnRef = useRef<HTMLButtonElement>(null);
|
|
1733
|
-
const modelPopoverRef = useRef<HTMLDivElement>(null);
|
|
1734
|
-
const reasoningBtnRef = useRef<HTMLButtonElement>(null);
|
|
1735
|
-
const reasoningPopoverRef = useRef<HTMLDivElement>(null);
|
|
1736
|
-
const usageBtnRef = useRef<HTMLButtonElement>(null);
|
|
1737
|
-
const usagePopoverRef = useRef<HTMLDivElement>(null);
|
|
1738
|
-
const permissionBtnRef = useRef<HTMLButtonElement>(null);
|
|
1739
|
-
const permissionPopoverRef = useRef<HTMLDivElement>(null);
|
|
1740
|
-
const notebookLabelRef = useRef<HTMLSpanElement | null>(null);
|
|
1741
|
-
const [isNotebookLabelTruncated, setIsNotebookLabelTruncated] = useState(false);
|
|
1742
|
-
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1743
|
-
const storedThreadCount = useMemo<number>(() => getStoredSessionThreadCount(), [sessions]);
|
|
1744
|
-
const selectedModel = modelOption === '__config__' ? '' : modelOption;
|
|
1745
|
-
const selectedReasoningEffort = reasoningEffort === '__config__' ? '' : reasoningEffort;
|
|
1746
|
-
const autoModel = cliDefaults.model;
|
|
1747
|
-
const autoReasoningEffort = cliDefaults.reasoningEffort;
|
|
1748
|
-
const reasoningModel = modelOption === '__config__' ? (autoModel || '') : modelOption;
|
|
1749
|
-
const reasoningOptions = useMemo<ReasoningOption[]>(
|
|
1750
|
-
() => buildReasoningOptions(cliDefaults.availableModels, reasoningModel),
|
|
1751
|
-
[cliDefaults.availableModels, reasoningModel]
|
|
1752
|
-
);
|
|
1753
|
-
|
|
1754
|
-
useEffect(() => {
|
|
1755
|
-
const dynamicModelOptions = readModelOptions(cliDefaults.availableModels);
|
|
1756
|
-
setModelOptions(dynamicModelOptions);
|
|
1757
|
-
}, [cliDefaults.availableModels]);
|
|
1758
|
-
|
|
1759
|
-
useEffect(() => {
|
|
1760
|
-
if (reasoningEffort === '__config__') {
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
if (!reasoningOptions.some(option => option.value === reasoningEffort)) {
|
|
1764
|
-
setReasoningEffort('__config__');
|
|
1765
|
-
}
|
|
1766
|
-
}, [reasoningEffort, reasoningOptions]);
|
|
1767
|
-
|
|
1768
|
-
useEffect(() => {
|
|
1769
|
-
if (modelOption === '__config__') {
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
if (!modelOptions.some(option => option.value === modelOption)) {
|
|
1773
|
-
setModelOption('__config__');
|
|
1774
|
-
}
|
|
1775
|
-
}, [modelOption, modelOptions]);
|
|
1776
|
-
|
|
1777
|
-
function saveCurrentInputDraft(value: string): void {
|
|
1778
|
-
const sessionKey = currentNotebookSessionKeyRef.current || '';
|
|
1779
|
-
if (!sessionKey) {
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
const nextDrafts = new Map(inputDraftsRef.current);
|
|
1784
|
-
if (value) {
|
|
1785
|
-
nextDrafts.set(sessionKey, value);
|
|
1786
|
-
} else {
|
|
1787
|
-
nextDrafts.delete(sessionKey);
|
|
1788
|
-
}
|
|
1789
|
-
inputDraftsRef.current = nextDrafts;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
function clearInputForCurrentSession(): void {
|
|
1793
|
-
const sessionKey = currentNotebookSessionKeyRef.current || '';
|
|
1794
|
-
if (!sessionKey) {
|
|
1795
|
-
setInput('');
|
|
1796
|
-
inputRef.current = '';
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
saveCurrentInputDraft('');
|
|
1800
|
-
setInput('');
|
|
1801
|
-
inputRef.current = '';
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
function updateInput(nextValue: string): void {
|
|
1805
|
-
saveCurrentInputDraft(nextValue);
|
|
1806
|
-
setInput(nextValue);
|
|
1807
|
-
inputRef.current = nextValue;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function restoreInput(sessionKey: string): void {
|
|
1811
|
-
const restoredInput = sessionKey ? inputDraftsRef.current.get(sessionKey) || '' : '';
|
|
1812
|
-
inputRef.current = restoredInput;
|
|
1813
|
-
setInput(restoredInput);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
useEffect(() => {
|
|
1817
|
-
inputRef.current = input;
|
|
1818
|
-
}, [input]);
|
|
1819
|
-
|
|
1820
|
-
useEffect(() => {
|
|
1821
|
-
sessionsRef.current = sessions;
|
|
1822
|
-
currentNotebookSessionKeyRef.current = currentNotebookSessionKey;
|
|
1823
|
-
}, [sessions]);
|
|
1824
|
-
|
|
1825
|
-
useEffect(() => {
|
|
1826
|
-
currentNotebookSessionKeyRef.current = currentNotebookSessionKey;
|
|
1827
|
-
persistStoredSessionThreads(sessions);
|
|
1828
|
-
}, [sessions, currentNotebookSessionKey]);
|
|
1829
|
-
|
|
1830
|
-
useEffect(() => {
|
|
1831
|
-
pendingImagesRef.current = pendingImages;
|
|
1832
|
-
}, [pendingImages]);
|
|
1833
|
-
|
|
1834
|
-
useEffect(() => {
|
|
1835
|
-
return () => {
|
|
1836
|
-
// Avoid leaking blob URLs if the panel unmounts.
|
|
1837
|
-
for (const image of pendingImagesRef.current) {
|
|
1838
|
-
URL.revokeObjectURL(image.previewUrl);
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1841
|
-
}, []);
|
|
1842
|
-
|
|
1843
|
-
useEffect(() => {
|
|
1844
|
-
persistModel(selectedModel);
|
|
1845
|
-
}, [selectedModel]);
|
|
1846
|
-
|
|
1847
|
-
useEffect(() => {
|
|
1848
|
-
persistAutoSave(autoSaveBeforeSend);
|
|
1849
|
-
}, [autoSaveBeforeSend]);
|
|
1850
|
-
|
|
1851
|
-
useEffect(() => {
|
|
1852
|
-
persistIncludeActiveCell(includeActiveCell);
|
|
1853
|
-
}, [includeActiveCell]);
|
|
1854
|
-
|
|
1855
|
-
useEffect(() => {
|
|
1856
|
-
// If the user enables "Include active cell" for the first time, default output to ON as well.
|
|
1857
|
-
if (includeActiveCell && !includeActiveCellOutput && !hasStoredValue(INCLUDE_ACTIVE_CELL_OUTPUT_STORAGE_KEY)) {
|
|
1858
|
-
setIncludeActiveCellOutput(true);
|
|
1859
|
-
}
|
|
1860
|
-
}, [includeActiveCell, includeActiveCellOutput]);
|
|
1861
|
-
|
|
1862
|
-
useEffect(() => {
|
|
1863
|
-
persistIncludeActiveCellOutput(includeActiveCellOutput);
|
|
1864
|
-
}, [includeActiveCellOutput]);
|
|
1865
|
-
|
|
1866
|
-
useEffect(() => {
|
|
1867
|
-
persistCommandPath(commandPath);
|
|
1868
|
-
}, [commandPath]);
|
|
1869
|
-
|
|
1870
|
-
useEffect(() => {
|
|
1871
|
-
persistReasoningEffort(reasoningEffort);
|
|
1872
|
-
}, [reasoningEffort]);
|
|
1873
|
-
|
|
1874
|
-
useEffect(() => {
|
|
1875
|
-
persistSandboxMode(sandboxMode);
|
|
1876
|
-
}, [sandboxMode]);
|
|
1877
|
-
|
|
1878
|
-
useEffect(() => {
|
|
1879
|
-
persistSettingsOpen(settingsOpen);
|
|
1880
|
-
}, [settingsOpen]);
|
|
1881
|
-
|
|
1882
|
-
useEffect(() => {
|
|
1883
|
-
notifyOnDoneRef.current = notifyOnDone;
|
|
1884
|
-
persistNotifyOnDone(notifyOnDone);
|
|
1885
|
-
}, [notifyOnDone]);
|
|
1886
|
-
|
|
1887
|
-
useEffect(() => {
|
|
1888
|
-
const normalized = Number.isFinite(notifyOnDoneMinSeconds) ? Math.max(0, Math.floor(notifyOnDoneMinSeconds)) : 0;
|
|
1889
|
-
notifyOnDoneMinSecondsRef.current = normalized;
|
|
1890
|
-
persistNotifyOnDoneMinSeconds(normalized);
|
|
1891
|
-
}, [notifyOnDoneMinSeconds]);
|
|
1892
|
-
|
|
1893
|
-
useEffect(() => {
|
|
1894
|
-
if (!modelMenuOpen && !reasoningMenuOpen && !usagePopoverOpen && !permissionMenuOpen) {
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
const onPointerDown = (event: PointerEvent) => {
|
|
1899
|
-
const target = event.target as Node | null;
|
|
1900
|
-
if (!target) {
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
const inModel = modelMenuWrapRef.current?.contains(target) ?? false;
|
|
1905
|
-
const inReasoning = reasoningMenuWrapRef.current?.contains(target) ?? false;
|
|
1906
|
-
const inUsage = usageMenuWrapRef.current?.contains(target) ?? false;
|
|
1907
|
-
const inPermission = permissionMenuWrapRef.current?.contains(target) ?? false;
|
|
1908
|
-
const inModelPopover = modelPopoverRef.current?.contains(target) ?? false;
|
|
1909
|
-
const inReasoningPopover = reasoningPopoverRef.current?.contains(target) ?? false;
|
|
1910
|
-
const inUsagePopover = usagePopoverRef.current?.contains(target) ?? false;
|
|
1911
|
-
const inPermissionPopover = permissionPopoverRef.current?.contains(target) ?? false;
|
|
1912
|
-
if (
|
|
1913
|
-
inModel ||
|
|
1914
|
-
inReasoning ||
|
|
1915
|
-
inUsage ||
|
|
1916
|
-
inPermission ||
|
|
1917
|
-
inModelPopover ||
|
|
1918
|
-
inReasoningPopover ||
|
|
1919
|
-
inUsagePopover ||
|
|
1920
|
-
inPermissionPopover
|
|
1921
|
-
) {
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
setModelMenuOpen(false);
|
|
1926
|
-
setReasoningMenuOpen(false);
|
|
1927
|
-
setUsagePopoverOpen(false);
|
|
1928
|
-
setPermissionMenuOpen(false);
|
|
1929
|
-
};
|
|
1930
|
-
|
|
1931
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
1932
|
-
if (event.key !== 'Escape') {
|
|
1933
|
-
return;
|
|
1934
|
-
}
|
|
1935
|
-
event.preventDefault();
|
|
1936
|
-
setModelMenuOpen(false);
|
|
1937
|
-
setReasoningMenuOpen(false);
|
|
1938
|
-
setUsagePopoverOpen(false);
|
|
1939
|
-
setPermissionMenuOpen(false);
|
|
1940
|
-
};
|
|
1941
|
-
|
|
1942
|
-
window.addEventListener('pointerdown', onPointerDown, true);
|
|
1943
|
-
window.addEventListener('keydown', onKeyDown);
|
|
1944
|
-
return () => {
|
|
1945
|
-
window.removeEventListener('pointerdown', onPointerDown, true);
|
|
1946
|
-
window.removeEventListener('keydown', onKeyDown);
|
|
1947
|
-
};
|
|
1948
|
-
}, [modelMenuOpen, reasoningMenuOpen, usagePopoverOpen, permissionMenuOpen]);
|
|
1949
|
-
|
|
1950
|
-
function replaceSessions(next: Map<string, NotebookSession>): void {
|
|
1951
|
-
sessionsRef.current = next;
|
|
1952
|
-
setSessions(next);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function updateSessions(
|
|
1956
|
-
updater: (prev: Map<string, NotebookSession>) => Map<string, NotebookSession>
|
|
1957
|
-
): void {
|
|
1958
|
-
setSessions(prev => {
|
|
1959
|
-
const next = updater(prev);
|
|
1960
|
-
sessionsRef.current = next;
|
|
1961
|
-
return next;
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
function resolveSessionKey(path: string): string {
|
|
1966
|
-
const normalizedPath = path || '';
|
|
1967
|
-
return makeSessionKey(normalizedPath);
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
function resolveCurrentSessionKey(path: string): string {
|
|
1971
|
-
return resolveSessionKey(path);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
function ensureSession(path: string, sessionKey?: string): NotebookSession {
|
|
1975
|
-
const normalizedPath = path || '';
|
|
1976
|
-
const effectiveSessionKey = sessionKey || resolveCurrentSessionKey(normalizedPath);
|
|
1977
|
-
const existing = sessionsRef.current.get(effectiveSessionKey);
|
|
1978
|
-
if (existing) {
|
|
1979
|
-
return existing;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
const created = createSession(normalizedPath, `Session started`, { sessionKey: effectiveSessionKey });
|
|
1983
|
-
const next = new Map(sessionsRef.current);
|
|
1984
|
-
next.set(effectiveSessionKey, created);
|
|
1985
|
-
activeSessionKeyByPathRef.current.set(normalizedPath, effectiveSessionKey);
|
|
1986
|
-
replaceSessions(next);
|
|
1987
|
-
return created;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
function requestRateLimitsRefresh(force = false): void {
|
|
1991
|
-
const socket = wsRef.current;
|
|
1992
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
const now = Date.now();
|
|
1997
|
-
if (!force && now - lastRateLimitsRefreshRef.current < 15000) {
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
lastRateLimitsRefreshRef.current = now;
|
|
2001
|
-
|
|
2002
|
-
socket.send(JSON.stringify({ type: 'refresh_rate_limits' }));
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
function toggleUsagePopover(): void {
|
|
2006
|
-
setUsagePopoverOpen(open => {
|
|
2007
|
-
const next = !open;
|
|
2008
|
-
if (next) {
|
|
2009
|
-
requestRateLimitsRefresh();
|
|
2010
|
-
}
|
|
2011
|
-
return next;
|
|
2012
|
-
});
|
|
2013
|
-
setModelMenuOpen(false);
|
|
2014
|
-
setReasoningMenuOpen(false);
|
|
2015
|
-
setPermissionMenuOpen(false);
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
async function updateNotifyOnDone(enabled: boolean): Promise<void> {
|
|
2019
|
-
if (!enabled) {
|
|
2020
|
-
setNotifyOnDone(false);
|
|
2021
|
-
return;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
const permission = getBrowserNotificationPermission();
|
|
2025
|
-
const systemSessionKey = currentNotebookSessionKeyRef.current || '';
|
|
2026
|
-
if (permission === 'unsupported') {
|
|
2027
|
-
appendMessage(systemSessionKey, 'system', 'Browser notifications are not supported in this environment.');
|
|
2028
|
-
setNotifyOnDone(false);
|
|
2029
|
-
return;
|
|
2030
|
-
}
|
|
2031
|
-
if (permission === 'granted') {
|
|
2032
|
-
setNotifyOnDone(true);
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
if (permission === 'denied') {
|
|
2036
|
-
appendMessage(
|
|
2037
|
-
systemSessionKey,
|
|
2038
|
-
'system',
|
|
2039
|
-
'Browser notifications are blocked for this site. Allow notifications in browser settings to enable this option.'
|
|
2040
|
-
);
|
|
2041
|
-
setNotifyOnDone(false);
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
try {
|
|
2046
|
-
const requested = await window.Notification.requestPermission();
|
|
2047
|
-
if (requested === 'granted') {
|
|
2048
|
-
setNotifyOnDone(true);
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
if (requested === 'denied') {
|
|
2052
|
-
appendMessage(
|
|
2053
|
-
systemSessionKey,
|
|
2054
|
-
'system',
|
|
2055
|
-
'Browser notification permission was denied. Allow notifications in browser settings to enable this option.'
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
setNotifyOnDone(false);
|
|
2059
|
-
} catch {
|
|
2060
|
-
appendMessage(systemSessionKey, 'system', 'Failed to request browser notification permission.');
|
|
2061
|
-
setNotifyOnDone(false);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
function getRunDurationMs(sessionKey: string): number | null {
|
|
2066
|
-
if (!sessionKey) {
|
|
2067
|
-
return null;
|
|
2068
|
-
}
|
|
2069
|
-
const session = sessionsRef.current.get(sessionKey);
|
|
2070
|
-
if (!session || typeof session.runStartedAt !== 'number' || !Number.isFinite(session.runStartedAt)) {
|
|
2071
|
-
return null;
|
|
2072
|
-
}
|
|
2073
|
-
return Math.max(0, Date.now() - session.runStartedAt);
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
function formatElapsedForNotification(elapsedMs: number): string {
|
|
2077
|
-
const totalSeconds = Math.round(Math.max(0, elapsedMs) / 1000);
|
|
2078
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
2079
|
-
const seconds = totalSeconds % 60;
|
|
2080
|
-
if (minutes === 0) {
|
|
2081
|
-
return `${totalSeconds}s`;
|
|
2082
|
-
}
|
|
2083
|
-
return `${minutes}m ${seconds}s`;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
function notifyRunDone(sessionKey: string, notebookPath: string, cancelled: boolean, exitCode: number | null): void {
|
|
2087
|
-
if (!notifyOnDoneRef.current) {
|
|
2088
|
-
return;
|
|
2089
|
-
}
|
|
2090
|
-
const permission = getBrowserNotificationPermission();
|
|
2091
|
-
if (permission !== 'granted' || typeof window === 'undefined' || typeof window.Notification === 'undefined') {
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
const elapsedMs = getRunDurationMs(sessionKey);
|
|
2096
|
-
const minimumMs = notifyOnDoneMinSecondsRef.current * 1000;
|
|
2097
|
-
if (minimumMs > 0 && (elapsedMs === null || elapsedMs < minimumMs)) {
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
const parsed = parseSessionKey(sessionKey);
|
|
2102
|
-
const pathLabel = parsed.path || notebookPath || currentNotebookPathRef.current || 'current notebook';
|
|
2103
|
-
const pathSummary = truncateMiddle(pathLabel, 120);
|
|
2104
|
-
const elapsedText = elapsedMs === null ? '' : ` (${formatElapsedForNotification(elapsedMs)} elapsed)`;
|
|
2105
|
-
const body = cancelled
|
|
2106
|
-
? `Run cancelled in ${pathSummary}${elapsedText}`
|
|
2107
|
-
: exitCode === null || exitCode === 0
|
|
2108
|
-
? `Run completed in ${pathSummary}${elapsedText}`
|
|
2109
|
-
: `Run failed (exit ${exitCode}) in ${pathSummary}${elapsedText}`;
|
|
2110
|
-
|
|
2111
|
-
try {
|
|
2112
|
-
const notification = new window.Notification('Codex run finished', {
|
|
2113
|
-
body,
|
|
2114
|
-
tag: sessionKey ? `codex-done-${sessionKey}` : undefined
|
|
2115
|
-
});
|
|
2116
|
-
window.setTimeout(() => notification.close(), 12000);
|
|
2117
|
-
} catch {
|
|
2118
|
-
// Ignore failures; completion is still shown in the panel.
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
function emitSessionThreadEvent(sessionKey: string, notebookPath: string, threadId: string): void {
|
|
2123
|
-
const source = sessionThreadSyncIdRef.current;
|
|
2124
|
-
const payload: SessionThreadSyncEvent = {
|
|
2125
|
-
kind: 'new-thread',
|
|
2126
|
-
sessionKey,
|
|
2127
|
-
notebookPath,
|
|
2128
|
-
threadId,
|
|
2129
|
-
source,
|
|
2130
|
-
id: createSessionEventId(),
|
|
2131
|
-
issuedAt: Date.now()
|
|
2132
|
-
};
|
|
2133
|
-
try {
|
|
2134
|
-
window.localStorage.setItem(SESSION_THREADS_EVENT_KEY, JSON.stringify(payload));
|
|
2135
|
-
} catch {
|
|
2136
|
-
// Ignore sync write failures; local tab still updates immediately.
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
function sendStartSession(
|
|
2141
|
-
session: NotebookSession,
|
|
2142
|
-
notebookPath: string,
|
|
2143
|
-
sessionKey: string,
|
|
2144
|
-
options?: { forceNewThread?: boolean }
|
|
2145
|
-
): void {
|
|
2146
|
-
const socket = wsRef.current;
|
|
2147
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
2148
|
-
return;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
socket.send(
|
|
2152
|
-
JSON.stringify({
|
|
2153
|
-
type: 'start_session',
|
|
2154
|
-
sessionId: session.threadId,
|
|
2155
|
-
notebookPath,
|
|
2156
|
-
sessionContextKey: sessionKey,
|
|
2157
|
-
forceNewThread: options?.forceNewThread === true
|
|
2158
|
-
})
|
|
2159
|
-
);
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
function deleteAllSessionsOnServer(): boolean {
|
|
2163
|
-
const socket = wsRef.current;
|
|
2164
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
2165
|
-
return false;
|
|
2166
|
-
}
|
|
2167
|
-
try {
|
|
2168
|
-
socket.send(JSON.stringify({ type: 'delete_all_sessions' }));
|
|
2169
|
-
return true;
|
|
2170
|
-
} catch {
|
|
2171
|
-
return false;
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
function setSessionRunState(sessionKey: string, runState: RunState, runId: string | null): void {
|
|
2176
|
-
if (!sessionKey) {
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
const now = Date.now();
|
|
2181
|
-
updateSessions(prev => {
|
|
2182
|
-
const next = new Map(prev);
|
|
2183
|
-
const existing = next.get(sessionKey) ?? createSession('', `Session started`, { sessionKey });
|
|
2184
|
-
const session = next.get(sessionKey) ?? existing;
|
|
2185
|
-
let messages = session.messages;
|
|
2186
|
-
let runStartedAt = session.runStartedAt;
|
|
2187
|
-
|
|
2188
|
-
if (runState === 'running' && session.runState !== 'running') {
|
|
2189
|
-
runStartedAt = now;
|
|
2190
|
-
}
|
|
2191
|
-
if (runState === 'ready' && session.runState === 'running') {
|
|
2192
|
-
const startedAt = runStartedAt;
|
|
2193
|
-
if (typeof startedAt === 'number' && Number.isFinite(startedAt)) {
|
|
2194
|
-
const elapsedMs = Math.max(0, now - startedAt);
|
|
2195
|
-
messages = [...messages, { kind: 'run-divider', id: crypto.randomUUID(), elapsedMs }];
|
|
2196
|
-
}
|
|
2197
|
-
runStartedAt = null;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
const progress = session.runState === runState ? session.progress : '';
|
|
2201
|
-
const progressKind = session.runState === runState ? session.progressKind : '';
|
|
2202
|
-
next.set(sessionKey, { ...session, messages, runState, activeRunId: runId, runStartedAt, progress, progressKind });
|
|
2203
|
-
return next;
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
function setSessionProgress(sessionKey: string, progress: string, kind: ProgressKind = ''): void {
|
|
2208
|
-
const targetSessionKey = sessionKey || currentNotebookSessionKeyRef.current || '';
|
|
2209
|
-
if (!targetSessionKey) {
|
|
2210
|
-
return;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
const nextProgress = progress ? truncateMiddle(progress, 260) : '';
|
|
2214
|
-
const nextKind: ProgressKind = nextProgress ? kind : '';
|
|
2215
|
-
updateSessions(prev => {
|
|
2216
|
-
const next = new Map(prev);
|
|
2217
|
-
const session =
|
|
2218
|
-
next.get(targetSessionKey) ?? createSession('', `Session started`, { sessionKey: targetSessionKey });
|
|
2219
|
-
if (session.progress === nextProgress && session.progressKind === nextKind) {
|
|
2220
|
-
return prev;
|
|
2221
|
-
}
|
|
2222
|
-
next.set(targetSessionKey, { ...session, progress: nextProgress, progressKind: nextKind });
|
|
2223
|
-
return next;
|
|
2224
|
-
});
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
function appendActivityItem(sessionKey: string, item: Omit<ActivityItem, 'id' | 'ts'>): void {
|
|
2228
|
-
const targetSessionKey = sessionKey || currentNotebookSessionKeyRef.current || '';
|
|
2229
|
-
if (!targetSessionKey) {
|
|
2230
|
-
return;
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
updateSessions(prev => {
|
|
2234
|
-
const next = new Map(prev);
|
|
2235
|
-
const session =
|
|
2236
|
-
next.get(targetSessionKey) ?? createSession('', `Session started`, { sessionKey: targetSessionKey });
|
|
2237
|
-
const entry: ActivityItem = { id: crypto.randomUUID(), ts: Date.now(), ...item };
|
|
2238
|
-
const extractCommandKey = (detail: string): string => {
|
|
2239
|
-
const raw = (detail || '').trim();
|
|
2240
|
-
if (!raw) {
|
|
2241
|
-
return '';
|
|
2242
|
-
}
|
|
2243
|
-
// Completed entries may include extra lines (e.g. exit code).
|
|
2244
|
-
return raw.split('\n')[0].trim();
|
|
2245
|
-
};
|
|
2246
|
-
const normalizePhaseBaseTitle = (title: string): string => {
|
|
2247
|
-
const raw = (title || '').trim();
|
|
2248
|
-
if (!raw) {
|
|
2249
|
-
return '';
|
|
2250
|
-
}
|
|
2251
|
-
return raw.replace(/\s+(started|completed)\s*$/i, '').trim();
|
|
2252
|
-
};
|
|
2253
|
-
const extractGenericKey = (title: string, detail: string): string => {
|
|
2254
|
-
const base = normalizePhaseBaseTitle(title);
|
|
2255
|
-
const firstLine = (detail || '').trim().split('\n')[0].trim();
|
|
2256
|
-
return `${base}::${firstLine}`;
|
|
2257
|
-
};
|
|
2258
|
-
|
|
2259
|
-
const messages = session.messages;
|
|
2260
|
-
|
|
2261
|
-
// Avoid noisy duplicates like repeated "Reasoning step" lines.
|
|
2262
|
-
if (entry.category === 'reasoning') {
|
|
2263
|
-
const last = messages[messages.length - 1];
|
|
2264
|
-
if (last && last.kind === 'activity') {
|
|
2265
|
-
const previousItem = last.item;
|
|
2266
|
-
if (
|
|
2267
|
-
previousItem.category === entry.category &&
|
|
2268
|
-
previousItem.phase === entry.phase &&
|
|
2269
|
-
previousItem.title === entry.title &&
|
|
2270
|
-
previousItem.detail === entry.detail
|
|
2271
|
-
) {
|
|
2272
|
-
return prev;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
// If we have a corresponding "started" command, update it in place instead of appending.
|
|
2278
|
-
if (entry.category === 'command' && entry.phase === 'completed') {
|
|
2279
|
-
const key = extractCommandKey(entry.detail);
|
|
2280
|
-
if (key) {
|
|
2281
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
2282
|
-
const msg = messages[idx];
|
|
2283
|
-
if (msg.kind !== 'activity') {
|
|
2284
|
-
continue;
|
|
2285
|
-
}
|
|
2286
|
-
const existing = msg.item;
|
|
2287
|
-
if (existing.category !== 'command' || existing.phase !== 'started') {
|
|
2288
|
-
continue;
|
|
2289
|
-
}
|
|
2290
|
-
if (extractCommandKey(existing.detail) !== key) {
|
|
2291
|
-
continue;
|
|
2292
|
-
}
|
|
2293
|
-
const updated: ActivityItem = {
|
|
2294
|
-
...existing,
|
|
2295
|
-
phase: 'completed',
|
|
2296
|
-
title: entry.title,
|
|
2297
|
-
detail: entry.detail,
|
|
2298
|
-
raw: entry.raw,
|
|
2299
|
-
};
|
|
2300
|
-
const updatedMessages: ChatEntry[] = [
|
|
2301
|
-
...messages.slice(0, idx),
|
|
2302
|
-
{ ...msg, item: updated },
|
|
2303
|
-
...messages.slice(idx + 1),
|
|
2304
|
-
];
|
|
2305
|
-
next.set(targetSessionKey, { ...session, messages: updatedMessages });
|
|
2306
|
-
return next;
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// Generic: If we have a corresponding "started" tool/event line, update it in place.
|
|
2312
|
-
// This keeps pairs like "Web Search started" -> "Web Search completed" on a single line.
|
|
2313
|
-
if (entry.phase === 'completed') {
|
|
2314
|
-
const key = extractGenericKey(entry.title, entry.detail);
|
|
2315
|
-
if (key) {
|
|
2316
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
2317
|
-
const msg = messages[idx];
|
|
2318
|
-
if (msg.kind !== 'activity') {
|
|
2319
|
-
continue;
|
|
2320
|
-
}
|
|
2321
|
-
const existing = msg.item;
|
|
2322
|
-
if (existing.phase !== 'started') {
|
|
2323
|
-
continue;
|
|
2324
|
-
}
|
|
2325
|
-
if (extractGenericKey(existing.title, existing.detail) !== key) {
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
const updated: ActivityItem = {
|
|
2329
|
-
...existing,
|
|
2330
|
-
phase: 'completed',
|
|
2331
|
-
title: entry.title,
|
|
2332
|
-
detail: entry.detail,
|
|
2333
|
-
raw: entry.raw,
|
|
2334
|
-
};
|
|
2335
|
-
const updatedMessages: ChatEntry[] = [
|
|
2336
|
-
...messages.slice(0, idx),
|
|
2337
|
-
{ ...msg, item: updated },
|
|
2338
|
-
...messages.slice(idx + 1),
|
|
2339
|
-
];
|
|
2340
|
-
next.set(targetSessionKey, { ...session, messages: updatedMessages });
|
|
2341
|
-
return next;
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
const updatedMessages: ChatEntry[] = [...messages, { kind: 'activity', id: entry.id, item: entry }];
|
|
2347
|
-
next.set(targetSessionKey, { ...session, messages: updatedMessages });
|
|
2348
|
-
return next;
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
function setSessionPairing(
|
|
2353
|
-
sessionKey: string,
|
|
2354
|
-
pairing: {
|
|
2355
|
-
pairedOk: boolean | null;
|
|
2356
|
-
pairedPath: string;
|
|
2357
|
-
pairedOsPath: string;
|
|
2358
|
-
pairedMessage: string;
|
|
2359
|
-
}
|
|
2360
|
-
): void {
|
|
2361
|
-
const targetSessionKey = sessionKey || currentNotebookSessionKeyRef.current || '';
|
|
2362
|
-
if (!targetSessionKey) {
|
|
2363
|
-
return;
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
updateSessions(prev => {
|
|
2367
|
-
const next = new Map(prev);
|
|
2368
|
-
const session =
|
|
2369
|
-
next.get(targetSessionKey) ?? createSession('', `Session started`, { sessionKey: targetSessionKey });
|
|
2370
|
-
next.set(targetSessionKey, { ...session, ...pairing });
|
|
2371
|
-
return next;
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
function resolveMessageSessionKey(msg: any): string {
|
|
2376
|
-
const messagePath = typeof msg.notebookPath === 'string' ? msg.notebookPath : '';
|
|
2377
|
-
const sessionContextKey = typeof msg.sessionContextKey === 'string' ? msg.sessionContextKey : '';
|
|
2378
|
-
if (sessionContextKey) {
|
|
2379
|
-
return sessionContextKey;
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
const runId = typeof msg.runId === 'string' ? msg.runId : '';
|
|
2383
|
-
if (runId) {
|
|
2384
|
-
const mapped = runToSessionKeyRef.current.get(runId);
|
|
2385
|
-
if (mapped) {
|
|
2386
|
-
return mapped;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
if (messagePath) {
|
|
2391
|
-
if (runId) {
|
|
2392
|
-
const activeSessionKey = activeSessionKeyByPathRef.current.get(messagePath);
|
|
2393
|
-
if (activeSessionKey) {
|
|
2394
|
-
runToSessionKeyRef.current.set(runId, activeSessionKey);
|
|
2395
|
-
return activeSessionKey;
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
const activeSessionKey = activeSessionKeyByPathRef.current.get(messagePath);
|
|
2399
|
-
if (activeSessionKey) {
|
|
2400
|
-
return activeSessionKey;
|
|
2401
|
-
}
|
|
2402
|
-
return makeSessionKey(messagePath);
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
if (runId) {
|
|
2406
|
-
const mapped = runToSessionKeyRef.current.get(runId);
|
|
2407
|
-
if (mapped) {
|
|
2408
|
-
return mapped;
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
return currentNotebookSessionKeyRef.current || '';
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
function appendMessage(sessionKey: string, role: TextRole, text: string): void {
|
|
2416
|
-
if (!text) {
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
const targetSessionKey = sessionKey || currentNotebookSessionKeyRef.current || '';
|
|
2420
|
-
if (!targetSessionKey) {
|
|
2421
|
-
return;
|
|
2422
|
-
}
|
|
2423
|
-
const nextText = normalizeSystemText(role, text);
|
|
2424
|
-
|
|
2425
|
-
updateSessions(prev => {
|
|
2426
|
-
const next = new Map(prev);
|
|
2427
|
-
const session =
|
|
2428
|
-
next.get(targetSessionKey) ?? createSession('', `Session started`, { sessionKey: targetSessionKey });
|
|
2429
|
-
const messages = session.messages;
|
|
2430
|
-
// Keep each incoming message as a distinct bubble for readability.
|
|
2431
|
-
const updatedMessages: ChatEntry[] = [
|
|
2432
|
-
...messages,
|
|
2433
|
-
{ kind: 'text', id: crypto.randomUUID(), role, text: nextText }
|
|
2434
|
-
];
|
|
2435
|
-
|
|
2436
|
-
next.set(targetSessionKey, { ...session, messages: updatedMessages });
|
|
2437
|
-
return next;
|
|
2438
|
-
});
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
function scrollToBottom(): void {
|
|
2442
|
-
endRef.current?.scrollIntoView({ block: 'end' });
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
function onScrollMessages(): void {
|
|
2446
|
-
const node = scrollRef.current;
|
|
2447
|
-
if (!node) {
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
2450
|
-
const threshold = 80;
|
|
2451
|
-
const atBottom = node.scrollHeight - (node.scrollTop + node.clientHeight) < threshold;
|
|
2452
|
-
setIsAtBottom(atBottom);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
function clearRunMappingForSessionKey(targetSessionKey: string): void {
|
|
2456
|
-
const runToSessionKey = runToSessionKeyRef.current;
|
|
2457
|
-
const next = new Map(runToSessionKey);
|
|
2458
|
-
for (const [runId, mappedSessionKey] of runToSessionKey) {
|
|
2459
|
-
if (mappedSessionKey === targetSessionKey) {
|
|
2460
|
-
next.delete(runId);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
runToSessionKeyRef.current = next;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
async function clearAllSessions(): Promise<void> {
|
|
2467
|
-
const count = getStoredSessionThreadCount();
|
|
2468
|
-
if (!count && sessions.size === 0) {
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
const result = await showDialog({
|
|
2473
|
-
title: 'Delete all conversations',
|
|
2474
|
-
body: `This will delete ${count} saved conversation(s) and all in-memory messages in this panel.`,
|
|
2475
|
-
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Delete' })]
|
|
2476
|
-
});
|
|
2477
|
-
if (!result.button.accept) {
|
|
2478
|
-
return;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
const activeSessionKey = currentNotebookSessionKeyRef.current || '';
|
|
2482
|
-
if (activeSessionKey) {
|
|
2483
|
-
clearRunMappingForSessionKey(activeSessionKey);
|
|
2484
|
-
}
|
|
2485
|
-
markDeleteAllPending();
|
|
2486
|
-
if (!deleteAllSessionsOnServer() && activeSessionKey) {
|
|
2487
|
-
appendMessage(activeSessionKey, 'system', 'Delete request could not be sent now. It will be retried when you reconnect.');
|
|
2488
|
-
}
|
|
2489
|
-
runToSessionKeyRef.current = new Map();
|
|
2490
|
-
activeSessionKeyByPathRef.current = new Map();
|
|
2491
|
-
safeLocalStorageRemove(SESSION_THREADS_STORAGE_KEY);
|
|
2492
|
-
replaceSessions(new Map());
|
|
2493
|
-
clearInputForCurrentSession();
|
|
2494
|
-
clearPendingImages();
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
async function refreshNotebook(sessionKey: string): Promise<void> {
|
|
2498
|
-
const { path } = parseSessionKey(sessionKey);
|
|
2499
|
-
if (!path) {
|
|
2500
|
-
return;
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
const widget = props.notebooks.currentWidget;
|
|
2504
|
-
if (!widget || widget.context.path !== path) {
|
|
2505
|
-
pendingRefreshPathsRef.current.add(sessionKey);
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
const context = widget.context;
|
|
2510
|
-
if (context.model.dirty) {
|
|
2511
|
-
const result = await showDialog({
|
|
2512
|
-
title: 'Notebook has unsaved changes',
|
|
2513
|
-
body: 'Codex updated the paired file. Reload notebook now? (Unsaved changes will be lost.)',
|
|
2514
|
-
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Reload' })]
|
|
2515
|
-
});
|
|
2516
|
-
if (!result.button.accept) {
|
|
2517
|
-
pendingRefreshPathsRef.current.add(sessionKey);
|
|
2518
|
-
return;
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
try {
|
|
2523
|
-
await context.revert();
|
|
2524
|
-
appendMessage(sessionKey, 'system', 'Notebook was refreshed after Codex file updates.');
|
|
2525
|
-
pendingRefreshPathsRef.current.delete(sessionKey);
|
|
2526
|
-
} catch (err) {
|
|
2527
|
-
appendMessage(sessionKey, 'system', `Failed to refresh notebook: ${String(err)}`);
|
|
2528
|
-
pendingRefreshPathsRef.current.add(sessionKey);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
useEffect(() => {
|
|
2533
|
-
const updateNotebook = () => {
|
|
2534
|
-
const path = getNotebookPath(props.notebooks);
|
|
2535
|
-
const sessionKey = resolveSessionKey(path);
|
|
2536
|
-
const previousKey = currentNotebookSessionKeyRef.current;
|
|
2537
|
-
if (sessionKey && sessionKey === previousKey) {
|
|
2538
|
-
return;
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
if (previousKey) {
|
|
2542
|
-
saveCurrentInputDraft(inputRef.current);
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
currentNotebookPathRef.current = path;
|
|
2546
|
-
setCurrentNotebookPath(path);
|
|
2547
|
-
setCurrentNotebookSessionKey(sessionKey);
|
|
2548
|
-
currentNotebookSessionKeyRef.current = sessionKey;
|
|
2549
|
-
if (!sessionKey) {
|
|
2550
|
-
clearInputForCurrentSession();
|
|
2551
|
-
return;
|
|
2552
|
-
}
|
|
2553
|
-
restoreInput(sessionKey);
|
|
2554
|
-
clearPendingImages();
|
|
2555
|
-
setIsAtBottom(true);
|
|
2556
|
-
|
|
2557
|
-
if (!path) {
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
const session = ensureSession(path, sessionKey);
|
|
2562
|
-
activeSessionKeyByPathRef.current.set(path, sessionKey);
|
|
2563
|
-
sendStartSession(session, path, sessionKey);
|
|
2564
|
-
if (pendingRefreshPathsRef.current.has(sessionKey)) {
|
|
2565
|
-
void refreshNotebook(sessionKey);
|
|
2566
|
-
return;
|
|
2567
|
-
}
|
|
2568
|
-
};
|
|
2569
|
-
|
|
2570
|
-
updateNotebook();
|
|
2571
|
-
props.notebooks.currentChanged.connect(updateNotebook);
|
|
2572
|
-
|
|
2573
|
-
return () => {
|
|
2574
|
-
props.notebooks.currentChanged.disconnect(updateNotebook);
|
|
2575
|
-
};
|
|
2576
|
-
}, [props.notebooks]);
|
|
2577
|
-
|
|
2578
|
-
useEffect(() => {
|
|
2579
|
-
const onStorage = (event: StorageEvent) => {
|
|
2580
|
-
if (event.key !== SESSION_THREADS_EVENT_KEY || !event.newValue) {
|
|
2581
|
-
return;
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
const syncEvent = coerceSessionThreadSyncEvent(event.newValue);
|
|
2585
|
-
if (!syncEvent) {
|
|
2586
|
-
return;
|
|
2587
|
-
}
|
|
2588
|
-
if (syncEvent.source === sessionThreadSyncIdRef.current) {
|
|
2589
|
-
return;
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
const notebookPath = syncEvent.notebookPath;
|
|
2593
|
-
const sessionKey = syncEvent.sessionKey;
|
|
2594
|
-
const threadId = syncEvent.threadId;
|
|
2595
|
-
if (!notebookPath || !sessionKey || !threadId) {
|
|
2596
|
-
return;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
const resolvedSessionKey = resolveSessionKey(notebookPath);
|
|
2600
|
-
if (!resolvedSessionKey || resolvedSessionKey !== sessionKey) {
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
const resetSession = createThreadResetSession(notebookPath, sessionKey, threadId);
|
|
2605
|
-
const currentPath = currentNotebookPathRef.current;
|
|
2606
|
-
|
|
2607
|
-
updateSessions(prev => {
|
|
2608
|
-
const next = new Map(prev);
|
|
2609
|
-
const existing = next.get(sessionKey);
|
|
2610
|
-
if (existing && existing.threadId === threadId) {
|
|
2611
|
-
return prev;
|
|
2612
|
-
}
|
|
2613
|
-
next.set(sessionKey, resetSession);
|
|
2614
|
-
return next;
|
|
2615
|
-
});
|
|
2616
|
-
activeSessionKeyByPathRef.current.set(notebookPath, sessionKey);
|
|
2617
|
-
clearRunMappingForSessionKey(sessionKey);
|
|
2618
|
-
|
|
2619
|
-
if (currentPath === notebookPath && currentNotebookSessionKeyRef.current !== sessionKey) {
|
|
2620
|
-
setCurrentNotebookSessionKey(sessionKey);
|
|
2621
|
-
}
|
|
2622
|
-
if (currentPath === notebookPath) {
|
|
2623
|
-
clearInputForCurrentSession();
|
|
2624
|
-
clearPendingImages();
|
|
2625
|
-
}
|
|
2626
|
-
sendStartSession(resetSession, notebookPath, sessionKey);
|
|
2627
|
-
};
|
|
2628
|
-
|
|
2629
|
-
window.addEventListener('storage', onStorage);
|
|
2630
|
-
return () => {
|
|
2631
|
-
window.removeEventListener('storage', onStorage);
|
|
2632
|
-
};
|
|
2633
|
-
}, []);
|
|
2634
|
-
|
|
2635
|
-
function reconnectSocket(): void {
|
|
2636
|
-
if (isReconnecting) {
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
setReconnectCounter(value => value + 1);
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
useEffect(() => {
|
|
2643
|
-
setIsReconnecting(true);
|
|
2644
|
-
const settings = ServerConnection.makeSettings();
|
|
2645
|
-
const wsUrl = URLExt.join(settings.wsUrl, 'codex', 'ws');
|
|
2646
|
-
let socket: WebSocket;
|
|
2647
|
-
try {
|
|
2648
|
-
socket = new WebSocket(wsUrl);
|
|
2649
|
-
} catch {
|
|
2650
|
-
setSocketConnected(false);
|
|
2651
|
-
setIsReconnecting(false);
|
|
2652
|
-
return;
|
|
2653
|
-
}
|
|
2654
|
-
wsRef.current = socket;
|
|
2655
|
-
|
|
2656
|
-
socket.onopen = () => {
|
|
2657
|
-
setSocketConnected(true);
|
|
2658
|
-
setIsReconnecting(false);
|
|
2659
|
-
|
|
2660
|
-
if (hasDeleteAllPending()) {
|
|
2661
|
-
deleteAllSessionsOnServer();
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
const notebookPath = currentNotebookPathRef.current || getNotebookPath(props.notebooks);
|
|
2665
|
-
if (!notebookPath) {
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
const currentSessionKey = resolveCurrentSessionKey(notebookPath);
|
|
2670
|
-
const session = ensureSession(notebookPath, currentSessionKey);
|
|
2671
|
-
sendStartSession(session, notebookPath, currentSessionKey);
|
|
2672
|
-
};
|
|
2673
|
-
|
|
2674
|
-
socket.onclose = () => {
|
|
2675
|
-
setSocketConnected(false);
|
|
2676
|
-
setIsReconnecting(false);
|
|
2677
|
-
runToSessionKeyRef.current = new Map();
|
|
2678
|
-
};
|
|
2679
|
-
|
|
2680
|
-
socket.onerror = () => {
|
|
2681
|
-
setSocketConnected(false);
|
|
2682
|
-
setIsReconnecting(false);
|
|
2683
|
-
};
|
|
2684
|
-
|
|
2685
|
-
socket.onmessage = event => {
|
|
2686
|
-
let msg: any;
|
|
2687
|
-
try {
|
|
2688
|
-
msg = JSON.parse(event.data);
|
|
2689
|
-
} catch (err) {
|
|
2690
|
-
appendMessage(currentNotebookSessionKeyRef.current || '', 'system', `Invalid message: ${String(event.data)}`);
|
|
2691
|
-
return;
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
const runId = typeof msg.runId === 'string' ? msg.runId : '';
|
|
2695
|
-
const targetSessionKey = resolveMessageSessionKey(msg);
|
|
2696
|
-
|
|
2697
|
-
if (msg.type === 'cli_defaults') {
|
|
2698
|
-
const modelIsPresent = Object.prototype.hasOwnProperty.call(msg, 'model');
|
|
2699
|
-
const reasoningIsPresent = Object.prototype.hasOwnProperty.call(msg, 'reasoningEffort');
|
|
2700
|
-
const availableModelsIsPresent = Object.prototype.hasOwnProperty.call(msg, 'availableModels');
|
|
2701
|
-
const model =
|
|
2702
|
-
modelIsPresent && typeof msg.model === 'string' && msg.model.trim() ? msg.model.trim() : null;
|
|
2703
|
-
const reasoningEffort = reasoningIsPresent
|
|
2704
|
-
? coerceReasoningEffort(typeof msg.reasoningEffort === 'string' ? msg.reasoningEffort : '')
|
|
2705
|
-
: null;
|
|
2706
|
-
const availableModels = Array.isArray(msg.availableModels) ? msg.availableModels : undefined;
|
|
2707
|
-
const normalizedAvailableModels = availableModels ? readModelCatalog(availableModels) : undefined;
|
|
2708
|
-
setCliDefaults(prev => ({
|
|
2709
|
-
model: modelIsPresent ? model : prev.model,
|
|
2710
|
-
reasoningEffort: reasoningIsPresent ? reasoningEffort : prev.reasoningEffort,
|
|
2711
|
-
availableModels: availableModelsIsPresent ? normalizedAvailableModels : prev.availableModels
|
|
2712
|
-
}));
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
if (msg.type === 'rate_limits') {
|
|
2717
|
-
setRateLimits(coerceRateLimitsSnapshot(msg.snapshot));
|
|
2718
|
-
return;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
if (msg.type === 'delete_all_sessions') {
|
|
2722
|
-
const ok = msg.ok === true;
|
|
2723
|
-
if (ok) {
|
|
2724
|
-
clearDeleteAllPending();
|
|
2725
|
-
}
|
|
2726
|
-
const deletedCount = Number.isFinite(Number(msg.deletedCount)) ? Number(msg.deletedCount) : 0;
|
|
2727
|
-
const failedCount = Number.isFinite(Number(msg.failedCount)) ? Number(msg.failedCount) : 0;
|
|
2728
|
-
const deletedSummary = deletedCount === 1 ? 'Deleted 1 conversation' : `Deleted ${deletedCount} conversations`;
|
|
2729
|
-
const feedbackSession = currentNotebookSessionKeyRef.current;
|
|
2730
|
-
const feedbackText = ok
|
|
2731
|
-
? `${deletedSummary} from the server.`
|
|
2732
|
-
: `${deletedSummary} from the server. Failed to delete ${failedCount} conversations. ${typeof msg.message === 'string' ? msg.message : 'Server error'}. Retry after reconnect.`;
|
|
2733
|
-
if (feedbackSession) {
|
|
2734
|
-
appendMessage(feedbackSession, 'system', feedbackText);
|
|
2735
|
-
}
|
|
2736
|
-
if (!ok && !hasDeleteAllPending()) {
|
|
2737
|
-
markDeleteAllPending();
|
|
2738
|
-
}
|
|
2739
|
-
return;
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
if (msg.type === 'status') {
|
|
2743
|
-
const sessionId = typeof msg.sessionId === 'string' && msg.sessionId.trim() ? msg.sessionId.trim() : '';
|
|
2744
|
-
const history = coerceSessionHistory(msg.history);
|
|
2745
|
-
if (targetSessionKey && (sessionId || history.length > 0)) {
|
|
2746
|
-
updateSessions(prev => {
|
|
2747
|
-
const next = new Map(prev);
|
|
2748
|
-
const existing =
|
|
2749
|
-
next.get(targetSessionKey) ?? createSession('', `Session started`, { sessionKey: targetSessionKey });
|
|
2750
|
-
const nextThreadId = sessionId || existing.threadId;
|
|
2751
|
-
let nextMessages = existing.messages;
|
|
2752
|
-
|
|
2753
|
-
const hasConversation = existing.messages.some(
|
|
2754
|
-
entry => entry.kind === 'text' && (entry.role === 'user' || entry.role === 'assistant')
|
|
2755
|
-
);
|
|
2756
|
-
if (!hasConversation && history.length > 0) {
|
|
2757
|
-
const introEntry =
|
|
2758
|
-
existing.messages.find(
|
|
2759
|
-
entry => entry.kind === 'text' && entry.role === 'system' && isSessionStartNotice(entry.text)
|
|
2760
|
-
) ??
|
|
2761
|
-
({
|
|
2762
|
-
kind: 'text',
|
|
2763
|
-
id: crypto.randomUUID(),
|
|
2764
|
-
role: 'system',
|
|
2765
|
-
text: normalizeSystemText('system', 'Session started')
|
|
2766
|
-
} as ChatEntry);
|
|
2767
|
-
const restoredEntries: ChatEntry[] = history.map(item => ({
|
|
2768
|
-
kind: 'text',
|
|
2769
|
-
id: crypto.randomUUID(),
|
|
2770
|
-
role: item.role,
|
|
2771
|
-
text: normalizeSystemText(item.role, item.content)
|
|
2772
|
-
}));
|
|
2773
|
-
nextMessages = [introEntry, ...restoredEntries];
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
if (nextThreadId === existing.threadId && nextMessages === existing.messages) {
|
|
2777
|
-
return prev;
|
|
2778
|
-
}
|
|
2779
|
-
next.set(targetSessionKey, { ...existing, threadId: nextThreadId, messages: nextMessages });
|
|
2780
|
-
return next;
|
|
2781
|
-
});
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
if (targetSessionKey) {
|
|
2785
|
-
const pairedOk = typeof msg.pairedOk === 'boolean' ? msg.pairedOk : null;
|
|
2786
|
-
const pairedPath = typeof msg.pairedPath === 'string' ? msg.pairedPath : '';
|
|
2787
|
-
const pairedOsPath = typeof msg.pairedOsPath === 'string' ? msg.pairedOsPath : '';
|
|
2788
|
-
const pairedMessage = typeof msg.pairedMessage === 'string' ? msg.pairedMessage : '';
|
|
2789
|
-
if (pairedOk !== null || pairedPath || pairedOsPath || pairedMessage) {
|
|
2790
|
-
setSessionPairing(targetSessionKey, { pairedOk, pairedPath, pairedOsPath, pairedMessage });
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
if (msg.state === 'running' && targetSessionKey) {
|
|
2794
|
-
setSessionRunState(targetSessionKey, 'running', runId || null);
|
|
2795
|
-
setSessionProgress(targetSessionKey, '', '');
|
|
2796
|
-
} else if (msg.state === 'ready' && targetSessionKey) {
|
|
2797
|
-
// The server emits "ready" in a few contexts that are *not* run lifecycle events
|
|
2798
|
-
// (e.g. socket open, start_session responses). Those payloads don't include a
|
|
2799
|
-
// runId and must not clobber an in-progress run state when users switch tabs.
|
|
2800
|
-
if (runId) {
|
|
2801
|
-
setSessionRunState(targetSessionKey, 'ready', null);
|
|
2802
|
-
setSessionProgress(targetSessionKey, '', '');
|
|
2803
|
-
runToSessionKeyRef.current.delete(runId);
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
return;
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
if (msg.type === 'output') {
|
|
2810
|
-
appendMessage(targetSessionKey, 'assistant', msg.text || '');
|
|
2811
|
-
return;
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
if (msg.type === 'event') {
|
|
2815
|
-
const payload = msg.payload;
|
|
2816
|
-
if (isNoiseCodexEvent(payload)) {
|
|
2817
|
-
return;
|
|
2818
|
-
}
|
|
2819
|
-
const summary = summarizeCodexEvent(payload);
|
|
2820
|
-
// "Reasoning" events are metadata-only and add noise; the loading indicator already conveys progress.
|
|
2821
|
-
if (summary.activity.category !== 'reasoning') {
|
|
2822
|
-
appendActivityItem(targetSessionKey, summary.activity);
|
|
2823
|
-
}
|
|
2824
|
-
setSessionProgress(targetSessionKey, summary.progress, summary.progressKind);
|
|
2825
|
-
return;
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
if (msg.type === 'error') {
|
|
2829
|
-
const suggestedCommandPath = typeof msg.suggestedCommandPath === 'string' ? msg.suggestedCommandPath.trim() : '';
|
|
2830
|
-
if (suggestedCommandPath && !commandPath) {
|
|
2831
|
-
setCommandPath(suggestedCommandPath);
|
|
2832
|
-
appendMessage(
|
|
2833
|
-
targetSessionKey,
|
|
2834
|
-
'system',
|
|
2835
|
-
`Suggested command path has been saved to settings: ${suggestedCommandPath}`
|
|
2836
|
-
);
|
|
2837
|
-
}
|
|
2838
|
-
appendMessage(targetSessionKey, 'system', msg.message || 'Unknown error');
|
|
2839
|
-
if (targetSessionKey) {
|
|
2840
|
-
const pairedOk = typeof msg.pairedOk === 'boolean' ? msg.pairedOk : null;
|
|
2841
|
-
const pairedPath = typeof msg.pairedPath === 'string' ? msg.pairedPath : '';
|
|
2842
|
-
const pairedOsPath = typeof msg.pairedOsPath === 'string' ? msg.pairedOsPath : '';
|
|
2843
|
-
const pairedMessage = typeof msg.pairedMessage === 'string' ? msg.pairedMessage : '';
|
|
2844
|
-
if (pairedOk !== null || pairedPath || pairedOsPath || pairedMessage) {
|
|
2845
|
-
setSessionPairing(targetSessionKey, { pairedOk, pairedPath, pairedOsPath, pairedMessage });
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
if (targetSessionKey) {
|
|
2849
|
-
setSessionRunState(targetSessionKey, 'ready', null);
|
|
2850
|
-
setSessionProgress(targetSessionKey, '', '');
|
|
2851
|
-
}
|
|
2852
|
-
if (runId) {
|
|
2853
|
-
runToSessionKeyRef.current.delete(runId);
|
|
2854
|
-
}
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
if (msg.type === 'done') {
|
|
2859
|
-
if (targetSessionKey) {
|
|
2860
|
-
const pairedOk = typeof msg.pairedOk === 'boolean' ? msg.pairedOk : null;
|
|
2861
|
-
const pairedPath = typeof msg.pairedPath === 'string' ? msg.pairedPath : '';
|
|
2862
|
-
const pairedOsPath = typeof msg.pairedOsPath === 'string' ? msg.pairedOsPath : '';
|
|
2863
|
-
const pairedMessage = typeof msg.pairedMessage === 'string' ? msg.pairedMessage : '';
|
|
2864
|
-
if (pairedOk !== null || pairedPath || pairedOsPath || pairedMessage) {
|
|
2865
|
-
setSessionPairing(targetSessionKey, { pairedOk, pairedPath, pairedOsPath, pairedMessage });
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
const exitCode = typeof msg.exitCode === 'number' ? msg.exitCode : null;
|
|
2869
|
-
const cancelled = Boolean(msg.cancelled);
|
|
2870
|
-
const fileChanged = Boolean(msg.fileChanged);
|
|
2871
|
-
if (fileChanged && targetSessionKey) {
|
|
2872
|
-
void refreshNotebook(targetSessionKey);
|
|
2873
|
-
}
|
|
2874
|
-
if (!cancelled && exitCode !== null && exitCode !== 0) {
|
|
2875
|
-
const explicitError =
|
|
2876
|
-
(typeof msg.error === 'string' && msg.error.trim()) ||
|
|
2877
|
-
(typeof msg.message === 'string' && msg.message.trim()) ||
|
|
2878
|
-
'';
|
|
2879
|
-
const trimmedError = explicitError ? truncateEnd(explicitError, 600) : '';
|
|
2880
|
-
const failureMessage = trimmedError
|
|
2881
|
-
? `Codex run failed (exit ${exitCode}): ${trimmedError}`
|
|
2882
|
-
: `Codex run failed (exit ${exitCode}). Check the logs above for the underlying error.`;
|
|
2883
|
-
appendMessage(targetSessionKey, 'system', failureMessage);
|
|
2884
|
-
}
|
|
2885
|
-
notifyRunDone(
|
|
2886
|
-
targetSessionKey,
|
|
2887
|
-
typeof msg.notebookPath === 'string' ? msg.notebookPath : '',
|
|
2888
|
-
cancelled,
|
|
2889
|
-
exitCode
|
|
2890
|
-
);
|
|
2891
|
-
if (targetSessionKey) {
|
|
2892
|
-
setSessionRunState(targetSessionKey, 'ready', null);
|
|
2893
|
-
setSessionProgress(targetSessionKey, '', '');
|
|
2894
|
-
}
|
|
2895
|
-
if (runId) {
|
|
2896
|
-
runToSessionKeyRef.current.delete(runId);
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
};
|
|
2900
|
-
|
|
2901
|
-
return () => {
|
|
2902
|
-
socket.close();
|
|
2903
|
-
wsRef.current = null;
|
|
2904
|
-
runToSessionKeyRef.current = new Map();
|
|
2905
|
-
};
|
|
2906
|
-
}, [props.notebooks, reconnectCounter]);
|
|
2907
|
-
|
|
2908
|
-
useEffect(() => {
|
|
2909
|
-
if (!isAtBottom) {
|
|
2910
|
-
return;
|
|
2911
|
-
}
|
|
2912
|
-
const id = window.requestAnimationFrame(() => scrollToBottom());
|
|
2913
|
-
return () => window.cancelAnimationFrame(id);
|
|
2914
|
-
}, [isAtBottom, sessions, currentNotebookPath, socketConnected]);
|
|
2915
|
-
|
|
2916
|
-
function autosizeComposerTextarea(el?: HTMLTextAreaElement | null): void {
|
|
2917
|
-
const textarea = el ?? composerTextareaRef.current;
|
|
2918
|
-
if (!textarea) {
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
// Reset first so it can shrink as content is removed.
|
|
2923
|
-
textarea.style.height = 'auto';
|
|
2924
|
-
|
|
2925
|
-
const cs = window.getComputedStyle(textarea);
|
|
2926
|
-
|
|
2927
|
-
// Prefer CSS-controlled min/max heights so the JS sizing stays consistent
|
|
2928
|
-
// with the visual design (and any future CSS tweaks).
|
|
2929
|
-
let minHeight = Number.parseFloat(cs.minHeight || '');
|
|
2930
|
-
let maxHeight = Number.parseFloat(cs.maxHeight || '');
|
|
2931
|
-
|
|
2932
|
-
if (!Number.isFinite(minHeight) || minHeight <= 0) {
|
|
2933
|
-
const fontSize = Number.parseFloat(cs.fontSize || '13');
|
|
2934
|
-
let lineHeight = Number.parseFloat(cs.lineHeight || '');
|
|
2935
|
-
if (!Number.isFinite(lineHeight)) {
|
|
2936
|
-
lineHeight = fontSize * 1.35;
|
|
2937
|
-
}
|
|
2938
|
-
minHeight = lineHeight;
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
|
|
2942
|
-
const fontSize = Number.parseFloat(cs.fontSize || '13');
|
|
2943
|
-
let lineHeight = Number.parseFloat(cs.lineHeight || '');
|
|
2944
|
-
if (!Number.isFinite(lineHeight)) {
|
|
2945
|
-
lineHeight = fontSize * 1.35;
|
|
2946
|
-
}
|
|
2947
|
-
maxHeight = lineHeight * 3;
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
const scrollHeight = textarea.scrollHeight;
|
|
2951
|
-
const isEmpty = textarea.value.length === 0;
|
|
2952
|
-
const unclampedHeight = isEmpty ? minHeight : scrollHeight;
|
|
2953
|
-
const nextHeight = Math.ceil(Math.min(Math.max(unclampedHeight, minHeight), maxHeight));
|
|
2954
|
-
|
|
2955
|
-
textarea.style.height = `${nextHeight}px`;
|
|
2956
|
-
textarea.style.overflowY = scrollHeight > maxHeight + 1 ? 'auto' : 'hidden';
|
|
2957
|
-
}
|
|
2958
|
-
|
|
2959
|
-
useEffect(() => {
|
|
2960
|
-
// Defer to the next frame so layout reflects the latest value.
|
|
2961
|
-
const id = window.requestAnimationFrame(() => autosizeComposerTextarea());
|
|
2962
|
-
return () => window.cancelAnimationFrame(id);
|
|
2963
|
-
}, [input]);
|
|
2964
|
-
|
|
2965
|
-
useEffect(() => {
|
|
2966
|
-
const onResize = () => autosizeComposerTextarea();
|
|
2967
|
-
window.addEventListener('resize', onResize);
|
|
2968
|
-
return () => window.removeEventListener('resize', onResize);
|
|
2969
|
-
}, []);
|
|
2970
|
-
|
|
2971
|
-
async function startNewThread(): Promise<void> {
|
|
2972
|
-
const path = currentNotebookPathRef.current || '';
|
|
2973
|
-
const sessionKey = currentNotebookSessionKeyRef.current || '';
|
|
2974
|
-
if (!path) {
|
|
2975
|
-
return;
|
|
2976
|
-
}
|
|
2977
|
-
if (!sessionKey) {
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
const existing = sessionsRef.current.get(sessionKey);
|
|
2982
|
-
const hasConversation =
|
|
2983
|
-
existing?.messages.some(
|
|
2984
|
-
msg => msg.kind === 'text' && (msg.role === 'user' || msg.role === 'assistant')
|
|
2985
|
-
) ?? false;
|
|
2986
|
-
if (hasConversation) {
|
|
2987
|
-
const result = await showDialog({
|
|
2988
|
-
title: 'Start a new thread?',
|
|
2989
|
-
body:
|
|
2990
|
-
'Starting a new thread will reset the current conversation, and you will not be able to view the previous conversation in this panel.\nContinue?',
|
|
2991
|
-
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'New thread' })]
|
|
2992
|
-
});
|
|
2993
|
-
if (!result.button.accept) {
|
|
2994
|
-
return;
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
const newSession = createThreadResetSession(path, sessionKey, createSessionEventId());
|
|
2999
|
-
updateSessions(prev => {
|
|
3000
|
-
const next = new Map(prev);
|
|
3001
|
-
next.set(sessionKey, newSession);
|
|
3002
|
-
return next;
|
|
3003
|
-
});
|
|
3004
|
-
clearRunMappingForSessionKey(sessionKey);
|
|
3005
|
-
clearInputForCurrentSession();
|
|
3006
|
-
clearPendingImages();
|
|
3007
|
-
sendStartSession(newSession, path, sessionKey, { forceNewThread: true });
|
|
3008
|
-
emitSessionThreadEvent(sessionKey, path, newSession.threadId);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
function cancelRun(): void {
|
|
3012
|
-
const socket = wsRef.current;
|
|
3013
|
-
const sessionKey = currentNotebookSessionKeyRef.current || '';
|
|
3014
|
-
if (!sessionKey) {
|
|
3015
|
-
return;
|
|
3016
|
-
}
|
|
3017
|
-
const session = sessionKey ? sessionsRef.current.get(sessionKey) : null;
|
|
3018
|
-
const runId = session?.activeRunId ?? null;
|
|
3019
|
-
|
|
3020
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
3021
|
-
appendMessage(sessionKey, 'system', 'Cancel failed: WebSocket is not connected.');
|
|
3022
|
-
return;
|
|
3023
|
-
}
|
|
3024
|
-
if (!runId) {
|
|
3025
|
-
appendMessage(sessionKey, 'system', 'Cancel not available yet (waiting for run id).');
|
|
3026
|
-
return;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
setSessionProgress(sessionKey, 'Cancelling...');
|
|
3030
|
-
socket.send(JSON.stringify({ type: 'cancel', runId }));
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
function clearPendingImages(): void {
|
|
3034
|
-
for (const image of pendingImagesRef.current) {
|
|
3035
|
-
URL.revokeObjectURL(image.previewUrl);
|
|
3036
|
-
}
|
|
3037
|
-
pendingImagesRef.current = [];
|
|
3038
|
-
setPendingImages([]);
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
function removePendingImage(id: string): void {
|
|
3042
|
-
setPendingImages(prev => {
|
|
3043
|
-
const removed = prev.find(image => image.id === id);
|
|
3044
|
-
if (removed) {
|
|
3045
|
-
URL.revokeObjectURL(removed.previewUrl);
|
|
3046
|
-
}
|
|
3047
|
-
const next = prev.filter(image => image.id !== id);
|
|
3048
|
-
pendingImagesRef.current = next;
|
|
3049
|
-
return next;
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
function onComposerPaste(event: React.ClipboardEvent<HTMLTextAreaElement>): void {
|
|
3054
|
-
const items = event.clipboardData?.items;
|
|
3055
|
-
if (!items || items.length === 0) {
|
|
3056
|
-
return;
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
const found: File[] = [];
|
|
3060
|
-
for (let idx = 0; idx < items.length; idx += 1) {
|
|
3061
|
-
const item = items[idx];
|
|
3062
|
-
if (!item || item.kind !== 'file' || !item.type.startsWith('image/')) {
|
|
3063
|
-
continue;
|
|
3064
|
-
}
|
|
3065
|
-
const file = item.getAsFile();
|
|
3066
|
-
if (file) {
|
|
3067
|
-
found.push(file);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
if (found.length === 0) {
|
|
3072
|
-
return;
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
const existingCount = pendingImagesRef.current.length;
|
|
3076
|
-
if (existingCount >= MAX_IMAGE_ATTACHMENTS) {
|
|
3077
|
-
appendMessage(
|
|
3078
|
-
currentNotebookSessionKeyRef.current || '',
|
|
3079
|
-
'system',
|
|
3080
|
-
`Too many images attached (max ${MAX_IMAGE_ATTACHMENTS}).`
|
|
3081
|
-
);
|
|
3082
|
-
return;
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
const remainingSlots = MAX_IMAGE_ATTACHMENTS - existingCount;
|
|
3086
|
-
const toAdd: PendingImageAttachment[] = [];
|
|
3087
|
-
let skippedLarge = 0;
|
|
3088
|
-
let skippedTotal = 0;
|
|
3089
|
-
let totalBytes = pendingImagesRef.current.reduce((sum, image) => sum + image.file.size, 0);
|
|
3090
|
-
for (const file of found.slice(0, remainingSlots)) {
|
|
3091
|
-
if (file.size > MAX_IMAGE_ATTACHMENT_BYTES) {
|
|
3092
|
-
skippedLarge += 1;
|
|
3093
|
-
continue;
|
|
3094
|
-
}
|
|
3095
|
-
if (totalBytes + file.size > MAX_IMAGE_ATTACHMENT_TOTAL_BYTES) {
|
|
3096
|
-
skippedTotal += 1;
|
|
3097
|
-
continue;
|
|
3098
|
-
}
|
|
3099
|
-
toAdd.push({ id: crypto.randomUUID(), file, previewUrl: URL.createObjectURL(file) });
|
|
3100
|
-
totalBytes += file.size;
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
if (skippedLarge > 0) {
|
|
3104
|
-
appendMessage(
|
|
3105
|
-
currentNotebookSessionKeyRef.current || '',
|
|
3106
|
-
'system',
|
|
3107
|
-
`Skipped ${skippedLarge} image(s): each must be <= ${Math.round(MAX_IMAGE_ATTACHMENT_BYTES / (1024 * 1024))}MB.`
|
|
3108
|
-
);
|
|
3109
|
-
}
|
|
3110
|
-
if (skippedTotal > 0) {
|
|
3111
|
-
appendMessage(
|
|
3112
|
-
currentNotebookSessionKeyRef.current || '',
|
|
3113
|
-
'system',
|
|
3114
|
-
`Skipped ${skippedTotal} image(s): total attachments must be <= ${Math.round(MAX_IMAGE_ATTACHMENT_TOTAL_BYTES / (1024 * 1024))}MB.`
|
|
3115
|
-
);
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
if (toAdd.length === 0) {
|
|
3119
|
-
return;
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
setPendingImages(prev => {
|
|
3123
|
-
const next = [...prev, ...toAdd];
|
|
3124
|
-
pendingImagesRef.current = next;
|
|
3125
|
-
return next;
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
async function sendMessage(): Promise<void> {
|
|
3130
|
-
const socket = wsRef.current;
|
|
3131
|
-
const notebookPath = currentNotebookPathRef.current || '';
|
|
3132
|
-
const sessionKey = currentNotebookSessionKeyRef.current || '';
|
|
3133
|
-
|
|
3134
|
-
if (!notebookPath) {
|
|
3135
|
-
return;
|
|
3136
|
-
}
|
|
3137
|
-
if (!sessionKey) {
|
|
3138
|
-
return;
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
3142
|
-
appendMessage(sessionKey, 'system', 'WebSocket is not connected.');
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
const existing = sessionKey ? sessionsRef.current.get(sessionKey) : null;
|
|
3147
|
-
if (existing?.pairedOk === false) {
|
|
3148
|
-
appendMessage(
|
|
3149
|
-
sessionKey,
|
|
3150
|
-
'system',
|
|
3151
|
-
existing.pairedMessage ||
|
|
3152
|
-
`Jupytext paired file not found. Expected: ${existing.pairedOsPath || existing.pairedPath || '<notebook>.py'}`
|
|
3153
|
-
);
|
|
3154
|
-
return;
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
const trimmed = input.trim();
|
|
3158
|
-
const hasImages = pendingImagesRef.current.length > 0;
|
|
3159
|
-
if (!trimmed && !hasImages) {
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
const current = sessionKey ? sessionsRef.current.get(sessionKey) : null;
|
|
3164
|
-
if (current?.runState === 'running') {
|
|
3165
|
-
return;
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
const widget = props.notebooks.currentWidget;
|
|
3169
|
-
if (autoSaveBeforeSend && widget && widget.context.model.dirty) {
|
|
3170
|
-
try {
|
|
3171
|
-
await widget.context.save();
|
|
3172
|
-
} catch (err) {
|
|
3173
|
-
appendMessage(sessionKey, 'system', `Auto-save failed: ${String(err)}`);
|
|
3174
|
-
return;
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
const selection = includeActiveCell ? getActiveCellText(props.notebooks) : '';
|
|
3179
|
-
const cellOutput =
|
|
3180
|
-
includeActiveCell && includeActiveCellOutput ? getActiveCellOutput(props.notebooks) : '';
|
|
3181
|
-
const session = ensureSession(notebookPath, sessionKey);
|
|
3182
|
-
|
|
3183
|
-
const content = trimmed || (hasImages ? 'Please analyze the attached image(s).' : '');
|
|
3184
|
-
let images: { name: string; dataUrl: string }[] | undefined;
|
|
3185
|
-
if (hasImages) {
|
|
3186
|
-
try {
|
|
3187
|
-
images = [];
|
|
3188
|
-
for (const image of pendingImagesRef.current) {
|
|
3189
|
-
const dataUrl = await blobToDataUrl(image.file);
|
|
3190
|
-
images.push({ name: image.file.name || 'clipboard-image', dataUrl });
|
|
3191
|
-
}
|
|
3192
|
-
} catch (err) {
|
|
3193
|
-
appendMessage(sessionKey, 'system', `Failed to attach image(s): ${String(err)}`);
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
socket.send(
|
|
3199
|
-
JSON.stringify({
|
|
3200
|
-
type: 'send',
|
|
3201
|
-
sessionId: session.threadId,
|
|
3202
|
-
sessionContextKey: sessionKey,
|
|
3203
|
-
content,
|
|
3204
|
-
selection,
|
|
3205
|
-
cellOutput,
|
|
3206
|
-
notebookPath,
|
|
3207
|
-
commandPath: commandPath.trim() || undefined,
|
|
3208
|
-
model: selectedModel || undefined,
|
|
3209
|
-
reasoningEffort: selectedReasoningEffort || undefined,
|
|
3210
|
-
sandbox: sandboxMode,
|
|
3211
|
-
images
|
|
3212
|
-
})
|
|
3213
|
-
);
|
|
3214
|
-
|
|
3215
|
-
const imageCount = pendingImagesRef.current.length;
|
|
3216
|
-
updateSessions(prev => {
|
|
3217
|
-
const next = new Map(prev);
|
|
3218
|
-
const existing = next.get(sessionKey) ?? createSession('', `Session started`, { sessionKey });
|
|
3219
|
-
const updatedMessages: ChatEntry[] = [
|
|
3220
|
-
...existing.messages,
|
|
3221
|
-
{
|
|
3222
|
-
kind: 'text',
|
|
3223
|
-
id: crypto.randomUUID(),
|
|
3224
|
-
role: 'user',
|
|
3225
|
-
text: content,
|
|
3226
|
-
attachments: imageCount > 0 ? { images: imageCount } : undefined
|
|
3227
|
-
}
|
|
3228
|
-
];
|
|
3229
|
-
next.set(sessionKey, {
|
|
3230
|
-
...existing,
|
|
3231
|
-
messages: updatedMessages,
|
|
3232
|
-
runState: 'running',
|
|
3233
|
-
activeRunId: null,
|
|
3234
|
-
runStartedAt: Date.now(),
|
|
3235
|
-
progress: '',
|
|
3236
|
-
progressKind: '',
|
|
3237
|
-
});
|
|
3238
|
-
return next;
|
|
3239
|
-
});
|
|
3240
|
-
clearInputForCurrentSession();
|
|
3241
|
-
clearPendingImages();
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
const currentSession = currentNotebookSessionKey ? sessions.get(currentNotebookSessionKey) : null;
|
|
3245
|
-
const messages = currentSession?.messages ?? [];
|
|
3246
|
-
const progress = currentSession?.progress ?? '';
|
|
3247
|
-
const progressKind = currentSession?.progressKind ?? '';
|
|
3248
|
-
const status: PanelStatus = socketConnected ? currentSession?.runState ?? 'ready' : 'disconnected';
|
|
3249
|
-
const displayPath = currentNotebookPath
|
|
3250
|
-
? currentNotebookPath.split('/').pop() || 'Untitled'
|
|
3251
|
-
: 'No notebook';
|
|
3252
|
-
const canSend =
|
|
3253
|
-
status === 'ready' &&
|
|
3254
|
-
currentNotebookPath.length > 0 &&
|
|
3255
|
-
currentSession?.pairedOk !== false;
|
|
3256
|
-
const runningSummary = status === 'running' ? progress || 'Working...' : '';
|
|
3257
|
-
const autoModelLabel = autoModel
|
|
3258
|
-
? findModelLabel(autoModel, modelOptions)
|
|
3259
|
-
: 'Auto';
|
|
3260
|
-
const selectedModelLabel =
|
|
3261
|
-
modelOption === '__config__'
|
|
3262
|
-
? autoModelLabel
|
|
3263
|
-
: findModelLabel(modelOption, modelOptions);
|
|
3264
|
-
const autoReasoningLabel = autoReasoningEffort
|
|
3265
|
-
? findReasoningLabel(autoReasoningEffort, reasoningOptions)
|
|
3266
|
-
: 'Auto';
|
|
3267
|
-
const selectedReasoningLabel =
|
|
3268
|
-
reasoningEffort === '__config__'
|
|
3269
|
-
? autoReasoningLabel
|
|
3270
|
-
: findReasoningLabel(reasoningEffort, reasoningOptions);
|
|
3271
|
-
const selectedSandboxLabel = SANDBOX_OPTIONS.find(option => option.value === sandboxMode)?.label ?? 'Permission';
|
|
3272
|
-
const notificationPermission = getBrowserNotificationPermission();
|
|
3273
|
-
const notificationsUnsupported = notificationPermission === 'unsupported';
|
|
3274
|
-
const minimumNotifyDurationLabel =
|
|
3275
|
-
notifyOnDoneMinSeconds === 0
|
|
3276
|
-
? 'All completed runs'
|
|
3277
|
-
: `Runs taking at least ${notifyOnDoneMinSeconds} second${notifyOnDoneMinSeconds === 1 ? '' : 's'}`;
|
|
3278
|
-
const notificationHelpText =
|
|
3279
|
-
notificationPermission === 'unsupported'
|
|
3280
|
-
? 'Browser notifications are not available in this environment.'
|
|
3281
|
-
: notificationPermission === 'denied'
|
|
3282
|
-
? 'Notifications are blocked for this site. Allow them in browser settings.'
|
|
3283
|
-
: notificationPermission === 'default'
|
|
3284
|
-
? 'Permission will be requested when enabling this option.'
|
|
3285
|
-
: `Shows a browser notification for ${minimumNotifyDurationLabel}.`;
|
|
3286
|
-
const canStop = status === 'running' && Boolean(currentSession?.activeRunId);
|
|
3287
|
-
const nowMs = Date.now();
|
|
3288
|
-
const rateUpdatedAtMs = safeParseDateMs(rateLimits?.updatedAt ?? null);
|
|
3289
|
-
const rateAgeMs = rateUpdatedAtMs == null ? null : nowMs - rateUpdatedAtMs;
|
|
3290
|
-
const usageIsStale = rateAgeMs == null ? true : rateAgeMs > 10 * 60 * 1000;
|
|
3291
|
-
const sessionLeftPercent = percentLeftFromUsed(rateLimits?.primary?.usedPercent ?? null);
|
|
3292
|
-
const weeklyLeftPercent = percentLeftFromUsed(rateLimits?.secondary?.usedPercent ?? null);
|
|
3293
|
-
const usageIsUnknown = rateUpdatedAtMs == null && sessionLeftPercent == null && weeklyLeftPercent == null;
|
|
3294
|
-
const sessionResetsIn = formatResetsIn(rateLimits?.primary?.resetsAt ?? null, nowMs);
|
|
3295
|
-
const weeklyResetsIn = formatResetsIn(rateLimits?.secondary?.resetsAt ?? null, nowMs);
|
|
3296
|
-
const usageIsOverdue =
|
|
3297
|
-
sessionResetsIn === 'Overdue' || weeklyResetsIn === 'Overdue' || (rateAgeMs != null && rateAgeMs > 60 * 60 * 1000);
|
|
3298
|
-
const batteryLevel = sessionLeftPercent == null ? null : sessionLeftPercent / 100;
|
|
3299
|
-
const usageUpdatedAgo = rateAgeMs == null ? 'Unknown' : `${formatDurationShort(rateAgeMs)} ago`;
|
|
3300
|
-
const sessionWindowMinutes = rateLimits?.primary?.windowMinutes ?? null;
|
|
3301
|
-
const weeklyWindowMinutes = rateLimits?.secondary?.windowMinutes ?? null;
|
|
3302
|
-
const sessionWindowLabel = sessionWindowMinutes == null ? '' : `Window: ${Math.round(sessionWindowMinutes / 60)}h`;
|
|
3303
|
-
const weeklyWindowLabel =
|
|
3304
|
-
weeklyWindowMinutes == null ? '' : `Window: ${Math.round(weeklyWindowMinutes / (60 * 24))}d`;
|
|
3305
|
-
|
|
3306
|
-
useLayoutEffect(() => {
|
|
3307
|
-
const target = notebookLabelRef.current;
|
|
3308
|
-
if (!target) {
|
|
3309
|
-
setIsNotebookLabelTruncated(false);
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
const update = (): void => {
|
|
3314
|
-
const next = target.scrollWidth - target.clientWidth > 1;
|
|
3315
|
-
setIsNotebookLabelTruncated(prev => (prev === next ? prev : next));
|
|
3316
|
-
};
|
|
3317
|
-
|
|
3318
|
-
update();
|
|
3319
|
-
|
|
3320
|
-
if (typeof ResizeObserver === 'undefined') {
|
|
3321
|
-
window.addEventListener('resize', update);
|
|
3322
|
-
return () => {
|
|
3323
|
-
window.removeEventListener('resize', update);
|
|
3324
|
-
};
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
const observer = new ResizeObserver(() => {
|
|
3328
|
-
update();
|
|
3329
|
-
});
|
|
3330
|
-
observer.observe(target);
|
|
3331
|
-
if (target.parentElement) {
|
|
3332
|
-
observer.observe(target.parentElement);
|
|
3333
|
-
}
|
|
3334
|
-
window.addEventListener('resize', update);
|
|
3335
|
-
|
|
3336
|
-
return () => {
|
|
3337
|
-
observer.disconnect();
|
|
3338
|
-
window.removeEventListener('resize', update);
|
|
3339
|
-
};
|
|
3340
|
-
}, [currentNotebookPath]);
|
|
3341
|
-
|
|
3342
|
-
return (
|
|
3343
|
-
<div className="jp-CodexChat">
|
|
3344
|
-
<div className="jp-CodexChat-header">
|
|
3345
|
-
<div className="jp-CodexChat-header-top">
|
|
3346
|
-
<div className="jp-CodexChat-header-left">
|
|
3347
|
-
<StatusPill status={status} />
|
|
3348
|
-
<span className="jp-CodexChat-notebookWrap" data-full-name={isNotebookLabelTruncated ? displayPath : undefined}>
|
|
3349
|
-
<span className="jp-CodexChat-notebook" ref={notebookLabelRef} title={isNotebookLabelTruncated ? displayPath : undefined}>
|
|
3350
|
-
{displayPath}
|
|
3351
|
-
</span>
|
|
3352
|
-
</span>
|
|
3353
|
-
</div>
|
|
3354
|
-
<div className="jp-CodexChat-header-actions">
|
|
3355
|
-
<button
|
|
3356
|
-
type="button"
|
|
3357
|
-
onClick={() => void startNewThread()}
|
|
3358
|
-
className="jp-CodexHeaderBtn"
|
|
3359
|
-
disabled={!currentNotebookPath || status === 'running'}
|
|
3360
|
-
aria-label="New thread"
|
|
3361
|
-
title="New thread"
|
|
3362
|
-
>
|
|
3363
|
-
<PlusIcon width={16} height={16} />
|
|
3364
|
-
</button>
|
|
3365
|
-
<div
|
|
3366
|
-
className="jp-CodexMenuWrap"
|
|
3367
|
-
ref={usageMenuWrapRef}
|
|
3368
|
-
>
|
|
3369
|
-
<button
|
|
3370
|
-
type="button"
|
|
3371
|
-
className={`jp-CodexHeaderBtn jp-CodexHeaderBtn-icon jp-CodexUsageBtn${usagePopoverOpen ? ' is-active is-open' : ''}${usageIsStale ? ' is-stale' : ''}${usageIsOverdue ? ' is-overdue' : ''}`}
|
|
3372
|
-
ref={usageBtnRef}
|
|
3373
|
-
onClick={() => toggleUsagePopover()}
|
|
3374
|
-
aria-label={sessionLeftPercent == null ? 'Codex usage' : `Codex usage: ${sessionLeftPercent}% left`}
|
|
3375
|
-
aria-haspopup="dialog"
|
|
3376
|
-
aria-expanded={usagePopoverOpen}
|
|
3377
|
-
title={
|
|
3378
|
-
sessionLeftPercent == null
|
|
3379
|
-
? 'Codex usage: unknown'
|
|
3380
|
-
: `Codex usage: ${sessionLeftPercent}% left (resets in ${sessionResetsIn})`
|
|
3381
|
-
}
|
|
3382
|
-
>
|
|
3383
|
-
<BatteryIcon level={batteryLevel} width={16} height={16} />
|
|
3384
|
-
</button>
|
|
3385
|
-
</div>
|
|
3386
|
-
<PortalMenu
|
|
3387
|
-
open={usagePopoverOpen}
|
|
3388
|
-
anchorRef={usageBtnRef}
|
|
3389
|
-
popoverRef={usagePopoverRef}
|
|
3390
|
-
className="jp-CodexUsagePopover"
|
|
3391
|
-
ariaLabel="Codex usage"
|
|
3392
|
-
role="dialog"
|
|
3393
|
-
align="right"
|
|
3394
|
-
>
|
|
3395
|
-
{(usageIsOverdue || usageIsStale) && (
|
|
3396
|
-
<div
|
|
3397
|
-
className={`jp-CodexUsageNotice${usageIsOverdue ? ' is-overdue' : usageIsStale ? ' is-stale' : ''}`}
|
|
3398
|
-
>
|
|
3399
|
-
<div className="jp-CodexUsageNoticeTitle">
|
|
3400
|
-
{usageIsUnknown
|
|
3401
|
-
? 'Usage unavailable'
|
|
3402
|
-
: usageIsOverdue
|
|
3403
|
-
? 'Overdue usage snapshot'
|
|
3404
|
-
: 'Stale usage snapshot'}
|
|
3405
|
-
</div>
|
|
3406
|
-
<div className="jp-CodexUsageNoticeBody">
|
|
3407
|
-
{usageIsUnknown ? 'Run Codex once to fetch usage limits.' : 'Run Codex again to refresh these numbers.'}
|
|
3408
|
-
</div>
|
|
3409
|
-
</div>
|
|
3410
|
-
)}
|
|
3411
|
-
|
|
3412
|
-
<div className="jp-CodexUsageSection">
|
|
3413
|
-
<div className="jp-CodexUsageSectionTop">
|
|
3414
|
-
<div className="jp-CodexUsageSectionTitle">Session</div>
|
|
3415
|
-
<div className="jp-CodexUsageSectionReset">Resets in {sessionResetsIn}</div>
|
|
3416
|
-
</div>
|
|
3417
|
-
<div className="jp-CodexUsageBar">
|
|
3418
|
-
<div
|
|
3419
|
-
className={`jp-CodexUsageBarFill${usageIsStale ? ' is-stale' : ''}`}
|
|
3420
|
-
style={{ width: `${sessionLeftPercent ?? 0}%` }}
|
|
3421
|
-
/>
|
|
3422
|
-
</div>
|
|
3423
|
-
<div className="jp-CodexUsageMeta">
|
|
3424
|
-
<div className="jp-CodexUsageMetaLeft">
|
|
3425
|
-
{sessionLeftPercent == null ? '--% left' : `${sessionLeftPercent}% left`}
|
|
3426
|
-
</div>
|
|
3427
|
-
<div className="jp-CodexUsageMetaRight">{sessionWindowLabel}</div>
|
|
3428
|
-
</div>
|
|
3429
|
-
</div>
|
|
3430
|
-
|
|
3431
|
-
<div className="jp-CodexMenuDivider" role="separator" />
|
|
3432
|
-
|
|
3433
|
-
<div className="jp-CodexUsageSection">
|
|
3434
|
-
<div className="jp-CodexUsageSectionTop">
|
|
3435
|
-
<div className="jp-CodexUsageSectionTitle">Weekly</div>
|
|
3436
|
-
<div className="jp-CodexUsageSectionReset">Resets in {weeklyResetsIn}</div>
|
|
3437
|
-
</div>
|
|
3438
|
-
<div className="jp-CodexUsageBar">
|
|
3439
|
-
<div
|
|
3440
|
-
className={`jp-CodexUsageBarFill${usageIsStale ? ' is-stale' : ''}`}
|
|
3441
|
-
style={{ width: `${weeklyLeftPercent ?? 0}%` }}
|
|
3442
|
-
/>
|
|
3443
|
-
</div>
|
|
3444
|
-
<div className="jp-CodexUsageMeta">
|
|
3445
|
-
<div className="jp-CodexUsageMetaLeft">
|
|
3446
|
-
{weeklyLeftPercent == null ? '--% left' : `${weeklyLeftPercent}% left`}
|
|
3447
|
-
</div>
|
|
3448
|
-
<div className="jp-CodexUsageMetaRight">{weeklyWindowLabel}</div>
|
|
3449
|
-
</div>
|
|
3450
|
-
</div>
|
|
3451
|
-
|
|
3452
|
-
<div className="jp-CodexUsageFooter">Last updated: {usageUpdatedAgo}</div>
|
|
3453
|
-
</PortalMenu>
|
|
3454
|
-
<button
|
|
3455
|
-
type="button"
|
|
3456
|
-
onClick={() => {
|
|
3457
|
-
setSettingsOpen(open => !open);
|
|
3458
|
-
setModelMenuOpen(false);
|
|
3459
|
-
setReasoningMenuOpen(false);
|
|
3460
|
-
setUsagePopoverOpen(false);
|
|
3461
|
-
setPermissionMenuOpen(false);
|
|
3462
|
-
}}
|
|
3463
|
-
className={`jp-CodexHeaderBtn jp-CodexHeaderBtn-icon${settingsOpen ? ' is-active' : ''}`}
|
|
3464
|
-
aria-label="Settings"
|
|
3465
|
-
aria-expanded={settingsOpen}
|
|
3466
|
-
title="Settings"
|
|
3467
|
-
>
|
|
3468
|
-
<GearIcon width={16} height={16} />
|
|
3469
|
-
</button>
|
|
3470
|
-
</div>
|
|
3471
|
-
</div>
|
|
3472
|
-
|
|
3473
|
-
{currentSession?.pairedOk === false && (
|
|
3474
|
-
<div className="jp-CodexPairingNotice" role="status" aria-live="polite">
|
|
3475
|
-
<div className="jp-CodexPairingNotice-title">Jupytext pairing required</div>
|
|
3476
|
-
<div className="jp-CodexPairingNotice-body">
|
|
3477
|
-
{currentSession.pairedMessage ||
|
|
3478
|
-
'This notebook must be paired (.ipynb ↔ .py) via Jupytext to enable running.'}
|
|
3479
|
-
</div>
|
|
3480
|
-
</div>
|
|
3481
|
-
)}
|
|
3482
|
-
|
|
3483
|
-
</div>
|
|
3484
|
-
|
|
3485
|
-
<div className="jp-CodexChat-body">
|
|
3486
|
-
<div className="jp-CodexChat-messages" ref={scrollRef} onScroll={onScrollMessages}>
|
|
3487
|
-
{status === 'disconnected' && (
|
|
3488
|
-
<div className="jp-CodexChat-message jp-CodexChat-system jp-CodexChat-reconnectNotice">
|
|
3489
|
-
<div className="jp-CodexChat-role">system</div>
|
|
3490
|
-
<div className="jp-CodexChat-text">Codex connection was lost. Reconnect to continue.</div>
|
|
3491
|
-
<button
|
|
3492
|
-
type="button"
|
|
3493
|
-
className="jp-CodexReconnectBtn"
|
|
3494
|
-
onClick={() => reconnectSocket()}
|
|
3495
|
-
disabled={isReconnecting}
|
|
3496
|
-
aria-label={isReconnecting ? 'Codex reconnecting' : 'Reconnect to Codex'}
|
|
3497
|
-
title={isReconnecting ? 'Attempting to reconnect...' : 'Reconnect to Codex'}
|
|
3498
|
-
>
|
|
3499
|
-
{isReconnecting ? 'Connecting...' : 'Reconnect'}
|
|
3500
|
-
</button>
|
|
3501
|
-
</div>
|
|
3502
|
-
)}
|
|
3503
|
-
{messages.length === 0 && (
|
|
3504
|
-
<div className="jp-CodexChat-message jp-CodexChat-system">
|
|
3505
|
-
<div className="jp-CodexChat-role">system</div>
|
|
3506
|
-
<div className="jp-CodexChat-text">Select a notebook, then start a conversation.</div>
|
|
3507
|
-
</div>
|
|
3508
|
-
)}
|
|
3509
|
-
{messages.map(entry => {
|
|
3510
|
-
if (entry.kind === 'text') {
|
|
3511
|
-
const systemVariant =
|
|
3512
|
-
entry.role === 'system'
|
|
3513
|
-
? isSessionStartNotice(entry.text)
|
|
3514
|
-
? ' is-success'
|
|
3515
|
-
: ''
|
|
3516
|
-
: '';
|
|
3517
|
-
const imageCount = entry.attachments?.images ?? 0;
|
|
3518
|
-
return (
|
|
3519
|
-
<div
|
|
3520
|
-
key={entry.id}
|
|
3521
|
-
className={`jp-CodexChat-message jp-CodexChat-${entry.role}${systemVariant}`}
|
|
3522
|
-
>
|
|
3523
|
-
<div className="jp-CodexChat-role">{entry.role}</div>
|
|
3524
|
-
<MessageText text={entry.text} canCopyCode={entry.role === 'assistant'} />
|
|
3525
|
-
{imageCount > 0 && (
|
|
3526
|
-
<div
|
|
3527
|
-
className="jp-CodexChat-attachments"
|
|
3528
|
-
aria-label={`${imageCount} image attachment(s)`}
|
|
3529
|
-
title={`${imageCount} image attachment(s)`}
|
|
3530
|
-
>
|
|
3531
|
-
<span className="jp-CodexChat-attachmentPill">
|
|
3532
|
-
<ImageIcon width={14} height={14} />
|
|
3533
|
-
<span className="jp-CodexChat-attachmentCount">{imageCount}</span>
|
|
3534
|
-
</span>
|
|
3535
|
-
</div>
|
|
3536
|
-
)}
|
|
3537
|
-
</div>
|
|
3538
|
-
);
|
|
3539
|
-
}
|
|
3540
|
-
if (entry.kind === 'run-divider') {
|
|
3541
|
-
return (
|
|
3542
|
-
<div key={entry.id} className="jp-CodexRunDivider" role="separator" aria-label="Run duration">
|
|
3543
|
-
<span className="jp-CodexRunDividerLabel">Worked for {formatRunDuration(entry.elapsedMs)}</span>
|
|
3544
|
-
</div>
|
|
3545
|
-
);
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
const item = entry.item;
|
|
3549
|
-
const trimmedDetail = (item.detail || '').trim();
|
|
3550
|
-
const isExpandable = Boolean(trimmedDetail);
|
|
3551
|
-
const activityClassName = `jp-CodexChat-message jp-CodexChat-activity${
|
|
3552
|
-
isExpandable ? ' is-expandable' : ''
|
|
3553
|
-
} is-${item.category}${item.phase ? ` is-${item.phase}` : ''}`;
|
|
3554
|
-
const icon =
|
|
3555
|
-
item.phase === 'completed' ? (
|
|
3556
|
-
<CheckIcon width={14} height={14} />
|
|
3557
|
-
) : item.phase === 'started' ? (
|
|
3558
|
-
<span className="jp-CodexActivityDot" />
|
|
3559
|
-
) : (
|
|
3560
|
-
<span className="jp-CodexActivityDot is-idle" />
|
|
3561
|
-
);
|
|
3562
|
-
|
|
3563
|
-
const summaryContent = (
|
|
3564
|
-
<>
|
|
3565
|
-
<span className="jp-CodexActivityLineIcon" aria-hidden="true">
|
|
3566
|
-
{icon}
|
|
3567
|
-
</span>
|
|
3568
|
-
<span className="jp-CodexActivityLineText">
|
|
3569
|
-
<span className="jp-CodexActivityLineTitle">{item.title}</span>
|
|
3570
|
-
</span>
|
|
3571
|
-
</>
|
|
3572
|
-
);
|
|
3573
|
-
|
|
3574
|
-
if (isExpandable) {
|
|
3575
|
-
return (
|
|
3576
|
-
<details
|
|
3577
|
-
key={entry.id}
|
|
3578
|
-
className={activityClassName}
|
|
3579
|
-
role="status"
|
|
3580
|
-
aria-live="polite"
|
|
3581
|
-
>
|
|
3582
|
-
<summary className="jp-CodexActivitySummary">{summaryContent}</summary>
|
|
3583
|
-
<div className="jp-CodexActivityBody">
|
|
3584
|
-
<pre className="jp-CodexActivityCode">
|
|
3585
|
-
<code>{trimmedDetail}</code>
|
|
3586
|
-
</pre>
|
|
3587
|
-
</div>
|
|
3588
|
-
</details>
|
|
3589
|
-
);
|
|
3590
|
-
}
|
|
3591
|
-
return (
|
|
3592
|
-
<div
|
|
3593
|
-
key={entry.id}
|
|
3594
|
-
className={activityClassName}
|
|
3595
|
-
role="status"
|
|
3596
|
-
aria-live="polite"
|
|
3597
|
-
>
|
|
3598
|
-
<div className="jp-CodexActivitySummary jp-CodexActivitySummaryStatic">
|
|
3599
|
-
{summaryContent}
|
|
3600
|
-
</div>
|
|
3601
|
-
</div>
|
|
3602
|
-
);
|
|
3603
|
-
})}
|
|
3604
|
-
|
|
3605
|
-
{status === 'running' && (
|
|
3606
|
-
<div
|
|
3607
|
-
className={`jp-CodexChat-loading${progressKind === 'reasoning' ? ' is-reasoning' : ''}`}
|
|
3608
|
-
aria-label={progressKind === 'reasoning' ? 'Reasoning' : 'Running'}
|
|
3609
|
-
>
|
|
3610
|
-
<div className="jp-CodexChat-loading-dots">
|
|
3611
|
-
<span></span>
|
|
3612
|
-
<span></span>
|
|
3613
|
-
<span></span>
|
|
3614
|
-
</div>
|
|
3615
|
-
</div>
|
|
3616
|
-
)}
|
|
3617
|
-
|
|
3618
|
-
<div ref={endRef} />
|
|
3619
|
-
</div>
|
|
3620
|
-
</div>
|
|
3621
|
-
|
|
3622
|
-
<div className="jp-CodexChat-input">
|
|
3623
|
-
<div className={`jp-CodexJumpBar${isAtBottom ? '' : ' is-visible'}`}>
|
|
3624
|
-
<button
|
|
3625
|
-
type="button"
|
|
3626
|
-
className="jp-CodexJumpToLatestBtn"
|
|
3627
|
-
onClick={scrollToBottom}
|
|
3628
|
-
aria-label="Jump to latest"
|
|
3629
|
-
aria-hidden={isAtBottom}
|
|
3630
|
-
tabIndex={isAtBottom ? -1 : 0}
|
|
3631
|
-
title="Jump to latest"
|
|
3632
|
-
>
|
|
3633
|
-
<ArrowDownIcon width={20} height={20} />
|
|
3634
|
-
</button>
|
|
3635
|
-
</div>
|
|
3636
|
-
<div className="jp-CodexComposer">
|
|
3637
|
-
<textarea
|
|
3638
|
-
ref={composerTextareaRef}
|
|
3639
|
-
value={input}
|
|
3640
|
-
onChange={e => {
|
|
3641
|
-
updateInput(e.currentTarget.value);
|
|
3642
|
-
// Resize using the current target so typing feels immediate.
|
|
3643
|
-
window.requestAnimationFrame(() => autosizeComposerTextarea(e.currentTarget));
|
|
3644
|
-
}}
|
|
3645
|
-
onPaste={onComposerPaste}
|
|
3646
|
-
placeholder={
|
|
3647
|
-
currentSession?.pairedOk === false
|
|
3648
|
-
? 'Disabled: missing Jupytext paired file (.py)'
|
|
3649
|
-
: currentNotebookPath
|
|
3650
|
-
? 'Ask Codex...'
|
|
3651
|
-
: 'Select a notebook first'
|
|
3652
|
-
}
|
|
3653
|
-
rows={1}
|
|
3654
|
-
onKeyDown={e => {
|
|
3655
|
-
// Avoid interfering with IME composition (Korean/Japanese/etc.)
|
|
3656
|
-
const native = e.nativeEvent as unknown as { isComposing?: boolean; keyCode?: number };
|
|
3657
|
-
if (native.isComposing || native.keyCode === 229) {
|
|
3658
|
-
return;
|
|
3659
|
-
}
|
|
3660
|
-
if (e.key === 'Enter' && !e.shiftKey && canSend && (input.trim() || pendingImages.length > 0)) {
|
|
3661
|
-
e.preventDefault();
|
|
3662
|
-
void sendMessage();
|
|
3663
|
-
}
|
|
3664
|
-
}}
|
|
3665
|
-
/>
|
|
3666
|
-
{pendingImages.length > 0 && (
|
|
3667
|
-
<div className="jp-CodexComposer-attachments" role="group" aria-label="Attachments">
|
|
3668
|
-
{pendingImages.map(image => (
|
|
3669
|
-
<div key={image.id} className="jp-CodexComposer-attachment">
|
|
3670
|
-
<img src={image.previewUrl} alt={image.file.name || 'Pasted image'} />
|
|
3671
|
-
<button
|
|
3672
|
-
type="button"
|
|
3673
|
-
className="jp-CodexComposer-attachmentRemove"
|
|
3674
|
-
onClick={() => removePendingImage(image.id)}
|
|
3675
|
-
aria-label="Remove image"
|
|
3676
|
-
title="Remove image"
|
|
3677
|
-
>
|
|
3678
|
-
<XIcon width={14} height={14} />
|
|
3679
|
-
</button>
|
|
3680
|
-
</div>
|
|
3681
|
-
))}
|
|
3682
|
-
</div>
|
|
3683
|
-
)}
|
|
3684
|
-
<div className="jp-CodexComposer-toolbar">
|
|
3685
|
-
<div className="jp-CodexComposer-toolbarLeft">
|
|
3686
|
-
<div className="jp-CodexMenuWrap jp-CodexModelWrap" ref={modelMenuWrapRef}>
|
|
3687
|
-
<button
|
|
3688
|
-
type="button"
|
|
3689
|
-
ref={modelBtnRef}
|
|
3690
|
-
className={`jp-CodexModelBtn ${modelMenuOpen ? 'is-open' : ''}`}
|
|
3691
|
-
onClick={() => {
|
|
3692
|
-
setModelMenuOpen(open => !open);
|
|
3693
|
-
setReasoningMenuOpen(false);
|
|
3694
|
-
setUsagePopoverOpen(false);
|
|
3695
|
-
setPermissionMenuOpen(false);
|
|
3696
|
-
}}
|
|
3697
|
-
disabled={status === 'running'}
|
|
3698
|
-
aria-label={`Model: ${selectedModelLabel}`}
|
|
3699
|
-
aria-haspopup="menu"
|
|
3700
|
-
aria-expanded={modelMenuOpen}
|
|
3701
|
-
title={`Model: ${selectedModelLabel}`}
|
|
3702
|
-
>
|
|
3703
|
-
<span className="jp-CodexModelBtn-label">{selectedModelLabel}</span>
|
|
3704
|
-
</button>
|
|
3705
|
-
</div>
|
|
3706
|
-
<PortalMenu
|
|
3707
|
-
open={modelMenuOpen}
|
|
3708
|
-
anchorRef={modelBtnRef}
|
|
3709
|
-
popoverRef={modelPopoverRef}
|
|
3710
|
-
role="menu"
|
|
3711
|
-
ariaLabel="Model"
|
|
3712
|
-
align="left"
|
|
3713
|
-
>
|
|
3714
|
-
{modelOptions.length === 0 && (
|
|
3715
|
-
<div className="jp-CodexMenuItem">No models available</div>
|
|
3716
|
-
)}
|
|
3717
|
-
{modelOptions.map(option => {
|
|
3718
|
-
const inferred =
|
|
3719
|
-
modelOption === '__config__' && autoModel && modelOptions.some(option => option.value === autoModel)
|
|
3720
|
-
? (autoModel as ModelOptionValue)
|
|
3721
|
-
: modelOption;
|
|
3722
|
-
const isActive = inferred === option.value;
|
|
3723
|
-
return (
|
|
3724
|
-
<button
|
|
3725
|
-
key={option.value}
|
|
3726
|
-
type="button"
|
|
3727
|
-
className={`jp-CodexMenuItem ${isActive ? 'is-active' : ''}`}
|
|
3728
|
-
onClick={() => {
|
|
3729
|
-
setModelOption(option.value);
|
|
3730
|
-
setModelMenuOpen(false);
|
|
3731
|
-
}}
|
|
3732
|
-
>
|
|
3733
|
-
<span className="jp-CodexMenuItemLabel">{option.label}</span>
|
|
3734
|
-
{isActive && (
|
|
3735
|
-
<CheckIcon className="jp-CodexMenuCheck" width={16} height={16} />
|
|
3736
|
-
)}
|
|
3737
|
-
</button>
|
|
3738
|
-
);
|
|
3739
|
-
})}
|
|
3740
|
-
</PortalMenu>
|
|
3741
|
-
|
|
3742
|
-
<div className="jp-CodexMenuWrap" ref={reasoningMenuWrapRef}>
|
|
3743
|
-
<button
|
|
3744
|
-
type="button"
|
|
3745
|
-
ref={reasoningBtnRef}
|
|
3746
|
-
className={`jp-CodexIconBtn ${reasoningMenuOpen ? 'is-open' : ''}`}
|
|
3747
|
-
onClick={() => {
|
|
3748
|
-
setReasoningMenuOpen(open => !open);
|
|
3749
|
-
setModelMenuOpen(false);
|
|
3750
|
-
setUsagePopoverOpen(false);
|
|
3751
|
-
setPermissionMenuOpen(false);
|
|
3752
|
-
}}
|
|
3753
|
-
disabled={status === 'running'}
|
|
3754
|
-
aria-label={`Reasoning: ${selectedReasoningLabel}`}
|
|
3755
|
-
aria-haspopup="menu"
|
|
3756
|
-
aria-expanded={reasoningMenuOpen}
|
|
3757
|
-
title={`Reasoning: ${selectedReasoningLabel}`}
|
|
3758
|
-
>
|
|
3759
|
-
<ReasoningEffortIcon
|
|
3760
|
-
effort={
|
|
3761
|
-
(reasoningEffort === '__config__' && autoReasoningEffort
|
|
3762
|
-
? autoReasoningEffort
|
|
3763
|
-
: reasoningEffort) as ReasoningOptionValue
|
|
3764
|
-
}
|
|
3765
|
-
effortOptions={reasoningOptions}
|
|
3766
|
-
width={18}
|
|
3767
|
-
height={18}
|
|
3768
|
-
/>
|
|
3769
|
-
</button>
|
|
3770
|
-
</div>
|
|
3771
|
-
<PortalMenu
|
|
3772
|
-
open={reasoningMenuOpen}
|
|
3773
|
-
anchorRef={reasoningBtnRef}
|
|
3774
|
-
popoverRef={reasoningPopoverRef}
|
|
3775
|
-
role="menu"
|
|
3776
|
-
ariaLabel="Reasoning"
|
|
3777
|
-
align="left"
|
|
3778
|
-
>
|
|
3779
|
-
{reasoningOptions.length === 0 && (
|
|
3780
|
-
<div className="jp-CodexMenuItem">No reasoning options</div>
|
|
3781
|
-
)}
|
|
3782
|
-
{reasoningOptions.map(option => {
|
|
3783
|
-
const inferred =
|
|
3784
|
-
reasoningEffort === '__config__' && autoReasoningEffort ? autoReasoningEffort : reasoningEffort;
|
|
3785
|
-
const isActive = inferred === option.value;
|
|
3786
|
-
return (
|
|
3787
|
-
<button
|
|
3788
|
-
key={option.value}
|
|
3789
|
-
type="button"
|
|
3790
|
-
className={`jp-CodexMenuItem ${isActive ? 'is-active' : ''}`}
|
|
3791
|
-
onClick={() => {
|
|
3792
|
-
setReasoningEffort(option.value);
|
|
3793
|
-
setReasoningMenuOpen(false);
|
|
3794
|
-
}}
|
|
3795
|
-
>
|
|
3796
|
-
<span className="jp-CodexMenuItemLabel">{option.label}</span>
|
|
3797
|
-
{isActive && (
|
|
3798
|
-
<CheckIcon className="jp-CodexMenuCheck" width={16} height={16} />
|
|
3799
|
-
)}
|
|
3800
|
-
</button>
|
|
3801
|
-
);
|
|
3802
|
-
})}
|
|
3803
|
-
</PortalMenu>
|
|
3804
|
-
|
|
3805
|
-
<div className="jp-CodexMenuWrap" ref={permissionMenuWrapRef}>
|
|
3806
|
-
<button
|
|
3807
|
-
type="button"
|
|
3808
|
-
className={`jp-CodexIconBtn jp-CodexPermissionBtn${permissionMenuOpen ? ' is-open' : ''}${sandboxMode === 'danger-full-access' ? ' is-danger' : ''}`}
|
|
3809
|
-
ref={permissionBtnRef}
|
|
3810
|
-
onClick={() => {
|
|
3811
|
-
setPermissionMenuOpen(open => !open);
|
|
3812
|
-
setModelMenuOpen(false);
|
|
3813
|
-
setReasoningMenuOpen(false);
|
|
3814
|
-
setUsagePopoverOpen(false);
|
|
3815
|
-
}}
|
|
3816
|
-
disabled={status === 'running'}
|
|
3817
|
-
aria-label={`Permission: ${selectedSandboxLabel}`}
|
|
3818
|
-
aria-haspopup="menu"
|
|
3819
|
-
aria-expanded={permissionMenuOpen}
|
|
3820
|
-
title={`Permission: ${selectedSandboxLabel}`}
|
|
3821
|
-
>
|
|
3822
|
-
<ShieldIcon width={18} height={18} />
|
|
3823
|
-
</button>
|
|
3824
|
-
</div>
|
|
3825
|
-
<PortalMenu
|
|
3826
|
-
open={permissionMenuOpen}
|
|
3827
|
-
anchorRef={permissionBtnRef}
|
|
3828
|
-
popoverRef={permissionPopoverRef}
|
|
3829
|
-
role="menu"
|
|
3830
|
-
ariaLabel="Permissions"
|
|
3831
|
-
align="right"
|
|
3832
|
-
>
|
|
3833
|
-
{SANDBOX_OPTIONS.map(option => (
|
|
3834
|
-
<button
|
|
3835
|
-
key={option.value}
|
|
3836
|
-
type="button"
|
|
3837
|
-
className={`jp-CodexMenuItem ${sandboxMode === option.value ? 'is-active' : ''}`}
|
|
3838
|
-
onClick={() => {
|
|
3839
|
-
setSandboxMode(option.value);
|
|
3840
|
-
setPermissionMenuOpen(false);
|
|
3841
|
-
}}
|
|
3842
|
-
>
|
|
3843
|
-
<span className="jp-CodexMenuItemLabel">{option.label}</span>
|
|
3844
|
-
{sandboxMode === option.value && <CheckIcon className="jp-CodexMenuCheck" width={16} height={16} />}
|
|
3845
|
-
</button>
|
|
3846
|
-
))}
|
|
3847
|
-
</PortalMenu>
|
|
3848
|
-
</div>
|
|
3849
|
-
|
|
3850
|
-
<div className="jp-CodexComposer-toolbarRight">
|
|
3851
|
-
<button
|
|
3852
|
-
type="button"
|
|
3853
|
-
className={`jp-CodexSendBtn${status === 'running' ? ' is-stop' : ''}`}
|
|
3854
|
-
onClick={() => {
|
|
3855
|
-
if (status === 'running') {
|
|
3856
|
-
cancelRun();
|
|
3857
|
-
return;
|
|
3858
|
-
}
|
|
3859
|
-
void sendMessage();
|
|
3860
|
-
}}
|
|
3861
|
-
disabled={status === 'running' ? !canStop : !canSend || (!input.trim() && pendingImages.length === 0)}
|
|
3862
|
-
aria-label={status === 'running' ? 'Stop run' : 'Send'}
|
|
3863
|
-
title={
|
|
3864
|
-
status === 'running'
|
|
3865
|
-
? currentSession?.activeRunId
|
|
3866
|
-
? `runId: ${currentSession.activeRunId}`
|
|
3867
|
-
: 'Waiting for run id...'
|
|
3868
|
-
: status === 'disconnected'
|
|
3869
|
-
? 'Connecting...'
|
|
3870
|
-
: 'Send'
|
|
3871
|
-
}
|
|
3872
|
-
>
|
|
3873
|
-
{status === 'running' ? (
|
|
3874
|
-
<StopIcon width={18} height={18} />
|
|
3875
|
-
) : (
|
|
3876
|
-
<ArrowUpIcon width={18} height={18} />
|
|
3877
|
-
)}
|
|
3878
|
-
</button>
|
|
3879
|
-
</div>
|
|
3880
|
-
</div>
|
|
3881
|
-
</div>
|
|
3882
|
-
</div>
|
|
3883
|
-
|
|
3884
|
-
{settingsOpen && (
|
|
3885
|
-
<div className="jp-CodexSettingsOverlay" role="dialog" aria-modal="true" aria-label="Settings">
|
|
3886
|
-
<button
|
|
3887
|
-
type="button"
|
|
3888
|
-
className="jp-CodexSettingsBackdrop"
|
|
3889
|
-
onClick={() => setSettingsOpen(false)}
|
|
3890
|
-
aria-label="Dismiss settings"
|
|
3891
|
-
title="Dismiss settings"
|
|
3892
|
-
/>
|
|
3893
|
-
<div className="jp-CodexSettingsPanel" onClick={e => e.stopPropagation()}>
|
|
3894
|
-
<div className="jp-CodexSettingsPanel-top">
|
|
3895
|
-
<div className="jp-CodexSettingsPanel-title">Settings</div>
|
|
3896
|
-
<button
|
|
3897
|
-
type="button"
|
|
3898
|
-
className="jp-CodexHeaderBtn jp-CodexHeaderBtn-icon"
|
|
3899
|
-
onClick={() => setSettingsOpen(false)}
|
|
3900
|
-
aria-label="Close settings"
|
|
3901
|
-
title="Close"
|
|
3902
|
-
>
|
|
3903
|
-
<XIcon width={16} height={16} />
|
|
3904
|
-
</button>
|
|
3905
|
-
</div>
|
|
3906
|
-
<div className="jp-CodexSettingsPanel-sections">
|
|
3907
|
-
<section className="jp-CodexSettingsSection" aria-label="General settings">
|
|
3908
|
-
<div className="jp-CodexSettingsSection-title">General</div>
|
|
3909
|
-
<label className="jp-CodexSettingsField">
|
|
3910
|
-
<span className="jp-CodexSettingsField-label">Codex command path</span>
|
|
3911
|
-
<input
|
|
3912
|
-
type="text"
|
|
3913
|
-
className="jp-CodexChat-model-input"
|
|
3914
|
-
placeholder="codex"
|
|
3915
|
-
value={commandPath}
|
|
3916
|
-
disabled={status === 'running'}
|
|
3917
|
-
onChange={e => setCommandPath(e.currentTarget.value.trimStart())}
|
|
3918
|
-
title="Leave empty to use PATH lookup."
|
|
3919
|
-
/>
|
|
3920
|
-
<span className="jp-CodexSettingsField-help">Leave empty to use PATH lookup.</span>
|
|
3921
|
-
</label>
|
|
3922
|
-
</section>
|
|
3923
|
-
|
|
3924
|
-
<section className="jp-CodexSettingsSection" aria-label="Message options">
|
|
3925
|
-
<div className="jp-CodexSettingsSection-title">Message Options</div>
|
|
3926
|
-
<div className="jp-CodexSettingsOptions">
|
|
3927
|
-
<label className="jp-CodexChat-toggle">
|
|
3928
|
-
<input
|
|
3929
|
-
type="checkbox"
|
|
3930
|
-
checked={autoSaveBeforeSend}
|
|
3931
|
-
onChange={e => setAutoSaveBeforeSend(e.currentTarget.checked)}
|
|
3932
|
-
disabled={status === 'running'}
|
|
3933
|
-
/>
|
|
3934
|
-
Auto-save before send
|
|
3935
|
-
</label>
|
|
3936
|
-
<label className="jp-CodexChat-toggle">
|
|
3937
|
-
<input
|
|
3938
|
-
type="checkbox"
|
|
3939
|
-
checked={includeActiveCell}
|
|
3940
|
-
onChange={e => setIncludeActiveCell(e.currentTarget.checked)}
|
|
3941
|
-
disabled={status === 'running'}
|
|
3942
|
-
/>
|
|
3943
|
-
Include active cell
|
|
3944
|
-
</label>
|
|
3945
|
-
<label className="jp-CodexChat-toggle">
|
|
3946
|
-
<input
|
|
3947
|
-
type="checkbox"
|
|
3948
|
-
checked={includeActiveCellOutput}
|
|
3949
|
-
onChange={e => setIncludeActiveCellOutput(e.currentTarget.checked)}
|
|
3950
|
-
disabled={status === 'running' || !includeActiveCell}
|
|
3951
|
-
/>
|
|
3952
|
-
Include active cell output
|
|
3953
|
-
</label>
|
|
3954
|
-
</div>
|
|
3955
|
-
</section>
|
|
3956
|
-
|
|
3957
|
-
<section className="jp-CodexSettingsSection" aria-label="Notification options">
|
|
3958
|
-
<div className="jp-CodexSettingsSection-title">NOTIFICATION</div>
|
|
3959
|
-
<div className="jp-CodexSettingsOptions">
|
|
3960
|
-
<label className="jp-CodexChat-toggle">
|
|
3961
|
-
<input
|
|
3962
|
-
type="checkbox"
|
|
3963
|
-
checked={notifyOnDone}
|
|
3964
|
-
onChange={e => void updateNotifyOnDone(e.currentTarget.checked)}
|
|
3965
|
-
disabled={status === 'running' || notificationsUnsupported}
|
|
3966
|
-
/>
|
|
3967
|
-
Notify when run finishes
|
|
3968
|
-
</label>
|
|
3969
|
-
<label className="jp-CodexSettingsField">
|
|
3970
|
-
<span className="jp-CodexSettingsField-label">Minimum runtime (seconds)</span>
|
|
3971
|
-
<input
|
|
3972
|
-
type="number"
|
|
3973
|
-
min="0"
|
|
3974
|
-
step="1"
|
|
3975
|
-
className="jp-CodexChat-model-input"
|
|
3976
|
-
value={notifyOnDoneMinSeconds}
|
|
3977
|
-
onChange={e =>
|
|
3978
|
-
setNotifyOnDoneMinSeconds(
|
|
3979
|
-
Number.isFinite(Number(e.currentTarget.value)) ? Math.max(0, Math.floor(Number(e.currentTarget.value))) : 0
|
|
3980
|
-
)
|
|
3981
|
-
}
|
|
3982
|
-
disabled={notificationsUnsupported}
|
|
3983
|
-
/>
|
|
3984
|
-
<span className="jp-CodexSettingsField-help">
|
|
3985
|
-
0 means notify for every finished run. Enter only when you want delayed notifications.
|
|
3986
|
-
</span>
|
|
3987
|
-
</label>
|
|
3988
|
-
<span className="jp-CodexSettingsField-help">{notificationHelpText}</span>
|
|
3989
|
-
</div>
|
|
3990
|
-
</section>
|
|
3991
|
-
|
|
3992
|
-
<section className="jp-CodexSettingsSection jp-CodexSettingsSection-danger" aria-label="Danger zone">
|
|
3993
|
-
<div className="jp-CodexSettingsSection-title jp-CodexSettingsSection-title-danger">Danger Zone</div>
|
|
3994
|
-
<div className="jp-CodexSettingsPanel-stats">
|
|
3995
|
-
<span className="jp-CodexSettingsPanel-stat">
|
|
3996
|
-
Saved conversations: {storedThreadCount}
|
|
3997
|
-
</span>
|
|
3998
|
-
</div>
|
|
3999
|
-
<div className="jp-CodexSettingsDanger">
|
|
4000
|
-
<button
|
|
4001
|
-
type="button"
|
|
4002
|
-
className="jp-CodexBtn jp-CodexBtn-xs jp-CodexBtn-danger"
|
|
4003
|
-
onClick={() => void clearAllSessions()}
|
|
4004
|
-
disabled={status === 'running' || (storedThreadCount === 0 && sessions.size === 0)}
|
|
4005
|
-
title={
|
|
4006
|
-
status === 'running'
|
|
4007
|
-
? 'Cannot delete while a run is in progress.'
|
|
4008
|
-
: 'Delete all saved conversations'
|
|
4009
|
-
}
|
|
4010
|
-
>
|
|
4011
|
-
Delete all
|
|
4012
|
-
</button>
|
|
4013
|
-
</div>
|
|
4014
|
-
</section>
|
|
4015
|
-
</div>
|
|
4016
|
-
</div>
|
|
4017
|
-
</div>
|
|
4018
|
-
)}
|
|
4019
|
-
</div>
|
|
4020
|
-
);
|
|
4021
|
-
}
|
|
4022
|
-
|
|
4023
|
-
function getNotebookPath(notebooks: INotebookTracker): string {
|
|
4024
|
-
return notebooks.currentWidget ? notebooks.currentWidget.context.path : '';
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
|
-
function getActiveCellText(notebooks: INotebookTracker): string {
|
|
4028
|
-
const widget = notebooks.currentWidget;
|
|
4029
|
-
if (!widget) {
|
|
4030
|
-
return '';
|
|
4031
|
-
}
|
|
4032
|
-
const activeCell = widget.content.activeCell;
|
|
4033
|
-
return activeCell ? activeCell.model.sharedModel.getSource() : '';
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
const ACTIVE_CELL_OUTPUT_MAX_CHARS = 6000;
|
|
4037
|
-
const ACTIVE_CELL_OUTPUT_MAX_ITEMS = 24;
|
|
4038
|
-
|
|
4039
|
-
function stripAnsi(value: string): string {
|
|
4040
|
-
// Best-effort removal of ANSI escape codes (tracebacks sometimes include them).
|
|
4041
|
-
return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
4042
|
-
}
|
|
4043
|
-
|
|
4044
|
-
function coerceText(value: unknown): string {
|
|
4045
|
-
if (typeof value === 'string') {
|
|
4046
|
-
return value;
|
|
4047
|
-
}
|
|
4048
|
-
if (Array.isArray(value)) {
|
|
4049
|
-
return value.filter(item => typeof item === 'string').join('');
|
|
4050
|
-
}
|
|
4051
|
-
return '';
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
function formatJupyterOutput(output: any): string {
|
|
4055
|
-
if (!output || typeof output !== 'object') {
|
|
4056
|
-
return '';
|
|
4057
|
-
}
|
|
4058
|
-
const outputType = typeof output.output_type === 'string' ? output.output_type : '';
|
|
4059
|
-
if (!outputType) {
|
|
4060
|
-
return '';
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
if (outputType === 'stream') {
|
|
4064
|
-
const text = coerceText(output.text);
|
|
4065
|
-
if (!text) {
|
|
4066
|
-
return '';
|
|
4067
|
-
}
|
|
4068
|
-
const name = typeof output.name === 'string' ? output.name : '';
|
|
4069
|
-
const cleaned = stripAnsi(text).replace(/\s+$/, '');
|
|
4070
|
-
if (!cleaned) {
|
|
4071
|
-
return '';
|
|
4072
|
-
}
|
|
4073
|
-
return name === 'stderr' ? `[stderr]\n${cleaned}` : cleaned;
|
|
4074
|
-
}
|
|
4075
|
-
|
|
4076
|
-
if (outputType === 'error') {
|
|
4077
|
-
const traceback = Array.isArray(output.traceback)
|
|
4078
|
-
? output.traceback.filter((line: unknown) => typeof line === 'string')
|
|
4079
|
-
: [];
|
|
4080
|
-
const tbText = stripAnsi(traceback.join('\n')).replace(/\s+$/, '');
|
|
4081
|
-
if (tbText) {
|
|
4082
|
-
return tbText;
|
|
4083
|
-
}
|
|
4084
|
-
const ename = typeof output.ename === 'string' ? output.ename : '';
|
|
4085
|
-
const evalue = typeof output.evalue === 'string' ? output.evalue : '';
|
|
4086
|
-
const summary = [ename, evalue].filter(Boolean).join(': ').trim();
|
|
4087
|
-
return summary;
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
if (outputType === 'execute_result' || outputType === 'display_data' || outputType === 'update_display_data') {
|
|
4091
|
-
const data = output.data && typeof output.data === 'object' ? output.data : null;
|
|
4092
|
-
if (!data) {
|
|
4093
|
-
return '';
|
|
4094
|
-
}
|
|
4095
|
-
const textPlain = coerceText((data as any)['text/plain']);
|
|
4096
|
-
if (textPlain) {
|
|
4097
|
-
const cleaned = stripAnsi(textPlain).replace(/\s+$/, '');
|
|
4098
|
-
if (!cleaned) {
|
|
4099
|
-
return '';
|
|
4100
|
-
}
|
|
4101
|
-
if (outputType === 'execute_result' && typeof output.execution_count === 'number') {
|
|
4102
|
-
return `Out[${output.execution_count}]:\n${cleaned}`;
|
|
4103
|
-
}
|
|
4104
|
-
return cleaned;
|
|
4105
|
-
}
|
|
4106
|
-
|
|
4107
|
-
const mimeTypes = Object.keys(data as any).filter(mime => mime && mime !== 'text/plain');
|
|
4108
|
-
if (mimeTypes.length > 0) {
|
|
4109
|
-
return `[non-text output omitted: ${mimeTypes.slice(0, 6).join(', ')}${mimeTypes.length > 6 ? ', ...' : ''}]`;
|
|
4110
|
-
}
|
|
4111
|
-
return '';
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
|
-
return '';
|
|
4115
|
-
}
|
|
4116
|
-
|
|
4117
|
-
function summarizeJupyterOutputs(outputs: any[]): string {
|
|
4118
|
-
if (!Array.isArray(outputs) || outputs.length === 0) {
|
|
4119
|
-
return '';
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
let combined = '';
|
|
4123
|
-
let appended = 0;
|
|
4124
|
-
let truncated = false;
|
|
4125
|
-
|
|
4126
|
-
for (const output of outputs) {
|
|
4127
|
-
if (appended >= ACTIVE_CELL_OUTPUT_MAX_ITEMS) {
|
|
4128
|
-
truncated = true;
|
|
4129
|
-
break;
|
|
4130
|
-
}
|
|
4131
|
-
const chunk = formatJupyterOutput(output);
|
|
4132
|
-
if (!chunk) {
|
|
4133
|
-
continue;
|
|
4134
|
-
}
|
|
4135
|
-
appended += 1;
|
|
4136
|
-
|
|
4137
|
-
const sep = combined ? '\n\n' : '';
|
|
4138
|
-
const remaining = ACTIVE_CELL_OUTPUT_MAX_CHARS - combined.length - sep.length;
|
|
4139
|
-
if (remaining <= 0) {
|
|
4140
|
-
truncated = true;
|
|
4141
|
-
break;
|
|
4142
|
-
}
|
|
4143
|
-
|
|
4144
|
-
const slice = chunk.length > remaining ? chunk.slice(0, remaining) : chunk;
|
|
4145
|
-
combined += sep + slice;
|
|
4146
|
-
if (slice.length !== chunk.length) {
|
|
4147
|
-
truncated = true;
|
|
4148
|
-
break;
|
|
4149
|
-
}
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
combined = combined.replace(/\s+$/, '');
|
|
4153
|
-
if (!combined) {
|
|
4154
|
-
return '';
|
|
4155
|
-
}
|
|
4156
|
-
if (truncated) {
|
|
4157
|
-
combined += '\n\n... (truncated)';
|
|
4158
|
-
}
|
|
4159
|
-
return combined;
|
|
4160
|
-
}
|
|
4161
|
-
|
|
4162
|
-
function getActiveCellOutput(notebooks: INotebookTracker): string {
|
|
4163
|
-
const widget = notebooks.currentWidget;
|
|
4164
|
-
if (!widget) {
|
|
4165
|
-
return '';
|
|
4166
|
-
}
|
|
4167
|
-
const activeCell = widget.content.activeCell;
|
|
4168
|
-
if (!activeCell) {
|
|
4169
|
-
return '';
|
|
4170
|
-
}
|
|
4171
|
-
|
|
4172
|
-
try {
|
|
4173
|
-
const model: any = activeCell.model as any;
|
|
4174
|
-
const cellType = typeof model?.type === 'string' ? model.type : '';
|
|
4175
|
-
if (cellType && cellType !== 'code') {
|
|
4176
|
-
return '';
|
|
4177
|
-
}
|
|
4178
|
-
const json = typeof model?.toJSON === 'function' ? model.toJSON() : null;
|
|
4179
|
-
const outputs = json && Array.isArray((json as any).outputs) ? (json as any).outputs : [];
|
|
4180
|
-
return summarizeJupyterOutputs(outputs);
|
|
4181
|
-
} catch {
|
|
4182
|
-
return '';
|
|
4183
|
-
}
|
|
4184
|
-
}
|
|
1
|
+
export { CodexPanel } from './codexChat';
|