jupyterlab-codex-sidebar 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.github/workflows/unit-tests.yml +27 -0
  3. package/.jupyterlab-playwright.log +0 -0
  4. package/README.md +83 -9
  5. package/docs/images/codex-sidebar-screenshot.png +0 -0
  6. package/jupyterlab_codex/handlers.py +938 -297
  7. package/jupyterlab_codex/labextension/package.json +13 -3
  8. package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
  9. package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
  10. package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
  11. package/jupyterlab_codex/protocol.py +297 -0
  12. package/jupyterlab_codex/runner.py +58 -15
  13. package/jupyterlab_codex/sessions.py +582 -97
  14. package/lib/codexChat.d.ts +13 -0
  15. package/lib/codexChat.js +2506 -0
  16. package/lib/codexChat.js.map +1 -0
  17. package/lib/codexChatAttachmentDedup.d.ts +10 -0
  18. package/lib/codexChatAttachmentDedup.js +35 -0
  19. package/lib/codexChatAttachmentDedup.js.map +1 -0
  20. package/lib/codexChatAttachmentLimit.d.ts +18 -0
  21. package/lib/codexChatAttachmentLimit.js +50 -0
  22. package/lib/codexChatAttachmentLimit.js.map +1 -0
  23. package/lib/codexChatAttachmentState.d.ts +15 -0
  24. package/lib/codexChatAttachmentState.js +16 -0
  25. package/lib/codexChatAttachmentState.js.map +1 -0
  26. package/lib/codexChatDocumentUtils.d.ts +70 -0
  27. package/lib/codexChatDocumentUtils.js +506 -0
  28. package/lib/codexChatDocumentUtils.js.map +1 -0
  29. package/lib/codexChatFormatting.d.ts +11 -0
  30. package/lib/codexChatFormatting.js +83 -0
  31. package/lib/codexChatFormatting.js.map +1 -0
  32. package/lib/codexChatNotice.d.ts +3 -0
  33. package/lib/codexChatNotice.js +74 -0
  34. package/lib/codexChatNotice.js.map +1 -0
  35. package/lib/codexChatPersistence.d.ts +35 -0
  36. package/lib/codexChatPersistence.js +158 -0
  37. package/lib/codexChatPersistence.js.map +1 -0
  38. package/lib/codexChatPrimitives.d.ts +44 -0
  39. package/lib/codexChatPrimitives.js +156 -0
  40. package/lib/codexChatPrimitives.js.map +1 -0
  41. package/lib/codexChatRender.d.ts +24 -0
  42. package/lib/codexChatRender.js +293 -0
  43. package/lib/codexChatRender.js.map +1 -0
  44. package/lib/codexChatSessionFactory.d.ts +15 -0
  45. package/lib/codexChatSessionFactory.js +45 -0
  46. package/lib/codexChatSessionFactory.js.map +1 -0
  47. package/lib/codexChatSessionKey.d.ts +3 -0
  48. package/lib/codexChatSessionKey.js +14 -0
  49. package/lib/codexChatSessionKey.js.map +1 -0
  50. package/lib/codexChatStorage.d.ts +4 -0
  51. package/lib/codexChatStorage.js +37 -0
  52. package/lib/codexChatStorage.js.map +1 -0
  53. package/lib/codexSessionResolver.d.ts +12 -0
  54. package/lib/codexSessionResolver.js +38 -0
  55. package/lib/codexSessionResolver.js.map +1 -0
  56. package/lib/handlers/activitySummarizer.d.ts +15 -0
  57. package/lib/handlers/activitySummarizer.js +327 -0
  58. package/lib/handlers/activitySummarizer.js.map +1 -0
  59. package/lib/handlers/codexMessageTypes.d.ts +30 -0
  60. package/lib/handlers/codexMessageTypes.js +2 -0
  61. package/lib/handlers/codexMessageTypes.js.map +1 -0
  62. package/lib/handlers/codexMessageUtils.d.ts +46 -0
  63. package/lib/handlers/codexMessageUtils.js +144 -0
  64. package/lib/handlers/codexMessageUtils.js.map +1 -0
  65. package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
  66. package/lib/handlers/handleCodexSocketMessage.js +78 -0
  67. package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
  68. package/lib/handlers/sessionSyncHandler.d.ts +34 -0
  69. package/lib/handlers/sessionSyncHandler.js +181 -0
  70. package/lib/handlers/sessionSyncHandler.js.map +1 -0
  71. package/lib/hooks/useCodexSocket.d.ts +15 -0
  72. package/lib/hooks/useCodexSocket.js +84 -0
  73. package/lib/hooks/useCodexSocket.js.map +1 -0
  74. package/lib/index.js +1 -1
  75. package/lib/index.js.map +1 -1
  76. package/lib/panel.d.ts +1 -11
  77. package/lib/panel.js +1 -2815
  78. package/lib/panel.js.map +1 -1
  79. package/lib/protocol.d.ts +235 -0
  80. package/lib/protocol.js +278 -0
  81. package/lib/protocol.js.map +1 -0
  82. package/package.json +13 -3
  83. package/playwright.config.cjs +27 -0
  84. package/playwright.unit.config.cjs +19 -0
  85. package/pyproject.toml +1 -1
  86. package/release.sh +52 -14
  87. package/scripts/run_playwright_e2e.sh +96 -0
  88. package/scripts/run_playwright_freeze_repro.sh +58 -0
  89. package/scripts/run_playwright_queue_repro.sh +60 -0
  90. package/scripts/run_playwright_repro.sh +55 -0
  91. package/src/codexChat.tsx +3914 -0
  92. package/src/codexChatAttachmentDedup.ts +47 -0
  93. package/src/codexChatAttachmentLimit.ts +81 -0
  94. package/src/codexChatAttachmentState.ts +37 -0
  95. package/src/codexChatDocumentUtils.ts +644 -0
  96. package/src/codexChatFormatting.ts +94 -0
  97. package/src/codexChatNotice.ts +95 -0
  98. package/src/codexChatPersistence.ts +191 -0
  99. package/src/codexChatPrimitives.tsx +446 -0
  100. package/src/codexChatRender.tsx +376 -0
  101. package/src/codexChatSessionFactory.ts +79 -0
  102. package/src/codexChatSessionKey.ts +16 -0
  103. package/src/codexChatStorage.ts +36 -0
  104. package/src/codexSessionResolver.ts +56 -0
  105. package/src/handlers/activitySummarizer.ts +369 -0
  106. package/src/handlers/codexMessageTypes.ts +34 -0
  107. package/src/handlers/codexMessageUtils.ts +217 -0
  108. package/src/handlers/handleCodexSocketMessage.ts +204 -0
  109. package/src/handlers/sessionSyncHandler.ts +308 -0
  110. package/src/hooks/useCodexSocket.ts +109 -0
  111. package/src/index.ts +1 -1
  112. package/src/panel.tsx +1 -4184
  113. package/src/protocol.ts +582 -0
  114. package/style/index.css +480 -11
  115. package/test-results/.last-run.json +4 -0
  116. package/test.py +0 -0
  117. package/tests/e2e/cell-output-error-tail.spec.js +156 -0
  118. package/tests/e2e/codex-ui-test-helpers.js +138 -0
  119. package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
  120. package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
  121. package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
  122. package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
  123. package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
  124. package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
  125. package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
  126. package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
  127. package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
  128. package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
  129. package/tests/e2e/mock-codex-cli-flood.py +127 -0
  130. package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
  131. package/tests/e2e/mock-codex-cli.py +95 -0
  132. package/tests/e2e/queue-multitab-repro.spec.js +189 -0
  133. package/tests/test_handlers.py +116 -0
  134. package/tests/test_protocol.py +169 -0
  135. package/tests/test_session_store_limits.py +50 -0
  136. package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
  137. package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
  138. package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
  139. package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
  140. package/tests/unit/codexChatLimit.spec.ts +18 -0
  141. package/tests/unit/codexChatNotice.spec.ts +45 -0
  142. package/tests/unit/codexChatPersistence.spec.ts +199 -0
  143. package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
  144. package/tests/unit/codexChatSessionKey.spec.ts +18 -0
  145. package/tests/unit/codexMessageUtils.spec.ts +89 -0
  146. package/tests/unit/codexSessionResolver.spec.ts +92 -0
  147. package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
  148. package/tsconfig.tsbuildinfo +1 -1
  149. package/webpack.config.js +6 -0
  150. package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
  151. package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
  152. package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
  153. /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
@@ -0,0 +1,369 @@
1
+ export type ActivityPhase = 'started' | 'completed' | '';
2
+ export type ActivityCategory = 'reasoning' | 'command' | 'file' | 'tool' | 'event';
3
+
4
+ export interface ActivitySummary {
5
+ activity: {
6
+ category: ActivityCategory;
7
+ phase: ActivityPhase;
8
+ title: string;
9
+ detail: string;
10
+ raw: string;
11
+ };
12
+ progress: string;
13
+ progressKind: '' | 'reasoning';
14
+ }
15
+
16
+ function truncateMiddle(value: string, max: number): string {
17
+ if (value.length <= max) {
18
+ return value;
19
+ }
20
+ const ellipsis = '...';
21
+ if (max <= ellipsis.length) {
22
+ return value.slice(0, max);
23
+ }
24
+ const head = Math.max(0, Math.floor((max - ellipsis.length) * 0.6));
25
+ const tail = Math.max(0, max - head - ellipsis.length);
26
+ return `${value.slice(0, head)}${ellipsis}${value.slice(value.length - tail)}`;
27
+ }
28
+
29
+ function safePreview(value: unknown, max = 220): string {
30
+ if (value == null) {
31
+ return '';
32
+ }
33
+ if (typeof value === 'string') {
34
+ return truncateMiddle(value, max);
35
+ }
36
+ if (typeof value === 'number' || typeof value === 'boolean') {
37
+ return String(value);
38
+ }
39
+ try {
40
+ return truncateMiddle(JSON.stringify(value), max);
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ function safeJsonPreview(value: any, maxChars = 6000): string {
47
+ try {
48
+ const text = JSON.stringify(value, null, 2);
49
+ if (text.length <= maxChars) {
50
+ return text;
51
+ }
52
+ return `${text.slice(0, Math.max(0, maxChars - 40))}\n... (truncated)`;
53
+ } catch {
54
+ return '';
55
+ }
56
+ }
57
+
58
+ function fileNameFromPath(path: string): string {
59
+ const raw = (path || '').trim();
60
+ if (!raw) {
61
+ return '';
62
+ }
63
+ const normalized = raw.replace(/\\/g, '/');
64
+ const parts = normalized.split('/').filter(Boolean);
65
+ return parts[parts.length - 1] || normalized;
66
+ }
67
+
68
+ function extractFileChangePaths(item: any): string[] {
69
+ if (!item || typeof item !== 'object') {
70
+ return [];
71
+ }
72
+
73
+ const seen = new Set<string>();
74
+ const paths: string[] = [];
75
+ const push = (value: unknown): void => {
76
+ if (typeof value !== 'string') {
77
+ return;
78
+ }
79
+ const path = value.trim();
80
+ if (!path || seen.has(path)) {
81
+ return;
82
+ }
83
+ seen.add(path);
84
+ paths.push(path);
85
+ };
86
+
87
+ const directKeys = [
88
+ 'path',
89
+ 'filename',
90
+ 'filePath',
91
+ 'file_path',
92
+ 'targetPath',
93
+ 'target_path',
94
+ 'newPath',
95
+ 'new_path',
96
+ 'oldPath',
97
+ 'old_path',
98
+ 'sourcePath',
99
+ 'source_path',
100
+ 'destinationPath',
101
+ 'destination_path'
102
+ ];
103
+ for (const key of directKeys) {
104
+ push(item[key]);
105
+ }
106
+
107
+ const nestedObjectKeys = ['file', 'target', 'source', 'destination'];
108
+ for (const key of nestedObjectKeys) {
109
+ const nested = item[key];
110
+ if (!nested || typeof nested !== 'object') {
111
+ continue;
112
+ }
113
+ push((nested as any).path);
114
+ push((nested as any).filename);
115
+ push((nested as any).filePath);
116
+ push((nested as any).file_path);
117
+ }
118
+
119
+ const listKeys = ['paths', 'files', 'changes', 'edits'];
120
+ for (const key of listKeys) {
121
+ const list = item[key];
122
+ if (!Array.isArray(list)) {
123
+ continue;
124
+ }
125
+ for (const entry of list) {
126
+ if (typeof entry === 'string') {
127
+ push(entry);
128
+ continue;
129
+ }
130
+ if (!entry || typeof entry !== 'object') {
131
+ continue;
132
+ }
133
+ push((entry as any).path);
134
+ push((entry as any).filename);
135
+ push((entry as any).filePath);
136
+ push((entry as any).file_path);
137
+ push((entry as any).targetPath);
138
+ push((entry as any).target_path);
139
+ push((entry as any).newPath);
140
+ push((entry as any).new_path);
141
+ push((entry as any).oldPath);
142
+ push((entry as any).old_path);
143
+ }
144
+ }
145
+
146
+ // Fallback: recursively scan nested objects for path-like keys.
147
+ if (paths.length === 0) {
148
+ const queue: unknown[] = [item];
149
+ let visited = 0;
150
+ while (queue.length > 0 && visited < 300) {
151
+ const current = queue.shift();
152
+ visited += 1;
153
+ if (!current || typeof current !== 'object') {
154
+ continue;
155
+ }
156
+ if (Array.isArray(current)) {
157
+ for (const value of current) {
158
+ queue.push(value);
159
+ }
160
+ continue;
161
+ }
162
+
163
+ for (const [rawKey, value] of Object.entries(current as Record<string, unknown>)) {
164
+ const key = rawKey.trim().toLowerCase();
165
+ if (
166
+ typeof value === 'string' &&
167
+ (key === 'filename' ||
168
+ key.endsWith('path') ||
169
+ key.endsWith('_path') ||
170
+ key.endsWith('filepath') ||
171
+ key.endsWith('file_path'))
172
+ ) {
173
+ push(value);
174
+ }
175
+ if (value && typeof value === 'object') {
176
+ queue.push(value);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ return paths;
183
+ }
184
+
185
+ function humanizeToken(value: string): string {
186
+ const input = (value || '').trim();
187
+ if (!input) {
188
+ return '';
189
+ }
190
+ return input
191
+ .split(/[_-]+/)
192
+ .filter(Boolean)
193
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
194
+ .join(' ');
195
+ }
196
+
197
+ export function summarizeCodexEvent(payload: any): ActivitySummary {
198
+ if (!payload || typeof payload !== 'object') {
199
+ return {
200
+ progress: 'Event',
201
+ progressKind: '',
202
+ activity: { category: 'event', phase: '', title: 'Event', detail: '', raw: safeJsonPreview(payload) }
203
+ };
204
+ }
205
+
206
+ const type =
207
+ (typeof payload.type === 'string' && payload.type) ||
208
+ (typeof payload.event === 'string' && payload.event) ||
209
+ 'event';
210
+ const raw = safeJsonPreview(payload);
211
+
212
+ // Common Codex CLI shape: { type: "item.completed|item.started", item: { type: "...", ... } }
213
+ if (
214
+ (type === 'item.completed' || type === 'item.started') &&
215
+ payload.item &&
216
+ typeof payload.item === 'object'
217
+ ) {
218
+ const item: any = payload.item;
219
+ const itemType = typeof item.type === 'string' ? item.type : '';
220
+ const toolName =
221
+ (typeof item.tool_name === 'string' && item.tool_name) ||
222
+ (typeof item.tool === 'string' && item.tool) ||
223
+ (typeof item.name === 'string' && item.name) ||
224
+ '';
225
+ const path = (typeof item.path === 'string' && item.path) || (typeof item.filename === 'string' && item.filename) || '';
226
+ const phase: ActivityPhase = type === 'item.started' ? 'started' : type === 'item.completed' ? 'completed' : '';
227
+
228
+ let category: ActivityCategory = 'event';
229
+ let progressKind: '' | 'reasoning' = '';
230
+
231
+ if (itemType === 'reasoning') {
232
+ category = 'reasoning';
233
+ progressKind = 'reasoning';
234
+ } else if (itemType === 'command_execution') {
235
+ category = 'command';
236
+ } else if (itemType === 'file_change') {
237
+ category = 'file';
238
+ } else if (toolName) {
239
+ category = 'tool';
240
+ }
241
+
242
+ let title = '';
243
+ let detail = '';
244
+
245
+ if (itemType === 'command_execution') {
246
+ const commandField =
247
+ item.command ?? item.cmd ?? item.shell_command ?? item.argv ?? item.args ?? item.commandLine ?? item.command_line;
248
+ let commandHint = '';
249
+ if (typeof commandField === 'string') {
250
+ commandHint = commandField;
251
+ } else if (
252
+ Array.isArray(commandField) &&
253
+ commandField.length > 0 &&
254
+ commandField.every((token: any) => typeof token === 'string')
255
+ ) {
256
+ commandHint = commandField.join(' ');
257
+ } else {
258
+ commandHint = safePreview(commandField);
259
+ }
260
+ const exitCode = typeof item.exit_code === 'number' ? item.exit_code : typeof item.exitCode === 'number' ? item.exitCode : null;
261
+ title = phase === 'started' ? 'Running command' : phase === 'completed' ? 'Command finished' : 'Command';
262
+ detail = commandHint || '';
263
+ if (exitCode != null) {
264
+ detail = detail ? `${detail}\n(exit ${exitCode})` : `(exit ${exitCode})`;
265
+ }
266
+ } else if (itemType === 'file_change') {
267
+ const filePaths = extractFileChangePaths(item);
268
+ const primaryPath = filePaths[0] || path || '';
269
+ const fileName = fileNameFromPath(primaryPath);
270
+ const baseTitle = phase === 'started' ? 'Update file' : phase === 'completed' ? 'File updated' : 'File change';
271
+ title = fileName ? `${baseTitle}: ${fileName}` : baseTitle;
272
+ detail = filePaths.length > 0 ? filePaths.join('\n') : path || '';
273
+ } else if (itemType === 'reasoning') {
274
+ title = phase === 'started' ? 'Reasoning' : phase === 'completed' ? 'Reasoning step' : 'Reasoning';
275
+ detail = toolName ? `(${toolName})` : '';
276
+ } else {
277
+ const base = humanizeToken(itemType) || humanizeToken(type) || 'Event';
278
+ title = phase === 'started' ? `${base} started` : phase === 'completed' ? `${base} completed` : base;
279
+ const detailParts: string[] = [];
280
+ if (toolName) {
281
+ detailParts.push(`tool: ${toolName}`);
282
+ }
283
+ if (path) {
284
+ detailParts.push(`path: ${path}`);
285
+ }
286
+ detail = detailParts.join('\n');
287
+ }
288
+
289
+ if (!title) {
290
+ title = humanizeToken(itemType) || 'Event';
291
+ }
292
+ detail = truncateMiddle(detail, 1400);
293
+
294
+ const progressParts: string[] = [];
295
+ if (title) {
296
+ progressParts.push(title);
297
+ }
298
+ if (detail) {
299
+ const oneLine = detail.replace(/\s+/g, ' ').trim();
300
+ progressParts.push(truncateMiddle(oneLine, 200));
301
+ }
302
+ const progress = progressParts.join(': ') || humanizeToken(type) || 'Event';
303
+
304
+ return {
305
+ progress,
306
+ progressKind,
307
+ activity: {
308
+ category,
309
+ phase,
310
+ title,
311
+ detail,
312
+ raw
313
+ }
314
+ };
315
+ }
316
+
317
+ const label =
318
+ (typeof payload.name === 'string' && payload.name) ||
319
+ (typeof payload.label === 'string' && payload.label) ||
320
+ (typeof payload.tool_name === 'string' && payload.tool_name) ||
321
+ (typeof payload.tool === 'string' && payload.tool) ||
322
+ '';
323
+ if (label && label !== type) {
324
+ const progress = `${humanizeToken(type) || type}: ${truncateMiddle(label, 120)}`;
325
+ return {
326
+ progress,
327
+ progressKind: '',
328
+ activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: label, raw }
329
+ };
330
+ }
331
+
332
+ const commandHint = safePreview(payload.command);
333
+ if (commandHint) {
334
+ const progress = `${humanizeToken(type) || type}: ${commandHint}`;
335
+ return {
336
+ progress,
337
+ progressKind: '',
338
+ activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: commandHint, raw }
339
+ };
340
+ }
341
+
342
+ const pathHint = safePreview(payload.path);
343
+ if (pathHint) {
344
+ const progress = `${humanizeToken(type) || type}: ${pathHint}`;
345
+ return {
346
+ progress,
347
+ progressKind: '',
348
+ activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: pathHint, raw }
349
+ };
350
+ }
351
+
352
+ return {
353
+ progress: humanizeToken(type) || type,
354
+ progressKind: '',
355
+ activity: { category: 'event', phase: '', title: humanizeToken(type) || type, detail: '', raw }
356
+ };
357
+ }
358
+
359
+ export function isNoiseCodexEvent(payload: any): boolean {
360
+ if (!payload || typeof payload !== 'object') {
361
+ return false;
362
+ }
363
+ const type =
364
+ (typeof payload.type === 'string' && payload.type) ||
365
+ (typeof payload.event === 'string' && payload.event) ||
366
+ '';
367
+ // These are useful for debugging but not meaningful for end users.
368
+ return type === 'thread.started' || type === 'turn.started' || type === 'turn.completed';
369
+ }
@@ -0,0 +1,34 @@
1
+ export type TextRole = 'user' | 'assistant' | 'system';
2
+
3
+ export interface HistoryEntry {
4
+ role: TextRole;
5
+ content: string;
6
+ selectionPreview?: {
7
+ locationLabel: string;
8
+ previewText: string;
9
+ };
10
+ cellOutputPreview?: {
11
+ locationLabel: string;
12
+ previewText: string;
13
+ };
14
+ }
15
+
16
+ export interface RateLimitWindowSnapshot {
17
+ usedPercent: number | null;
18
+ windowMinutes: number | null;
19
+ resetsAt: number | null;
20
+ }
21
+
22
+ export interface ContextWindowSnapshot {
23
+ windowTokens: number | null;
24
+ usedTokens: number | null;
25
+ leftTokens: number | null;
26
+ usedPercent: number | null;
27
+ }
28
+
29
+ export interface CodexRateLimitsSnapshot {
30
+ updatedAt: string | null;
31
+ primary: RateLimitWindowSnapshot | null;
32
+ secondary: RateLimitWindowSnapshot | null;
33
+ contextWindow: ContextWindowSnapshot | null;
34
+ }
@@ -0,0 +1,217 @@
1
+ import type {
2
+ CodexRateLimitsSnapshot,
3
+ ContextWindowSnapshot,
4
+ HistoryEntry,
5
+ RateLimitWindowSnapshot,
6
+ TextRole
7
+ } from './codexMessageTypes';
8
+
9
+ export type { HistoryEntry, TextRole };
10
+
11
+ export type ProgressKind = '' | 'reasoning';
12
+ export type ActivityPhase = 'started' | 'completed' | '';
13
+ export type ActivityCategory = 'reasoning' | 'command' | 'file' | 'tool' | 'event';
14
+
15
+ export interface SelectionPreview {
16
+ locationLabel: string;
17
+ previewText: string;
18
+ }
19
+
20
+ export interface MessageContextPreview {
21
+ selectionPreview?: SelectionPreview;
22
+ cellOutputPreview?: SelectionPreview;
23
+ }
24
+
25
+ export interface ActivityItem {
26
+ category: ActivityCategory;
27
+ phase: ActivityPhase;
28
+ title: string;
29
+ detail: string;
30
+ raw: string;
31
+ }
32
+
33
+ export interface ActivitySummary {
34
+ activity: Omit<ActivityItem, 'id' | 'ts'>;
35
+ progress: string;
36
+ progressKind: ProgressKind;
37
+ }
38
+
39
+ export interface SessionPreviewSplit {
40
+ selectionPreview?: unknown;
41
+ cellOutputPreview?: unknown;
42
+ }
43
+
44
+ export function truncateEnd(text: string, max: number): string {
45
+ if (text.length <= max) {
46
+ return text;
47
+ }
48
+ if (max <= 3) {
49
+ return text.slice(0, max);
50
+ }
51
+ return `${text.slice(0, max - 3)}...`;
52
+ }
53
+
54
+ export function coerceSelectionPreview(
55
+ value: unknown,
56
+ options: {
57
+ maxChars?: number;
58
+ normalize?: (value: string) => string;
59
+ } = {}
60
+ ): SelectionPreview | undefined {
61
+ if (!value || typeof value !== 'object') {
62
+ return undefined;
63
+ }
64
+ const raw = value as Record<string, unknown>;
65
+ const locationLabel = typeof raw.locationLabel === 'string' ? raw.locationLabel.trim() : '';
66
+ const normalizer = options.normalize ?? ((text: string) => text.trim());
67
+ const maxChars = options.maxChars ?? 500;
68
+ const previewText =
69
+ typeof raw.previewText === 'string' ? truncateEnd(normalizer(raw.previewText), maxChars) : '';
70
+ if (!locationLabel || !previewText) {
71
+ return undefined;
72
+ }
73
+ return { locationLabel, previewText };
74
+ }
75
+
76
+ export function coerceMessageContextPreview(
77
+ value: unknown,
78
+ options: {
79
+ maxChars?: number;
80
+ normalize?: (value: string) => string;
81
+ } = {}
82
+ ): MessageContextPreview | undefined {
83
+ if (!value || typeof value !== 'object') {
84
+ return undefined;
85
+ }
86
+ const raw = value as Record<string, unknown>;
87
+ const selectionPreview = coerceSelectionPreview(raw.selectionPreview, options);
88
+ const cellOutputPreview = coerceSelectionPreview(raw.cellOutputPreview, options);
89
+ if (selectionPreview || cellOutputPreview) {
90
+ return {
91
+ ...(selectionPreview ? { selectionPreview } : {}),
92
+ ...(cellOutputPreview ? { cellOutputPreview } : {})
93
+ };
94
+ }
95
+
96
+ // Legacy format: a single preview object was stored directly as selection preview.
97
+ const legacySelectionPreview = coerceSelectionPreview(raw, options);
98
+ if (legacySelectionPreview) {
99
+ return { selectionPreview: legacySelectionPreview };
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ export function coerceSessionHistory(
105
+ raw: unknown,
106
+ options: {
107
+ maxChars?: number;
108
+ normalize?: (value: string) => string;
109
+ } = {}
110
+ ): HistoryEntry[] {
111
+ if (!Array.isArray(raw)) {
112
+ return [];
113
+ }
114
+ const result: HistoryEntry[] = [];
115
+ for (const item of raw) {
116
+ if (!item || typeof item !== 'object') {
117
+ continue;
118
+ }
119
+ const role = (item as any).role;
120
+ const content = (item as any).content;
121
+ if ((role !== 'user' && role !== 'assistant' && role !== 'system') || typeof content !== 'string') {
122
+ continue;
123
+ }
124
+ const selectionPreview = coerceSelectionPreview((item as any).selectionPreview, options);
125
+ const cellOutputPreview = coerceSelectionPreview((item as any).cellOutputPreview, options);
126
+ result.push({
127
+ role,
128
+ content,
129
+ selectionPreview,
130
+ cellOutputPreview
131
+ });
132
+ }
133
+ return result;
134
+ }
135
+
136
+ function coerceFiniteNumber(value: unknown): number | null {
137
+ if (typeof value === 'number' && Number.isFinite(value)) {
138
+ return value;
139
+ }
140
+ if (typeof value === 'string') {
141
+ const trimmed = value.trim();
142
+ if (!trimmed) {
143
+ return null;
144
+ }
145
+ const parsed = Number(trimmed);
146
+ return Number.isFinite(parsed) ? parsed : null;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ export function coerceRateLimitWindow(raw: unknown): RateLimitWindowSnapshot | null {
152
+ if (!raw || typeof raw !== 'object') {
153
+ return null;
154
+ }
155
+ const record = raw as Record<string, unknown>;
156
+ const usedPercent = coerceFiniteNumber(record.usedPercent);
157
+ const windowMinutesValue = coerceFiniteNumber(record.windowMinutes);
158
+ const resetsAtValue = coerceFiniteNumber(record.resetsAt);
159
+ const windowMinutes = windowMinutesValue === null ? null : Math.round(windowMinutesValue);
160
+ const resetsAt = resetsAtValue === null ? null : Math.round(resetsAtValue);
161
+ return { usedPercent, windowMinutes, resetsAt };
162
+ }
163
+
164
+ export function coerceContextWindowSnapshot(raw: unknown): ContextWindowSnapshot | null {
165
+ if (!raw || typeof raw !== 'object') {
166
+ return null;
167
+ }
168
+ const record = raw as Record<string, unknown>;
169
+ const windowTokensValue = coerceFiniteNumber(record.windowTokens);
170
+ const usedTokensValue = coerceFiniteNumber(record.usedTokens);
171
+ const leftTokensValue = coerceFiniteNumber(record.leftTokens);
172
+ const usedPercent = coerceFiniteNumber(record.usedPercent);
173
+ const windowTokens = windowTokensValue === null ? null : Math.round(windowTokensValue);
174
+ const usedTokens = usedTokensValue === null ? null : Math.round(usedTokensValue);
175
+ const leftTokens = leftTokensValue === null ? null : Math.round(leftTokensValue);
176
+ if (windowTokens == null && usedTokens == null && leftTokens == null && usedPercent == null) {
177
+ return null;
178
+ }
179
+ return { windowTokens, usedTokens, leftTokens, usedPercent };
180
+ }
181
+
182
+ export function coerceRateLimitsSnapshot(raw: unknown): CodexRateLimitsSnapshot | null {
183
+ if (!raw || typeof raw !== 'object') {
184
+ return null;
185
+ }
186
+ const rawRecord = raw as Record<string, unknown>;
187
+ const updatedAt = typeof rawRecord.updatedAt === 'string' && rawRecord.updatedAt.trim() ? rawRecord.updatedAt : null;
188
+ return {
189
+ updatedAt,
190
+ primary: coerceRateLimitWindow(rawRecord.primary),
191
+ secondary: coerceRateLimitWindow(rawRecord.secondary),
192
+ contextWindow: coerceContextWindowSnapshot(rawRecord.contextWindow)
193
+ };
194
+ }
195
+
196
+ export function splitStoredMessagePreview(value: unknown): SessionPreviewSplit {
197
+ if (!value || typeof value !== 'object') {
198
+ return {};
199
+ }
200
+
201
+ const raw = value as Record<string, unknown>;
202
+ const hasNestedPreview =
203
+ Object.prototype.hasOwnProperty.call(raw, 'selectionPreview') ||
204
+ Object.prototype.hasOwnProperty.call(raw, 'cellOutputPreview');
205
+ if (hasNestedPreview) {
206
+ return {
207
+ selectionPreview: raw.selectionPreview,
208
+ cellOutputPreview: raw.cellOutputPreview
209
+ };
210
+ }
211
+
212
+ // Legacy format: a single preview object was stored directly as selectionPreview.
213
+ if (Object.prototype.hasOwnProperty.call(raw, 'locationLabel') && Object.prototype.hasOwnProperty.call(raw, 'previewText')) {
214
+ return { selectionPreview: raw };
215
+ }
216
+ return {};
217
+ }