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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { test, expect } = require('@playwright/test');
|
|
3
|
+
|
|
4
|
+
const DOCS = [
|
|
5
|
+
{ path: 'tests/e2e/fixtures/notebooks/tab1.py', name: 'tab1.py', kind: 'py' },
|
|
6
|
+
{ path: 'tests/e2e/fixtures/notebooks/tab2.py', name: 'tab2.py', kind: 'py' },
|
|
7
|
+
{ path: 'tests/e2e/fixtures/notebooks/tab3.py', name: 'tab3.py', kind: 'py' },
|
|
8
|
+
{ path: 'tests/e2e/fixtures/notebooks/tab4.py', name: 'tab4.py', kind: 'py' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const STRESS_PROMPT = '양자역학을 시뮬레이팅하는 코드를 작성해줘';
|
|
12
|
+
|
|
13
|
+
function escapeRegExp(value) {
|
|
14
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildInitialDocumentUrl(baseLabUrl, documentPath) {
|
|
18
|
+
const parsed = new URL(baseLabUrl);
|
|
19
|
+
const basePath = parsed.pathname.replace(/\/$/, '');
|
|
20
|
+
parsed.pathname = `${basePath}/tree/${documentPath}`;
|
|
21
|
+
parsed.searchParams.set('reset', '1');
|
|
22
|
+
return parsed.toString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function dismissBlockingDialogs(page) {
|
|
26
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
27
|
+
const dialog = page.locator('dialog.jp-Dialog, [role="dialog"].jp-Dialog, [role="dialog"]').first();
|
|
28
|
+
if (!(await dialog.count()) || !(await dialog.isVisible())) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const clickedNoKernel = await page.evaluate(() => {
|
|
33
|
+
const buttons = Array.from(
|
|
34
|
+
document.querySelectorAll('dialog.jp-Dialog button, [role="dialog"] button')
|
|
35
|
+
);
|
|
36
|
+
const target = buttons.find(button => /No Kernel/i.test(button.textContent || ''));
|
|
37
|
+
if (!target) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
target.click();
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
if (clickedNoKernel) {
|
|
44
|
+
await page.waitForTimeout(120);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const clickedSelect = await page.evaluate(() => {
|
|
49
|
+
const buttons = Array.from(
|
|
50
|
+
document.querySelectorAll('dialog.jp-Dialog button, [role="dialog"] button')
|
|
51
|
+
);
|
|
52
|
+
const target = buttons.find(button => /^Select$/i.test((button.textContent || '').trim()));
|
|
53
|
+
if (!target) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
target.click();
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
if (clickedSelect) {
|
|
60
|
+
await page.waitForTimeout(120);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const clickedCancel = await page.evaluate(() => {
|
|
65
|
+
const buttons = Array.from(
|
|
66
|
+
document.querySelectorAll('dialog.jp-Dialog button, [role="dialog"] button')
|
|
67
|
+
);
|
|
68
|
+
const target = buttons.find(button => /^Cancel$/i.test((button.textContent || '').trim()));
|
|
69
|
+
if (!target) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
target.click();
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
if (clickedCancel) {
|
|
76
|
+
await page.waitForTimeout(120);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await page.keyboard.press('Escape');
|
|
81
|
+
await page.waitForTimeout(120);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function ensureFileBrowserVisible(page) {
|
|
86
|
+
await dismissBlockingDialogs(page);
|
|
87
|
+
const fileBrowserTab = page.getByRole('tab', { name: /File Browser/i }).first();
|
|
88
|
+
if (await fileBrowserTab.count()) {
|
|
89
|
+
const isActiveBefore = await fileBrowserTab.evaluate(
|
|
90
|
+
element =>
|
|
91
|
+
element.getAttribute('aria-selected') === 'true' ||
|
|
92
|
+
(typeof element.className === 'string' && element.className.includes('lm-mod-current'))
|
|
93
|
+
);
|
|
94
|
+
if (!isActiveBefore) {
|
|
95
|
+
await fileBrowserTab.click();
|
|
96
|
+
}
|
|
97
|
+
const isActiveAfter = await fileBrowserTab.evaluate(
|
|
98
|
+
element =>
|
|
99
|
+
element.getAttribute('aria-selected') === 'true' ||
|
|
100
|
+
(typeof element.className === 'string' && element.className.includes('lm-mod-current'))
|
|
101
|
+
);
|
|
102
|
+
if (!isActiveAfter) {
|
|
103
|
+
await fileBrowserTab.click();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
await expect(page.locator('.jp-BreadCrumbs-home')).toBeVisible({ timeout: 10000 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function openDocumentFromFileBrowser(page, documentName) {
|
|
110
|
+
await ensureFileBrowserVisible(page);
|
|
111
|
+
let item = page
|
|
112
|
+
.getByRole('listitem', { name: new RegExp(`\\b${escapeRegExp(documentName)}\\b`, 'i') })
|
|
113
|
+
.first();
|
|
114
|
+
if (!(await item.count())) {
|
|
115
|
+
item = page.locator('.jp-DirListing-item', { hasText: documentName }).first();
|
|
116
|
+
}
|
|
117
|
+
if (!(await item.count())) {
|
|
118
|
+
item = page.locator('.jp-FileBrowser').getByText(documentName, { exact: true }).first();
|
|
119
|
+
}
|
|
120
|
+
await expect(item).toBeVisible({ timeout: 20000 });
|
|
121
|
+
await dismissBlockingDialogs(page);
|
|
122
|
+
await item.dblclick();
|
|
123
|
+
await dismissBlockingDialogs(page);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function activateDocumentTab(page, documentName) {
|
|
127
|
+
await dismissBlockingDialogs(page);
|
|
128
|
+
const exactTab = page.getByRole('tab', { name: new RegExp(`^${escapeRegExp(documentName)}$`) }).first();
|
|
129
|
+
await expect(exactTab).toBeVisible({ timeout: 20000 });
|
|
130
|
+
await exactTab.click();
|
|
131
|
+
await expect(page.locator('.jp-CodexChat-notebook')).toHaveText(documentName, { timeout: 20000 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function ensureCodexPanel(page) {
|
|
135
|
+
await dismissBlockingDialogs(page);
|
|
136
|
+
const composer = page.locator('.jp-CodexPanel .jp-CodexComposer textarea');
|
|
137
|
+
if ((await composer.count()) > 0 && (await composer.first().isVisible())) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const codexSideTab = page
|
|
142
|
+
.locator('.jp-SideBar.jp-mod-right .lm-TabBar-tab[title="Codex"], .jp-SideBar.jp-mod-right .lm-TabBar-tab')
|
|
143
|
+
.filter({ hasText: 'Codex' })
|
|
144
|
+
.first();
|
|
145
|
+
await expect(codexSideTab).toBeVisible({ timeout: 20000 });
|
|
146
|
+
await codexSideTab.click();
|
|
147
|
+
|
|
148
|
+
await expect(composer.first()).toBeVisible({ timeout: 20000 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function sendMessage(page, text) {
|
|
152
|
+
await dismissBlockingDialogs(page);
|
|
153
|
+
const composer = page.locator('.jp-CodexComposer textarea');
|
|
154
|
+
const sendBtn = page.locator('.jp-CodexSendBtn:visible');
|
|
155
|
+
|
|
156
|
+
await expect(composer).toBeVisible({ timeout: 20000 });
|
|
157
|
+
await composer.fill(text);
|
|
158
|
+
const isEnabled = await sendBtn.isEnabled();
|
|
159
|
+
if (!isEnabled) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
await sendBtn.click();
|
|
163
|
+
await expect(page.locator('.jp-CodexSendBtn.is-stop')).toBeVisible({ timeout: 15000 });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function sendMessageFromCurrentTab(page, text, options = {}) {
|
|
168
|
+
const sendAllowed = options.sendAllowed ?? true;
|
|
169
|
+
await dismissBlockingDialogs(page);
|
|
170
|
+
const composer = page.locator('.jp-CodexComposer textarea');
|
|
171
|
+
const sendBtn = page.locator('.jp-CodexSendBtn:visible');
|
|
172
|
+
|
|
173
|
+
await expect(composer).toBeVisible({ timeout: 20000 });
|
|
174
|
+
await composer.fill(text);
|
|
175
|
+
|
|
176
|
+
const isEnabled = await sendBtn.isEnabled();
|
|
177
|
+
if (!sendAllowed) {
|
|
178
|
+
expect(isEnabled).toBe(false);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (!isEnabled) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await sendBtn.click();
|
|
186
|
+
await expect(page.locator('.jp-CodexSendBtn.is-stop')).toBeVisible({ timeout: 15000 });
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function keepLastPyTabInteractive(page, durationMs = 15000) {
|
|
191
|
+
const editor = page
|
|
192
|
+
.locator('.jp-FileEditor .cm-content:visible, .jp-CodeMirrorEditor .cm-content:visible')
|
|
193
|
+
.first();
|
|
194
|
+
await expect(editor).toBeVisible({ timeout: 20000 });
|
|
195
|
+
|
|
196
|
+
const probe = `PW_UI_RESPONSIVE_PROBE_MARKER_${Date.now()}`;
|
|
197
|
+
await editor.click({ timeout: 5000 });
|
|
198
|
+
await page.keyboard.type(`\n# ${probe}`);
|
|
199
|
+
await expect(editor).toContainText(probe, { timeout: 5000 });
|
|
200
|
+
|
|
201
|
+
const deadline = Date.now() + durationMs;
|
|
202
|
+
let round = 0;
|
|
203
|
+
while (Date.now() < deadline) {
|
|
204
|
+
try {
|
|
205
|
+
await editor.click({ timeout: 1000 });
|
|
206
|
+
const marker = `PW_UI_RESPONSIVE_ROUND_${round}`;
|
|
207
|
+
await page.keyboard.type(`\n# ${marker}`);
|
|
208
|
+
await expect(editor).toContainText(marker, { timeout: 1000 });
|
|
209
|
+
await page.mouse.wheel(0, 420);
|
|
210
|
+
await page.mouse.wheel(0, -420);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const dismissed = await page.evaluate(() => {
|
|
213
|
+
const buttons = Array.from(document.querySelectorAll('dialog.jp-Dialog button, [role="dialog"] button'));
|
|
214
|
+
const target = buttons.find(button =>
|
|
215
|
+
/^(No Kernel|Cancel|Reload|Select)$/i.test((button.textContent || '').trim())
|
|
216
|
+
);
|
|
217
|
+
if (!target) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
target.click();
|
|
221
|
+
return true;
|
|
222
|
+
});
|
|
223
|
+
if (!dismissed) {
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
round += 1;
|
|
228
|
+
await page.waitForTimeout(80);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
test('4 notebook-tab rapid sends with immediate tab switching keep final .py tab interactive', async ({ page, baseURL }) => {
|
|
233
|
+
const codexCommandPath =
|
|
234
|
+
process.env.PLAYWRIGHT_CODEX_COMMAND || path.resolve(__dirname, 'mock-codex-cli-flood.py');
|
|
235
|
+
const targetUrl = baseURL || process.env.JUPYTERLAB_URL || 'http://127.0.0.1:8888/lab';
|
|
236
|
+
const interactiveMsRaw = Number(process.env.PLAYWRIGHT_INTERACTIVE_MS || '60000');
|
|
237
|
+
const interactiveMs = Number.isFinite(interactiveMsRaw) ? Math.max(1000, Math.floor(interactiveMsRaw)) : 18000;
|
|
238
|
+
const initialDocumentUrl = buildInitialDocumentUrl(targetUrl, DOCS[0].path);
|
|
239
|
+
const pageErrors = [];
|
|
240
|
+
const codexConsoleErrors = [];
|
|
241
|
+
|
|
242
|
+
page.on('pageerror', err => {
|
|
243
|
+
pageErrors.push(String(err?.message || err));
|
|
244
|
+
});
|
|
245
|
+
page.on('console', msg => {
|
|
246
|
+
if (msg.type() !== 'error') {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const text = msg.text();
|
|
250
|
+
if (text.includes('[Codex] onSocketMessage failed') || text.includes('Internal UI error')) {
|
|
251
|
+
codexConsoleErrors.push(text);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await page.addInitScript(commandPath => {
|
|
256
|
+
window.localStorage.setItem('jupyterlab-codex:command-path', commandPath);
|
|
257
|
+
window.localStorage.setItem('jupyterlab-codex:model', 'gpt-5.3-codex');
|
|
258
|
+
window.localStorage.setItem('jupyterlab-codex:reasoning-effort', 'high');
|
|
259
|
+
}, codexCommandPath);
|
|
260
|
+
|
|
261
|
+
await page.goto(initialDocumentUrl, { waitUntil: 'domcontentloaded' });
|
|
262
|
+
await page.waitForSelector('main[aria-label="Main Content"], .jp-LabShell, .lm-DockPanel', {
|
|
263
|
+
timeout: 30000
|
|
264
|
+
});
|
|
265
|
+
await dismissBlockingDialogs(page);
|
|
266
|
+
await ensureCodexPanel(page);
|
|
267
|
+
|
|
268
|
+
for (const document of DOCS.slice(1)) {
|
|
269
|
+
await openDocumentFromFileBrowser(page, document.name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const document of DOCS.slice(0, 1)) {
|
|
273
|
+
await activateDocumentTab(page, document.name);
|
|
274
|
+
const sent = await sendMessage(page, STRESS_PROMPT);
|
|
275
|
+
expect(sent).toBe(true);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (let round = 0; round < 5; round += 1) {
|
|
279
|
+
for (const document of DOCS.slice(1)) {
|
|
280
|
+
await activateDocumentTab(page, document.name);
|
|
281
|
+
const sent = await sendMessageFromCurrentTab(page, STRESS_PROMPT, { sendAllowed: false });
|
|
282
|
+
expect(sent).toBe(false);
|
|
283
|
+
await expect(page.locator('.jp-CodexSendBtn:visible')).toBeDisabled({ timeout: 5000 });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const lastDoc = DOCS[DOCS.length - 1];
|
|
288
|
+
await activateDocumentTab(page, lastDoc.name);
|
|
289
|
+
await keepLastPyTabInteractive(page, interactiveMs);
|
|
290
|
+
|
|
291
|
+
await expect(page.locator('.jp-CodexChat-reconnectNotice')).toHaveCount(0);
|
|
292
|
+
await expect(page.getByText('Internal UI error while processing a server message')).toHaveCount(0);
|
|
293
|
+
expect(pageErrors).toEqual([]);
|
|
294
|
+
expect(codexConsoleErrors).toEqual([]);
|
|
295
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
12
|
+
sys.stdout.write(json.dumps(payload) + "\n")
|
|
13
|
+
sys.stdout.flush()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run_app_server() -> int:
|
|
17
|
+
for raw_line in sys.stdin:
|
|
18
|
+
line = raw_line.strip()
|
|
19
|
+
if not line:
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
req = json.loads(line)
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
req_id = req.get("id")
|
|
27
|
+
method = req.get("method")
|
|
28
|
+
if method == "initialize" and req_id is not None:
|
|
29
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {"capabilities": {}}})
|
|
30
|
+
continue
|
|
31
|
+
if method == "model/list" and req_id is not None:
|
|
32
|
+
_emit(
|
|
33
|
+
{
|
|
34
|
+
"jsonrpc": "2.0",
|
|
35
|
+
"id": req_id,
|
|
36
|
+
"result": {
|
|
37
|
+
"data": [
|
|
38
|
+
{
|
|
39
|
+
"model": "gpt-5.3-codex",
|
|
40
|
+
"displayName": "GPT-5.3 Codex",
|
|
41
|
+
"supportedReasoningEfforts": ["low", "medium", "high"],
|
|
42
|
+
"defaultReasoningEffort": "high",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
if method == "shutdown" and req_id is not None:
|
|
50
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _int_env(name: str, default: int, minimum: int) -> int:
|
|
57
|
+
raw = os.environ.get(name, str(default)).strip()
|
|
58
|
+
try:
|
|
59
|
+
value = int(raw)
|
|
60
|
+
except ValueError:
|
|
61
|
+
value = default
|
|
62
|
+
return max(minimum, value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_exec_mode() -> int:
|
|
66
|
+
event_count = _int_env("MOCK_CODEX_EVENT_COUNT", 320, 20)
|
|
67
|
+
event_delay_ms = _int_env("MOCK_CODEX_EVENT_DELAY_MS", 20, 0)
|
|
68
|
+
chunk_words = _int_env("MOCK_CODEX_CHUNK_WORDS", 14, 4)
|
|
69
|
+
|
|
70
|
+
exit_code_raw = os.environ.get("MOCK_CODEX_EXIT_CODE", "0").strip()
|
|
71
|
+
try:
|
|
72
|
+
exit_code = int(exit_code_raw)
|
|
73
|
+
except ValueError:
|
|
74
|
+
exit_code = 0
|
|
75
|
+
|
|
76
|
+
prompt = sys.stdin.read().strip()
|
|
77
|
+
prompt_preview = prompt[:120] if prompt else "(empty prompt)"
|
|
78
|
+
repeated = " ".join(["analysis"] * chunk_words)
|
|
79
|
+
thread_id = f"mock-thread-{uuid.uuid4().hex[:12]}"
|
|
80
|
+
|
|
81
|
+
_emit({"type": "thread.started", "thread_id": thread_id})
|
|
82
|
+
_emit({"type": "item.started", "item": {"type": "reasoning", "title": "Long reasoning started"}})
|
|
83
|
+
|
|
84
|
+
for idx in range(event_count):
|
|
85
|
+
step = idx + 1
|
|
86
|
+
_emit(
|
|
87
|
+
{
|
|
88
|
+
"type": "item.completed",
|
|
89
|
+
"item": {
|
|
90
|
+
"type": "agent_message",
|
|
91
|
+
"text": f"[{step}/{event_count}] {prompt_preview} :: {repeated}",
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
_emit(
|
|
96
|
+
{
|
|
97
|
+
"type": "item.started",
|
|
98
|
+
"item": {"type": "command", "title": f"Tool step {step} started", "command": f"python -m check {step}"},
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
_emit(
|
|
102
|
+
{
|
|
103
|
+
"type": "item.completed",
|
|
104
|
+
"item": {
|
|
105
|
+
"type": "command",
|
|
106
|
+
"title": f"Tool step {step} completed",
|
|
107
|
+
"command": f"python -m check {step}",
|
|
108
|
+
"exit_code": 0,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
if event_delay_ms > 0:
|
|
113
|
+
time.sleep(event_delay_ms / 1000.0)
|
|
114
|
+
|
|
115
|
+
_emit({"type": "item.completed", "item": {"type": "reasoning", "title": "Long reasoning completed"}})
|
|
116
|
+
return exit_code
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main() -> int:
|
|
120
|
+
argv = sys.argv[1:]
|
|
121
|
+
if len(argv) >= 1 and argv[0] == "app-server":
|
|
122
|
+
return _run_app_server()
|
|
123
|
+
return _run_exec_mode()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
10
|
+
sys.stdout.write(json.dumps(payload) + "\n")
|
|
11
|
+
sys.stdout.flush()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run_app_server() -> int:
|
|
15
|
+
for raw_line in sys.stdin:
|
|
16
|
+
line = raw_line.strip()
|
|
17
|
+
if not line:
|
|
18
|
+
continue
|
|
19
|
+
try:
|
|
20
|
+
req = json.loads(line)
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
req_id = req.get("id")
|
|
25
|
+
method = req.get("method")
|
|
26
|
+
if method == "initialize" and req_id is not None:
|
|
27
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {"capabilities": {}}})
|
|
28
|
+
continue
|
|
29
|
+
if method == "model/list" and req_id is not None:
|
|
30
|
+
_emit(
|
|
31
|
+
{
|
|
32
|
+
"jsonrpc": "2.0",
|
|
33
|
+
"id": req_id,
|
|
34
|
+
"result": {
|
|
35
|
+
"data": [
|
|
36
|
+
{
|
|
37
|
+
"model": "mock-gpt",
|
|
38
|
+
"displayName": "Mock GPT",
|
|
39
|
+
"supportedReasoningEfforts": ["low", "medium", "high"],
|
|
40
|
+
"defaultReasoningEffort": "medium",
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
continue
|
|
47
|
+
if method == "shutdown" and req_id is not None:
|
|
48
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_exec_mode() -> int:
|
|
55
|
+
prompt = sys.stdin.read()
|
|
56
|
+
thread_id = f"mock-thread-{uuid.uuid4().hex[:12]}"
|
|
57
|
+
head = prompt[:800]
|
|
58
|
+
tail = prompt[-800:] if prompt else ""
|
|
59
|
+
|
|
60
|
+
_emit({"type": "thread.started", "thread_id": thread_id})
|
|
61
|
+
_emit(
|
|
62
|
+
{
|
|
63
|
+
"type": "item.completed",
|
|
64
|
+
"item": {
|
|
65
|
+
"type": "agent_message",
|
|
66
|
+
"text": (
|
|
67
|
+
"PROMPT_HEAD_START\n"
|
|
68
|
+
f"{head}\n"
|
|
69
|
+
"PROMPT_HEAD_END\n"
|
|
70
|
+
"PROMPT_TAIL_START\n"
|
|
71
|
+
f"{tail}\n"
|
|
72
|
+
"PROMPT_TAIL_END"
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> int:
|
|
81
|
+
argv = sys.argv[1:]
|
|
82
|
+
if len(argv) >= 1 and argv[0] == "app-server":
|
|
83
|
+
return _run_app_server()
|
|
84
|
+
return _run_exec_mode()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
12
|
+
sys.stdout.write(json.dumps(payload) + "\n")
|
|
13
|
+
sys.stdout.flush()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run_app_server() -> int:
|
|
17
|
+
for raw_line in sys.stdin:
|
|
18
|
+
line = raw_line.strip()
|
|
19
|
+
if not line:
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
req = json.loads(line)
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
req_id = req.get("id")
|
|
27
|
+
method = req.get("method")
|
|
28
|
+
if method == "initialize" and req_id is not None:
|
|
29
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {"capabilities": {}}})
|
|
30
|
+
continue
|
|
31
|
+
if method == "model/list" and req_id is not None:
|
|
32
|
+
_emit(
|
|
33
|
+
{
|
|
34
|
+
"jsonrpc": "2.0",
|
|
35
|
+
"id": req_id,
|
|
36
|
+
"result": {
|
|
37
|
+
"data": [
|
|
38
|
+
{
|
|
39
|
+
"model": "mock-gpt",
|
|
40
|
+
"displayName": "Mock GPT",
|
|
41
|
+
"supportedReasoningEfforts": ["low", "medium", "high"],
|
|
42
|
+
"defaultReasoningEffort": "medium",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
if method == "shutdown" and req_id is not None:
|
|
50
|
+
_emit({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_exec_mode() -> int:
|
|
57
|
+
try:
|
|
58
|
+
delay_ms = int(os.environ.get("MOCK_CODEX_DELAY_MS", "2600"))
|
|
59
|
+
except ValueError:
|
|
60
|
+
delay_ms = 2600
|
|
61
|
+
delay_ms = max(200, delay_ms)
|
|
62
|
+
exit_code_raw = os.environ.get("MOCK_CODEX_EXIT_CODE", "0")
|
|
63
|
+
try:
|
|
64
|
+
exit_code = int(exit_code_raw)
|
|
65
|
+
except ValueError:
|
|
66
|
+
exit_code = 0
|
|
67
|
+
|
|
68
|
+
prompt = sys.stdin.read().strip()
|
|
69
|
+
thread_id = f"mock-thread-{uuid.uuid4().hex[:12]}"
|
|
70
|
+
|
|
71
|
+
_emit({"type": "thread.started", "thread_id": thread_id})
|
|
72
|
+
_emit({"type": "item.started", "item": {"type": "reasoning", "title": "Mock run started"}})
|
|
73
|
+
time.sleep(delay_ms / 2000.0)
|
|
74
|
+
_emit(
|
|
75
|
+
{
|
|
76
|
+
"type": "item.completed",
|
|
77
|
+
"item": {
|
|
78
|
+
"type": "agent_message",
|
|
79
|
+
"text": f"Mock response for: {prompt[:120] if prompt else '(empty prompt)'}",
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
time.sleep(delay_ms / 2000.0)
|
|
84
|
+
return exit_code
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main() -> int:
|
|
88
|
+
argv = sys.argv[1:]
|
|
89
|
+
if len(argv) >= 1 and argv[0] == "app-server":
|
|
90
|
+
return _run_app_server()
|
|
91
|
+
return _run_exec_mode()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
raise SystemExit(main())
|