jupyterlab-codex-sidebar 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jupyterlab-playwright.log +43 -0
- package/README.md +19 -3
- package/jupyterlab_codex/labextension/package.json +2 -2
- package/jupyterlab_codex/labextension/static/737.58bcf09100c9bc7bd90d.js +1 -0
- package/jupyterlab_codex/labextension/static/{remoteEntry.c1e865f207776f7f24ff.js → remoteEntry.34af8a0df422b4d029c3.js} +1 -1
- package/jupyterlab_codex/sessions.py +1 -1
- package/lib/codexChat.js +148 -52
- package/lib/codexChat.js.map +1 -1
- package/lib/codexChatAttachmentLimit.d.ts +12 -2
- package/lib/codexChatAttachmentLimit.js +43 -30
- package/lib/codexChatAttachmentLimit.js.map +1 -1
- 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 +4 -1
- package/lib/codexChatDocumentUtils.js +71 -19
- package/lib/codexChatDocumentUtils.js.map +1 -1
- package/lib/codexChatPrimitives.d.ts +4 -1
- package/lib/codexChatPrimitives.js +4 -0
- package/lib/codexChatPrimitives.js.map +1 -1
- package/package.json +1 -1
- package/playwright.config.cjs +4 -1
- package/pyproject.toml +1 -1
- package/src/codexChat.tsx +234 -75
- package/src/codexChatAttachmentLimit.ts +59 -33
- package/src/codexChatAttachmentState.ts +37 -0
- package/src/codexChatDocumentUtils.ts +89 -21
- package/src/codexChatPrimitives.tsx +25 -1
- package/style/index.css +96 -40
- package/test-results/.last-run.json +4 -0
- package/test.py +0 -0
- package/tests/e2e/cell-output-error-tail.spec.js +165 -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/mock-codex-cli-prompt-echo.py +92 -0
- package/tests/unit/codexChatAttachmentLimit.spec.ts +33 -8
- package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
- package/tests/unit/codexChatDocumentUtils.spec.ts +78 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/jupyterlab_codex/labextension/static/855.d20f6158cd81bb4c9056.js +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { test, expect } = require('@playwright/test');
|
|
4
|
+
|
|
5
|
+
const NOTEBOOK_RELATIVE_PATH = 'tests/e2e/fixtures/notebooks/error-output-tail.ipynb';
|
|
6
|
+
const NOTEBOOK_NAME = 'error-output-tail.ipynb';
|
|
7
|
+
const NOTEBOOK_PY_PATH = path.resolve(__dirname, 'fixtures/notebooks/error-output-tail.py');
|
|
8
|
+
const NOTEBOOK_IPYNB_PATH = path.resolve(__dirname, 'fixtures/notebooks/error-output-tail.ipynb');
|
|
9
|
+
const TRACE_HEAD_MARKER = 'TRACE_HEAD_MARKER_ABCDEFGHIJ';
|
|
10
|
+
const TRACE_TAIL_MARKER = 'TRACE_TAIL_MARKER_ZYXWVUTSRQ';
|
|
11
|
+
|
|
12
|
+
function buildInitialNotebookUrl(baseLabUrl, notebookPath) {
|
|
13
|
+
const parsed = new URL(baseLabUrl);
|
|
14
|
+
const basePath = parsed.pathname.replace(/\/$/, '');
|
|
15
|
+
parsed.pathname = `${basePath}/tree/${notebookPath}`;
|
|
16
|
+
parsed.searchParams.set('reset', '1');
|
|
17
|
+
return parsed.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function dismissBlockingDialogs(page) {
|
|
21
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
22
|
+
const dialog = page.locator('dialog.jp-Dialog, [role="dialog"].jp-Dialog, [role="dialog"]').first();
|
|
23
|
+
if (!(await dialog.count()) || !(await dialog.isVisible())) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const noKernelButton = page.getByRole('button', { name: /^No Kernel$/ }).first();
|
|
28
|
+
if ((await noKernelButton.count()) > 0 && (await noKernelButton.isVisible())) {
|
|
29
|
+
await noKernelButton.click();
|
|
30
|
+
await page.waitForTimeout(150);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await page.keyboard.press('Escape');
|
|
35
|
+
await page.waitForTimeout(150);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ensureCodexPanel(page) {
|
|
40
|
+
await dismissBlockingDialogs(page);
|
|
41
|
+
const composer = page.locator('.jp-CodexPanel .jp-CodexComposer textarea');
|
|
42
|
+
if ((await composer.count()) > 0 && (await composer.first().isVisible())) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const codexSideTab = page
|
|
47
|
+
.locator('.jp-SideBar.jp-mod-right .lm-TabBar-tab[title="Codex"], .jp-SideBar.jp-mod-right .lm-TabBar-tab')
|
|
48
|
+
.filter({ hasText: 'Codex' })
|
|
49
|
+
.first();
|
|
50
|
+
await expect(codexSideTab).toBeVisible({ timeout: 20000 });
|
|
51
|
+
await codexSideTab.click();
|
|
52
|
+
await expect(composer.first()).toBeVisible({ timeout: 20000 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeNotebookFixture() {
|
|
56
|
+
const traceback = [
|
|
57
|
+
'Traceback (most recent call last):',
|
|
58
|
+
` ${TRACE_HEAD_MARKER}`,
|
|
59
|
+
`${'A'.repeat(22000)}`,
|
|
60
|
+
`ValueError: ${TRACE_TAIL_MARKER}`
|
|
61
|
+
];
|
|
62
|
+
const notebook = {
|
|
63
|
+
cells: [
|
|
64
|
+
{
|
|
65
|
+
cell_type: 'code',
|
|
66
|
+
execution_count: 1,
|
|
67
|
+
id: 'error-output-tail-cell',
|
|
68
|
+
metadata: {},
|
|
69
|
+
outputs: [
|
|
70
|
+
{
|
|
71
|
+
output_type: 'error',
|
|
72
|
+
ename: 'ValueError',
|
|
73
|
+
evalue: TRACE_TAIL_MARKER,
|
|
74
|
+
traceback
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
source: ['raise ValueError("synthetic traceback for tail truncation test")\n']
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
metadata: {
|
|
81
|
+
jupytext: {
|
|
82
|
+
formats: 'ipynb,py:percent'
|
|
83
|
+
},
|
|
84
|
+
kernelspec: {
|
|
85
|
+
display_name: 'Python 3 (ipykernel)',
|
|
86
|
+
language: 'python',
|
|
87
|
+
name: 'python3'
|
|
88
|
+
},
|
|
89
|
+
language_info: {
|
|
90
|
+
codemirror_mode: {
|
|
91
|
+
name: 'ipython',
|
|
92
|
+
version: 3
|
|
93
|
+
},
|
|
94
|
+
file_extension: '.py',
|
|
95
|
+
mimetype: 'text/x-python',
|
|
96
|
+
name: 'python',
|
|
97
|
+
nbconvert_exporter: 'python',
|
|
98
|
+
pygments_lexer: 'ipython3',
|
|
99
|
+
version: '3.13.9'
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
nbformat: 4,
|
|
103
|
+
nbformat_minor: 5
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(NOTEBOOK_IPYNB_PATH, `${JSON.stringify(notebook, null, 2)}\n`, 'utf8');
|
|
107
|
+
fs.writeFileSync(
|
|
108
|
+
NOTEBOOK_PY_PATH,
|
|
109
|
+
[
|
|
110
|
+
'# %%',
|
|
111
|
+
'raise ValueError("synthetic traceback for tail truncation test")',
|
|
112
|
+
''
|
|
113
|
+
].join('\n'),
|
|
114
|
+
'utf8'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test('error cell output sends the tail of the traceback to Codex', async ({ page, baseURL }) => {
|
|
119
|
+
const originalNotebook = fs.readFileSync(NOTEBOOK_IPYNB_PATH, 'utf8');
|
|
120
|
+
const originalPy = fs.readFileSync(NOTEBOOK_PY_PATH, 'utf8');
|
|
121
|
+
const codexCommandPath =
|
|
122
|
+
process.env.PLAYWRIGHT_CODEX_COMMAND || path.resolve(__dirname, 'mock-codex-cli-prompt-echo.py');
|
|
123
|
+
const targetUrl = baseURL || process.env.JUPYTERLAB_URL || 'http://127.0.0.1:8888/lab';
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
writeNotebookFixture();
|
|
127
|
+
|
|
128
|
+
await page.addInitScript(commandPath => {
|
|
129
|
+
window.localStorage.setItem('jupyterlab-codex:command-path', commandPath);
|
|
130
|
+
window.localStorage.setItem('jupyterlab-codex:include-active-cell', 'true');
|
|
131
|
+
window.localStorage.setItem('jupyterlab-codex:include-active-cell-output', 'true');
|
|
132
|
+
}, codexCommandPath);
|
|
133
|
+
|
|
134
|
+
await page.goto(buildInitialNotebookUrl(targetUrl, NOTEBOOK_RELATIVE_PATH), { waitUntil: 'domcontentloaded' });
|
|
135
|
+
await page.waitForSelector('main[aria-label="Main Content"], .jp-LabShell, .lm-DockPanel', {
|
|
136
|
+
timeout: 30000
|
|
137
|
+
});
|
|
138
|
+
await dismissBlockingDialogs(page);
|
|
139
|
+
await ensureCodexPanel(page);
|
|
140
|
+
|
|
141
|
+
const cellEditor = page.locator('.jp-Notebook [aria-label="Code Cell Content with Output"] [role="textbox"]');
|
|
142
|
+
await expect(cellEditor).toBeVisible({ timeout: 20000 });
|
|
143
|
+
await cellEditor.click();
|
|
144
|
+
|
|
145
|
+
const composer = page.locator('.jp-CodexComposer textarea');
|
|
146
|
+
const sendBtn = page.locator('.jp-CodexSendBtn');
|
|
147
|
+
await composer.fill('Inspect the attached output.');
|
|
148
|
+
await expect(sendBtn).toBeEnabled({ timeout: 15000 });
|
|
149
|
+
await sendBtn.click();
|
|
150
|
+
await expect(page.locator('.jp-CodexSendBtn.is-stop')).toHaveCount(0, { timeout: 30000 });
|
|
151
|
+
|
|
152
|
+
const assistantMessage = page.locator('.jp-CodexChat-message.jp-CodexChat-assistant').last();
|
|
153
|
+
await expect(assistantMessage).toContainText('PROMPT_TAIL_START', { timeout: 30000 });
|
|
154
|
+
const responseText = await assistantMessage.innerText();
|
|
155
|
+
|
|
156
|
+
const tailSection = responseText.split('PROMPT_TAIL_START\n')[1]?.split('\nPROMPT_TAIL_END')[0] || '';
|
|
157
|
+
expect(responseText).toContain('PROMPT_HAS_CURRENT_CELL_CONTENT=true');
|
|
158
|
+
expect(responseText).toContain('PROMPT_HAS_CURRENT_CELL_OUTPUT=true');
|
|
159
|
+
expect(tailSection).toContain(TRACE_TAIL_MARKER);
|
|
160
|
+
expect(tailSection).not.toContain(TRACE_HEAD_MARKER);
|
|
161
|
+
} finally {
|
|
162
|
+
fs.writeFileSync(NOTEBOOK_IPYNB_PATH, originalNotebook, 'utf8');
|
|
163
|
+
fs.writeFileSync(NOTEBOOK_PY_PATH, originalPy, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function buildNotebookUrl(baseLabUrl, notebookPath) {
|
|
4
|
+
const parsed = new URL(baseLabUrl);
|
|
5
|
+
const basePath = parsed.pathname.replace(/\/$/, '');
|
|
6
|
+
parsed.pathname = `${basePath}/tree/${notebookPath}`;
|
|
7
|
+
parsed.searchParams.set('reset', '1');
|
|
8
|
+
return parsed.toString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function escapeRegExp(value) {
|
|
12
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function dismissBlockingDialogs(page) {
|
|
16
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
17
|
+
const dialog = page.locator('dialog.jp-Dialog, [role="dialog"].jp-Dialog, [role="dialog"]').first();
|
|
18
|
+
if (!(await dialog.count()) || !(await dialog.isVisible())) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const noKernelButton = page.getByRole('button', { name: /^No Kernel$/ }).first();
|
|
23
|
+
if ((await noKernelButton.count()) > 0 && (await noKernelButton.isVisible())) {
|
|
24
|
+
await noKernelButton.click();
|
|
25
|
+
await page.waitForTimeout(120);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const selectKernelButton = page.getByRole('button', { name: /^Select$/ }).first();
|
|
30
|
+
if ((await selectKernelButton.count()) > 0 && (await selectKernelButton.isVisible())) {
|
|
31
|
+
await selectKernelButton.click();
|
|
32
|
+
await page.waitForTimeout(120);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cancelButton = page.getByRole('button', { name: /^Cancel$/ }).first();
|
|
37
|
+
if ((await cancelButton.count()) > 0 && (await cancelButton.isVisible())) {
|
|
38
|
+
await cancelButton.click();
|
|
39
|
+
await page.waitForTimeout(120);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await page.keyboard.press('Escape');
|
|
44
|
+
await page.waitForTimeout(120);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ensureFileBrowserVisible(page) {
|
|
49
|
+
await dismissBlockingDialogs(page);
|
|
50
|
+
const fileBrowserTab = page.getByRole('tab', { name: /File Browser/i }).first();
|
|
51
|
+
if (await fileBrowserTab.count()) {
|
|
52
|
+
const isActiveBefore = await fileBrowserTab.evaluate(
|
|
53
|
+
element =>
|
|
54
|
+
element.getAttribute('aria-selected') === 'true' ||
|
|
55
|
+
(typeof element.className === 'string' && element.className.includes('lm-mod-current'))
|
|
56
|
+
);
|
|
57
|
+
if (!isActiveBefore) {
|
|
58
|
+
await fileBrowserTab.click();
|
|
59
|
+
}
|
|
60
|
+
const isActiveAfter = await fileBrowserTab.evaluate(
|
|
61
|
+
element =>
|
|
62
|
+
element.getAttribute('aria-selected') === 'true' ||
|
|
63
|
+
(typeof element.className === 'string' && element.className.includes('lm-mod-current'))
|
|
64
|
+
);
|
|
65
|
+
if (!isActiveAfter) {
|
|
66
|
+
await fileBrowserTab.click();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await page.getByRole('list', { name: /files/i }).first().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function openDocumentFromFileBrowser(page, documentName) {
|
|
73
|
+
await ensureFileBrowserVisible(page);
|
|
74
|
+
let item = page
|
|
75
|
+
.getByRole('listitem', { name: new RegExp(`\\b${escapeRegExp(documentName)}\\b`, 'i') })
|
|
76
|
+
.first();
|
|
77
|
+
if (!(await item.count())) {
|
|
78
|
+
item = page.locator('.jp-DirListing-item', { hasText: documentName }).first();
|
|
79
|
+
}
|
|
80
|
+
if (!(await item.count())) {
|
|
81
|
+
item = page.locator('.jp-FileBrowser').getByText(documentName, { exact: true }).first();
|
|
82
|
+
}
|
|
83
|
+
await item.first().waitFor({ state: 'visible', timeout: 20000 });
|
|
84
|
+
await item.first().dblclick();
|
|
85
|
+
await dismissBlockingDialogs(page);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function activateDocumentTab(page, documentName) {
|
|
89
|
+
await dismissBlockingDialogs(page);
|
|
90
|
+
const exactTab = page
|
|
91
|
+
.getByRole('tab', { name: new RegExp(`^${escapeRegExp(documentName)}$`) })
|
|
92
|
+
.first();
|
|
93
|
+
await exactTab.click();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ensureCodexPanel(page) {
|
|
97
|
+
await dismissBlockingDialogs(page);
|
|
98
|
+
const composer = page.locator('.jp-CodexPanel .jp-CodexComposer textarea');
|
|
99
|
+
if ((await composer.count()) > 0 && (await composer.first().isVisible())) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const codexSideTab = page
|
|
104
|
+
.locator('.jp-SideBar.jp-mod-right .lm-TabBar-tab[title="Codex"], .jp-SideBar.jp-mod-right .lm-TabBar-tab')
|
|
105
|
+
.filter({ hasText: 'Codex' })
|
|
106
|
+
.first();
|
|
107
|
+
await codexSideTab.click();
|
|
108
|
+
await expect(composer.first()).toBeVisible({ timeout: 20000 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function openNotebookInCodex(page, baseURL, notebook) {
|
|
112
|
+
const commandPath = notebook.commandPath || path.resolve(__dirname, 'mock-codex-cli.py');
|
|
113
|
+
await page.addInitScript(cmd => {
|
|
114
|
+
window.localStorage.setItem('jupyterlab-codex:command-path', cmd);
|
|
115
|
+
if (!window.localStorage.getItem('jupyterlab-codex:model')) {
|
|
116
|
+
window.localStorage.removeItem('jupyterlab-codex:model');
|
|
117
|
+
}
|
|
118
|
+
}, commandPath);
|
|
119
|
+
|
|
120
|
+
const targetUrl = buildNotebookUrl(baseURL, notebook.path);
|
|
121
|
+
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
|
122
|
+
await page.waitForSelector('main[aria-label="Main Content"], .jp-LabShell, .lm-DockPanel', { timeout: 30000 });
|
|
123
|
+
await dismissBlockingDialogs(page);
|
|
124
|
+
await ensureCodexPanel(page);
|
|
125
|
+
|
|
126
|
+
await openDocumentFromFileBrowser(page, notebook.name);
|
|
127
|
+
await activateDocumentTab(page, notebook.name);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
buildNotebookUrl,
|
|
132
|
+
dismissBlockingDialogs,
|
|
133
|
+
ensureFileBrowserVisible,
|
|
134
|
+
openDocumentFromFileBrowser,
|
|
135
|
+
activateDocumentTab,
|
|
136
|
+
ensureCodexPanel,
|
|
137
|
+
openNotebookInCodex
|
|
138
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "code",
|
|
5
|
+
"execution_count": 1,
|
|
6
|
+
"id": "error-output-tail-cell",
|
|
7
|
+
"metadata": {},
|
|
8
|
+
"outputs": [
|
|
9
|
+
{
|
|
10
|
+
"ename": "ValueError",
|
|
11
|
+
"evalue": "synthetic traceback for tail truncation test",
|
|
12
|
+
"output_type": "error",
|
|
13
|
+
"traceback": [
|
|
14
|
+
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
|
15
|
+
"\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
|
|
16
|
+
"Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msynthetic traceback for tail truncation test\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
|
|
17
|
+
"\u001b[0;31mValueError\u001b[0m: synthetic traceback for tail truncation test"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"source": [
|
|
22
|
+
"raise ValueError(\"synthetic traceback for tail truncation test\")"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "code",
|
|
27
|
+
"execution_count": null,
|
|
28
|
+
"id": "05fa12e1-3ea7-4081-b658-7e2799c32d07",
|
|
29
|
+
"metadata": {},
|
|
30
|
+
"outputs": [],
|
|
31
|
+
"source": []
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"metadata": {
|
|
35
|
+
"jupytext": {
|
|
36
|
+
"formats": "ipynb,py:percent"
|
|
37
|
+
},
|
|
38
|
+
"kernelspec": {
|
|
39
|
+
"display_name": "Python 3 (ipykernel)",
|
|
40
|
+
"language": "python",
|
|
41
|
+
"name": "python3"
|
|
42
|
+
},
|
|
43
|
+
"language_info": {
|
|
44
|
+
"codemirror_mode": {
|
|
45
|
+
"name": "ipython",
|
|
46
|
+
"version": 3
|
|
47
|
+
},
|
|
48
|
+
"file_extension": ".py",
|
|
49
|
+
"mimetype": "text/x-python",
|
|
50
|
+
"name": "python",
|
|
51
|
+
"nbconvert_exporter": "python",
|
|
52
|
+
"pygments_lexer": "ipython3",
|
|
53
|
+
"version": "3.12.2"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"nbformat": 4,
|
|
57
|
+
"nbformat_minor": 5
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ---
|
|
2
|
+
# jupyter:
|
|
3
|
+
# jupytext:
|
|
4
|
+
# formats: ipynb,py:percent
|
|
5
|
+
# text_representation:
|
|
6
|
+
# extension: .py
|
|
7
|
+
# format_name: percent
|
|
8
|
+
# format_version: '1.3'
|
|
9
|
+
# jupytext_version: 1.17.1
|
|
10
|
+
# kernelspec:
|
|
11
|
+
# display_name: Python 3 (ipykernel)
|
|
12
|
+
# language: python
|
|
13
|
+
# name: python3
|
|
14
|
+
# ---
|
|
15
|
+
|
|
16
|
+
# %%
|
|
17
|
+
raise ValueError("synthetic traceback for tail truncation test")
|
|
18
|
+
|
|
19
|
+
# %%
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
has_current_cell_content = "Current Cell Content:" in prompt
|
|
60
|
+
has_current_cell_output = "Current Cell Output:" in prompt
|
|
61
|
+
|
|
62
|
+
_emit({"type": "thread.started", "thread_id": thread_id})
|
|
63
|
+
_emit(
|
|
64
|
+
{
|
|
65
|
+
"type": "item.completed",
|
|
66
|
+
"item": {
|
|
67
|
+
"type": "agent_message",
|
|
68
|
+
"text": (
|
|
69
|
+
f"PROMPT_HAS_CURRENT_CELL_CONTENT={str(has_current_cell_content).lower()}\n"
|
|
70
|
+
f"PROMPT_HAS_CURRENT_CELL_OUTPUT={str(has_current_cell_output).lower()}\n"
|
|
71
|
+
"PROMPT_HEAD_START\n"
|
|
72
|
+
f"{head}\n"
|
|
73
|
+
"PROMPT_HEAD_END\n"
|
|
74
|
+
"PROMPT_TAIL_START\n"
|
|
75
|
+
f"{tail}\n"
|
|
76
|
+
"PROMPT_TAIL_END"
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main() -> int:
|
|
85
|
+
argv = sys.argv[1:]
|
|
86
|
+
if len(argv) >= 1 and argv[0] == "app-server":
|
|
87
|
+
return _run_app_server()
|
|
88
|
+
return _run_exec_mode()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
raise SystemExit(main())
|
|
@@ -2,11 +2,12 @@ import { expect, test } from '@playwright/test';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildAttachmentTruncationNotice,
|
|
5
|
-
limitActiveCellAttachmentPayload
|
|
5
|
+
limitActiveCellAttachmentPayload,
|
|
6
|
+
resolveSentAttachmentTruncation
|
|
6
7
|
} from '../../src/codexChatAttachmentLimit';
|
|
7
8
|
|
|
8
9
|
test('limitActiveCellAttachmentPayload keeps payload unchanged when within max', () => {
|
|
9
|
-
const result = limitActiveCellAttachmentPayload('abc', 'def', 10);
|
|
10
|
+
const result = limitActiveCellAttachmentPayload('abc', 'def', 10, 10);
|
|
10
11
|
expect(result).toEqual({
|
|
11
12
|
selection: 'abc',
|
|
12
13
|
cellOutput: 'def',
|
|
@@ -15,8 +16,8 @@ test('limitActiveCellAttachmentPayload keeps payload unchanged when within max',
|
|
|
15
16
|
});
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
test('limitActiveCellAttachmentPayload
|
|
19
|
-
const result = limitActiveCellAttachmentPayload('12345', 'abcdef',
|
|
19
|
+
test('limitActiveCellAttachmentPayload applies separate limits to input and output', () => {
|
|
20
|
+
const result = limitActiveCellAttachmentPayload('12345', 'abcdef', 2, 6);
|
|
20
21
|
expect(result).toEqual({
|
|
21
22
|
selection: '12',
|
|
22
23
|
cellOutput: 'abcdef',
|
|
@@ -25,10 +26,20 @@ test('limitActiveCellAttachmentPayload preserves output first, then input', () =
|
|
|
25
26
|
});
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
test('limitActiveCellAttachmentPayload truncates
|
|
29
|
-
const result = limitActiveCellAttachmentPayload('
|
|
29
|
+
test('limitActiveCellAttachmentPayload truncates long single-line input from the middle', () => {
|
|
30
|
+
const result = limitActiveCellAttachmentPayload(`start-${'x'.repeat(3000)}-end`, '', 4000, 10);
|
|
31
|
+
expect(result.selection).toContain('[long line truncated]');
|
|
32
|
+
expect(result.selection.startsWith('start-')).toBeTruthy();
|
|
33
|
+
expect(result.selection.endsWith('-end')).toBeTruthy();
|
|
34
|
+
expect(result.selection.length).toBeLessThanOrEqual(4000);
|
|
35
|
+
expect(result.selectionTruncated).toBeTruthy();
|
|
36
|
+
expect(result.cellOutputTruncated).toBeFalsy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('limitActiveCellAttachmentPayload truncates output independently when output exceeds max', () => {
|
|
40
|
+
const result = limitActiveCellAttachmentPayload('12345', 'abcdefghij', 4, 4);
|
|
30
41
|
expect(result).toEqual({
|
|
31
|
-
selection: '',
|
|
42
|
+
selection: '1234',
|
|
32
43
|
cellOutput: 'abcd',
|
|
33
44
|
selectionTruncated: true,
|
|
34
45
|
cellOutputTruncated: true
|
|
@@ -36,7 +47,21 @@ test('limitActiveCellAttachmentPayload truncates output first when output exceed
|
|
|
36
47
|
});
|
|
37
48
|
|
|
38
49
|
test('buildAttachmentTruncationNotice includes source hint when input is truncated', () => {
|
|
39
|
-
const notice = buildAttachmentTruncationNotice(true, false, 4000);
|
|
50
|
+
const notice = buildAttachmentTruncationNotice(true, false, 4000, 20000);
|
|
40
51
|
expect(notice).toContain('source file/cell');
|
|
41
52
|
expect(notice).toContain('4000');
|
|
42
53
|
});
|
|
54
|
+
|
|
55
|
+
test('resolveSentAttachmentTruncation only reports truncation for attachments that were sent', () => {
|
|
56
|
+
expect(
|
|
57
|
+
resolveSentAttachmentTruncation({
|
|
58
|
+
includeSelection: false,
|
|
59
|
+
includeCellOutput: true,
|
|
60
|
+
selectionTruncated: true,
|
|
61
|
+
cellOutputTruncated: true
|
|
62
|
+
})
|
|
63
|
+
).toEqual({
|
|
64
|
+
selectionTruncated: false,
|
|
65
|
+
cellOutputTruncated: true
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { resolveCellAttachmentState } from '../../src/codexChatAttachmentState';
|
|
4
|
+
|
|
5
|
+
test('resolveCellAttachmentState shows notebook cell attachment for jupytext notebook editor', () => {
|
|
6
|
+
expect(
|
|
7
|
+
resolveCellAttachmentState({
|
|
8
|
+
includeActiveCell: true,
|
|
9
|
+
includeActiveCellOutput: true,
|
|
10
|
+
notebookMode: 'jupytext_py',
|
|
11
|
+
isNotebookEditor: true,
|
|
12
|
+
currentNotebookPath: '/tmp/example.py',
|
|
13
|
+
pairedOk: true
|
|
14
|
+
})
|
|
15
|
+
).toEqual({
|
|
16
|
+
showBadge: true,
|
|
17
|
+
contentEnabled: true,
|
|
18
|
+
outputEnabled: true
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('resolveCellAttachmentState hides cell attachment for jupytext text editor', () => {
|
|
23
|
+
expect(
|
|
24
|
+
resolveCellAttachmentState({
|
|
25
|
+
includeActiveCell: true,
|
|
26
|
+
includeActiveCellOutput: true,
|
|
27
|
+
notebookMode: 'jupytext_py',
|
|
28
|
+
isNotebookEditor: false,
|
|
29
|
+
currentNotebookPath: '/tmp/example.py',
|
|
30
|
+
pairedOk: true
|
|
31
|
+
})
|
|
32
|
+
).toEqual({
|
|
33
|
+
showBadge: false,
|
|
34
|
+
contentEnabled: false,
|
|
35
|
+
outputEnabled: false
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('resolveCellAttachmentState hides badge for plain python notebook mode', () => {
|
|
40
|
+
expect(
|
|
41
|
+
resolveCellAttachmentState({
|
|
42
|
+
includeActiveCell: true,
|
|
43
|
+
includeActiveCellOutput: true,
|
|
44
|
+
notebookMode: 'plain_py',
|
|
45
|
+
isNotebookEditor: true,
|
|
46
|
+
currentNotebookPath: '/tmp/example.py',
|
|
47
|
+
pairedOk: true
|
|
48
|
+
})
|
|
49
|
+
).toEqual({
|
|
50
|
+
showBadge: false,
|
|
51
|
+
contentEnabled: false,
|
|
52
|
+
outputEnabled: false
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('resolveCellAttachmentState keeps the toggle visible when attachment is off', () => {
|
|
57
|
+
expect(
|
|
58
|
+
resolveCellAttachmentState({
|
|
59
|
+
includeActiveCell: false,
|
|
60
|
+
includeActiveCellOutput: true,
|
|
61
|
+
notebookMode: 'ipynb',
|
|
62
|
+
isNotebookEditor: true,
|
|
63
|
+
currentNotebookPath: '/tmp/example.ipynb',
|
|
64
|
+
pairedOk: true
|
|
65
|
+
})
|
|
66
|
+
).toEqual({
|
|
67
|
+
showBadge: true,
|
|
68
|
+
contentEnabled: false,
|
|
69
|
+
outputEnabled: false
|
|
70
|
+
});
|
|
71
|
+
});
|