python-codex 0.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

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 (58) hide show
  1. pycodex/__init__.py +3 -27
  2. pycodex/bootstrap.py +5 -45
  3. pycodex/cli.py +1 -14
  4. pycodex/context.py +1 -1
  5. pycodex/events.py +1 -1
  6. pycodex/model.py +2 -2
  7. pycodex/portable/__init__.py +45 -0
  8. pycodex/{portable.py → portable/client.py} +4 -7
  9. pycodex/{portable_server.py → portable/server.py} +3 -3
  10. pycodex/protocol.py +1 -1
  11. pycodex/runtime.py +1 -1
  12. pycodex/tools/base_tool.py +1 -1
  13. pycodex/tools/close_agent_tool.py +1 -1
  14. pycodex/tools/code_mode_manager.py +1 -1
  15. pycodex/tools/request_permissions_tool.py +1 -1
  16. pycodex/tools/resume_agent_tool.py +1 -1
  17. pycodex/{runtime_services.py → tools/runtime_services.py} +5 -5
  18. pycodex/tools/send_input_tool.py +1 -1
  19. pycodex/tools/spawn_agent_tool.py +1 -1
  20. pycodex/tools/unified_exec_manager.py +1 -1
  21. pycodex/tools/update_plan_tool.py +1 -1
  22. pycodex/tools/view_image_tool.py +5 -0
  23. pycodex/tools/wait_agent_tool.py +1 -1
  24. pycodex/utils/__init__.py +0 -4
  25. pycodex/utils/_aes_gcm/LICENSE +56 -0
  26. pycodex/utils/_aes_gcm/README.md +61 -0
  27. pycodex/utils/_aes_gcm/__init__.py +1 -0
  28. pycodex/utils/_aes_gcm/aes.py +175 -0
  29. pycodex/utils/_aes_gcm/gcm.py +73 -0
  30. pycodex/utils/_aes_gcm/source-sha256.json +4 -0
  31. pycodex/{compat.py → utils/compat.py} +2 -0
  32. pycodex/utils/crypto.py +35 -0
  33. pycodex/utils/debug.py +24 -0
  34. pycodex/{doctor.py → utils/doctor.py} +3 -3
  35. pycodex/utils/image_utils.py +3 -2
  36. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  37. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/METADATA +120 -42
  38. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/RECORD +42 -50
  39. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/entry_points.txt +0 -1
  40. responses_server/app.py +28 -15
  41. pycodex/feishu_card.py +0 -793
  42. pycodex/feishu_link.py +0 -440
  43. pycodex/tools/ipython_tool.py +0 -142
  44. workspace_server/__init__.py +0 -39
  45. workspace_server/__main__.py +0 -4
  46. workspace_server/app.py +0 -1906
  47. workspace_server/content_views.LICENSE.txt +0 -470
  48. workspace_server/content_views.css +0 -1
  49. workspace_server/content_views.mjs +0 -25
  50. workspace_server/editor.css +0 -1
  51. workspace_server/editor.mjs +0 -1260
  52. workspace_server/owned_process.py +0 -27
  53. workspace_server/workspace.html +0 -2267
  54. workspace_server/workspace_views.py +0 -533
  55. workspace_server/workspaces.html +0 -550
  56. workspace_server/workspaces.py +0 -632
  57. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/WHEEL +0 -0
  58. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,2267 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>__WORKSPACE_TITLE__</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
8
- <link rel="stylesheet" href="assets/content_views.css">
9
- <style>
10
- :root {
11
- color-scheme: light;
12
- --bg: #eef3f6;
13
- --panel: #f8fbfd;
14
- --panel-deep: #f2f6f8;
15
- --panel-raised: #ffffff;
16
- --panel-hover: #e8f1f5;
17
- --board-chrome: #ffffff;
18
- --ink: #172630;
19
- --muted: #60727d;
20
- --line: #cbd8df;
21
- --line-strong: #aebfc9;
22
- --accent: #087f9a;
23
- --accent-ink: #ffffff;
24
- --accent-soft: #e6f6f8;
25
- --warn: #b77908;
26
- --warn-ink: #62480c;
27
- --warn-soft: #fff7df;
28
- --info: #2868b2;
29
- --info-soft: #edf5ff;
30
- --success: #247a52;
31
- --success-line: #8bc9a8;
32
- --success-soft: #edf9f2;
33
- --danger: #b33d50;
34
- --danger-soft: #fff0f2;
35
- --violet: #6850a4;
36
- --violet-soft: #f4f0ff;
37
- --code: #f1f5f7;
38
- --entry-shadow: 0 1px 2px rgba(27, 57, 72, 0.06);
39
- --space-1: 2px;
40
- --space-2: 4px;
41
- --space-3: 6px;
42
- --space-4: 8px;
43
- --space-5: 10px;
44
- --space-6: 12px;
45
- --space-7: 14px;
46
- --radius-sm: 6px;
47
- --radius-md: 10px;
48
- --chat-width: min(42vw, 760px);
49
- --splitter-width: 8px;
50
- }
51
- * { box-sizing: border-box; }
52
- body {
53
- margin: 0;
54
- height: 100dvh;
55
- overflow: hidden;
56
- background: var(--bg);
57
- color: var(--ink);
58
- font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
59
- }
60
- button, textarea { font: inherit; }
61
- .pane-toggle { display: none; }
62
- .workspace {
63
- display: grid;
64
- grid-template-columns: minmax(0, 1fr) var(--splitter-width) var(--chat-width);
65
- height: 100dvh;
66
- min-width: 0;
67
- }
68
- .workspace.collapsed .splitter::after {
69
- background: var(--accent);
70
- }
71
- .board-pane {
72
- min-width: 0;
73
- height: 100dvh;
74
- display: grid;
75
- grid-template-rows: auto minmax(0, 1fr);
76
- background: var(--board-chrome);
77
- }
78
- .boardbar {
79
- min-width: 0;
80
- border-bottom: 1px solid var(--line);
81
- height: 44px;
82
- padding: 0 8px;
83
- color: var(--muted);
84
- font-size: 12px;
85
- display: flex;
86
- align-items: center;
87
- gap: 8px;
88
- white-space: nowrap;
89
- overflow: hidden;
90
- }
91
- .workspace-back {
92
- text-decoration: none;
93
- }
94
- .board {
95
- border: 0;
96
- min-width: 0;
97
- width: 100%;
98
- height: 100%;
99
- background: var(--board-chrome);
100
- }
101
- .splitter {
102
- position: relative;
103
- width: 8px;
104
- height: 100dvh;
105
- border: 0;
106
- border-right: 1px solid var(--line);
107
- background: var(--board-chrome);
108
- cursor: col-resize;
109
- padding: 0;
110
- touch-action: none;
111
- }
112
- .splitter::after {
113
- content: "";
114
- position: absolute;
115
- left: 50%;
116
- top: 50%;
117
- width: 2px;
118
- height: 28px;
119
- border-radius: 999px;
120
- background: var(--line-strong);
121
- transform: translate(-50%, -50%);
122
- }
123
- .splitter:hover,
124
- .workspace.resizing .splitter {
125
- background: #dce9ee;
126
- border-color: var(--line-strong);
127
- }
128
- .splitter:hover::after,
129
- .workspace.resizing .splitter::after {
130
- background: var(--accent);
131
- }
132
- .workspace.resizing,
133
- .workspace.resizing * {
134
- cursor: col-resize;
135
- user-select: none;
136
- }
137
- .chat {
138
- position: relative;
139
- min-width: 0;
140
- height: 100dvh;
141
- overflow: hidden;
142
- display: grid;
143
- grid-template-rows: auto minmax(0, 1fr) auto;
144
- background: var(--panel-raised);
145
- }
146
- .topbar {
147
- min-width: 0;
148
- height: 44px;
149
- padding: 6px 20px;
150
- border-bottom: 1px solid var(--line);
151
- display: grid;
152
- align-content: center;
153
- }
154
- .tabbar {
155
- display: flex;
156
- align-items: center;
157
- gap: 8px;
158
- min-width: 0;
159
- }
160
- .context-control {
161
- position: relative;
162
- display: flex;
163
- flex: 0 0 auto;
164
- }
165
- .context-meter {
166
- --context-remaining: 0%;
167
- --context-meter-size: 24px;
168
- position: relative;
169
- display: inline-grid;
170
- place-items: center;
171
- width: var(--context-meter-size);
172
- height: var(--context-meter-size);
173
- border: 0;
174
- padding: 0;
175
- border-radius: 999px;
176
- background: conic-gradient(var(--accent) 0 var(--context-remaining), var(--line) 0 100%);
177
- color: var(--ink);
178
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
179
- font-size: 10px;
180
- font-weight: 600;
181
- line-height: 1;
182
- flex: 0 0 auto;
183
- cursor: pointer;
184
- }
185
- .context-meter:focus-visible {
186
- outline: 2px solid var(--accent);
187
- outline-offset: 3px;
188
- }
189
- .context-meter::before {
190
- content: "";
191
- position: absolute;
192
- inset: 3px;
193
- border-radius: 999px;
194
- background: var(--panel-raised);
195
- }
196
- .context-meter-value {
197
- position: relative;
198
- z-index: 1;
199
- }
200
- .context-details {
201
- position: absolute;
202
- bottom: 100%;
203
- left: -6px;
204
- z-index: 5;
205
- width: 264px;
206
- max-width: calc(100vw - 48px);
207
- padding-bottom: 10px;
208
- color: var(--ink);
209
- font-size: 12px;
210
- line-height: 1.5;
211
- }
212
- .context-details-card {
213
- padding: 12px 14px;
214
- border: 1px solid var(--line);
215
- border-radius: 10px;
216
- background: var(--panel-raised);
217
- box-shadow: 0 6px 24px rgba(27, 57, 72, 0.12);
218
- }
219
- .context-details-heading {
220
- display: flex;
221
- justify-content: space-between;
222
- font-weight: 600;
223
- }
224
- .context-details-heading span {
225
- color: var(--muted);
226
- font-weight: 400;
227
- }
228
- .context-details dl {
229
- display: grid;
230
- grid-template-columns: 1fr auto;
231
- gap: 4px 12px;
232
- margin: 8px 0;
233
- }
234
- .context-details dt { color: var(--muted); }
235
- .context-details dd {
236
- margin: 0;
237
- font-variant-numeric: tabular-nums;
238
- text-align: right;
239
- }
240
- .context-details p { margin: 0; color: var(--muted); }
241
- .tabs {
242
- display: flex;
243
- align-items: center;
244
- gap: 4px;
245
- min-width: 0;
246
- overflow-x: auto;
247
- scrollbar-width: none;
248
- }
249
- .tabs::-webkit-scrollbar {
250
- display: none;
251
- }
252
- .tab {
253
- position: relative;
254
- display: inline-flex;
255
- align-items: center;
256
- gap: var(--space-2);
257
- min-width: 88px;
258
- max-width: 180px;
259
- height: 30px;
260
- border: 0;
261
- border-radius: var(--radius-md);
262
- background: var(--panel-deep);
263
- color: var(--muted);
264
- padding: 0 8px 0 12px;
265
- font-size: 13px;
266
- cursor: pointer;
267
- user-select: none;
268
- }
269
- .tab:hover {
270
- background: var(--panel-hover);
271
- color: var(--ink);
272
- }
273
- .tab-new:focus-visible,
274
- .workspace-back:focus-visible {
275
- outline: 2px solid var(--accent);
276
- outline-offset: 1px;
277
- }
278
- .tab.active {
279
- background: var(--accent-soft);
280
- color: var(--accent);
281
- font-weight: 600;
282
- }
283
- .tab-status {
284
- position: relative;
285
- display: inline-flex;
286
- align-items: center;
287
- justify-content: center;
288
- width: 15px;
289
- height: 15px;
290
- border: 1px solid transparent;
291
- border-radius: 999px;
292
- background: transparent;
293
- padding: 0;
294
- flex: 0 0 auto;
295
- }
296
- .tab-status::before {
297
- content: "";
298
- width: 5px;
299
- height: 5px;
300
- border-radius: 999px;
301
- background: var(--accent);
302
- animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
303
- }
304
- .tab-status[hidden],
305
- .tab-label[hidden] {
306
- display: none;
307
- }
308
- .tab-label {
309
- min-width: 0;
310
- overflow: hidden;
311
- text-overflow: ellipsis;
312
- white-space: nowrap;
313
- flex: 1;
314
- text-align: left;
315
- border: 0;
316
- padding: 0;
317
- background: transparent;
318
- color: inherit;
319
- font: inherit;
320
- cursor: inherit;
321
- }
322
- .tab-label:focus-visible {
323
- outline: 0;
324
- text-decoration: underline;
325
- text-underline-offset: 3px;
326
- }
327
- .tab-title-input {
328
- min-width: 0;
329
- width: 0;
330
- flex: 1;
331
- border: 0;
332
- padding: 0;
333
- outline: 0;
334
- background: transparent;
335
- color: inherit;
336
- font: inherit;
337
- user-select: text;
338
- }
339
- .tab-close,
340
- .tab-new,
341
- .workspace-back {
342
- width: 31px;
343
- height: 31px;
344
- border: 0;
345
- border-radius: var(--radius-md);
346
- background: var(--panel-raised);
347
- color: var(--muted);
348
- padding: 0;
349
- line-height: 1;
350
- cursor: pointer;
351
- flex: 0 0 auto;
352
- }
353
- .tab-close {
354
- display: grid;
355
- place-items: center;
356
- width: 18px;
357
- height: 18px;
358
- border: 0;
359
- background: transparent;
360
- opacity: 0;
361
- visibility: hidden;
362
- pointer-events: none;
363
- }
364
- .tab-close svg {
365
- display: block;
366
- width: 12px;
367
- height: 12px;
368
- }
369
- .tab-new,
370
- .workspace-back {
371
- display: grid;
372
- place-items: center;
373
- width: 30px;
374
- height: 30px;
375
- background: var(--panel-deep);
376
- }
377
- .tab-new svg,
378
- .workspace-back svg {
379
- display: block;
380
- width: 18px;
381
- height: 18px;
382
- }
383
- .tab.active .tab-close {
384
- opacity: 0.85;
385
- visibility: visible;
386
- pointer-events: auto;
387
- }
388
- .tab.active .tab-close:disabled {
389
- opacity: 0.22;
390
- cursor: default;
391
- }
392
- .tab-close:hover,
393
- .tab-new:hover,
394
- .workspace-back:hover {
395
- color: var(--ink);
396
- background: var(--panel-hover);
397
- }
398
- .log {
399
- min-height: 0;
400
- overflow-y: auto;
401
- overflow-x: hidden;
402
- padding: 16px 20px 20px;
403
- display: flex;
404
- flex-direction: column;
405
- gap: 14px;
406
- background: var(--panel-raised);
407
- scrollbar-width: thin;
408
- scrollbar-color: var(--line) transparent;
409
- }
410
- .entry {
411
- min-width: 0;
412
- flex: 0 0 auto;
413
- line-height: 1.6;
414
- }
415
- .entry-label {
416
- margin-bottom: 8px;
417
- font-size: 12px;
418
- font-weight: 600;
419
- color: var(--muted);
420
- }
421
- .entry.user {
422
- align-self: flex-end;
423
- max-width: 92%;
424
- padding: 7px 12px;
425
- border-radius: 10px 10px 3px 10px;
426
- background: var(--success-soft);
427
- }
428
- .entry.response,
429
- .entry.assistant {
430
- padding: 0;
431
- }
432
- .entry.thinking,
433
- .entry.control,
434
- .entry.tool {
435
- border-left: 2px solid var(--line);
436
- padding: 2px 0 2px 10px;
437
- color: var(--muted);
438
- font-size: 13px;
439
- }
440
- .entry.thinking {
441
- border-left-color: var(--accent);
442
- animation: activity-breathe 1.6s ease-in-out infinite;
443
- }
444
- .entry.control {
445
- white-space: pre-wrap;
446
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
447
- }
448
- .entry.tool {
449
- font-size: 12px;
450
- }
451
- .entry.error {
452
- border-left: 2px solid var(--danger);
453
- border-radius: var(--radius-sm);
454
- padding: 12px 16px;
455
- background: var(--danger-soft);
456
- color: var(--danger);
457
- }
458
- .empty-chat {
459
- margin: auto 0;
460
- padding: 36px 0 56px;
461
- max-width: 360px;
462
- }
463
- .empty-chat .entry-label { color: var(--accent); }
464
- .empty-chat h1 { margin: 12px 0; font-size: 28px; line-height: 1.25; font-weight: 600; letter-spacing: -0.6px; }
465
- .empty-chat p { margin: 0; color: var(--muted); line-height: 1.75; }
466
- .text {
467
- white-space: pre-wrap;
468
- overflow-wrap: anywhere;
469
- }
470
- .markdown {
471
- white-space: normal;
472
- overflow-wrap: anywhere;
473
- }
474
- .markdown p {
475
- margin: 0 0 8px;
476
- }
477
- .markdown p:last-child,
478
- .markdown ul:last-child,
479
- .markdown ol:last-child,
480
- .markdown pre:last-child,
481
- .markdown table:last-child {
482
- margin-bottom: 0;
483
- }
484
- .markdown ul,
485
- .markdown ol {
486
- margin: 0 0 8px 18px;
487
- padding: 0;
488
- }
489
- .markdown li {
490
- margin: 2px 0;
491
- }
492
- .markdown pre {
493
- margin: 0 0 10px;
494
- padding: 10px 12px;
495
- border: 0;
496
- border-radius: var(--radius-md);
497
- background: var(--code);
498
- overflow-x: auto;
499
- white-space: pre;
500
- }
501
- .markdown pre code {
502
- padding: 0;
503
- border: 0;
504
- background: transparent;
505
- font-size: 12px;
506
- }
507
- .markdown table {
508
- width: 100%;
509
- margin: 0 0 var(--space-4);
510
- border-collapse: collapse;
511
- font-size: 13px;
512
- }
513
- .markdown th,
514
- .markdown td {
515
- border: 1px solid var(--line);
516
- padding: var(--space-3) var(--space-4);
517
- text-align: left;
518
- vertical-align: top;
519
- }
520
- .markdown th {
521
- background: var(--accent-soft);
522
- font-weight: 650;
523
- }
524
- .markdown a {
525
- color: var(--info);
526
- text-decoration: none;
527
- }
528
- .markdown a:hover {
529
- text-decoration: underline;
530
- }
531
- .markdown h1, .markdown h2, .markdown h3 { line-height: 1.4; margin: 16px 0 8px; font-weight: 600; }
532
- .markdown h1 { font-size: 20px; }
533
- .markdown h2 { font-size: 18px; }
534
- .markdown h3 { font-size: 16px; }
535
- .markdown > :first-child { margin-top: 0; }
536
- .markdown blockquote { margin: 16px 0; padding-left: 16px; border-left: 2px solid var(--line); color: var(--muted); }
537
- .markdown img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }
538
- .markdown .katex-display {
539
- margin: 0 0 var(--space-4);
540
- overflow-x: auto;
541
- overflow-y: hidden;
542
- overflow-wrap: normal;
543
- word-break: normal;
544
- }
545
- .markdown .katex {
546
- overflow-wrap: normal;
547
- word-break: normal;
548
- }
549
- .markdown .katex .text {
550
- white-space: normal;
551
- overflow-wrap: normal;
552
- word-break: normal;
553
- }
554
- .markdown .katex-display:last-child {
555
- margin-bottom: 0;
556
- }
557
- .markdown .mermaid-figure {
558
- margin: 0 0 var(--space-4);
559
- }
560
- .markdown .mermaid-diagram {
561
- overflow-x: auto;
562
- padding: 8px 0;
563
- color: var(--muted);
564
- font-size: 13px;
565
- }
566
- .markdown .mermaid-diagram svg {
567
- display: block;
568
- height: auto;
569
- }
570
- .markdown .mermaid-source {
571
- flex: 1;
572
- min-width: 0;
573
- color: var(--muted);
574
- font-size: 12px;
575
- }
576
- .markdown .mermaid-source summary {
577
- width: fit-content;
578
- cursor: pointer;
579
- }
580
- .markdown .mermaid-source pre {
581
- margin-top: 8px;
582
- }
583
- .mermaid-controls,
584
- .mermaid-viewer-header {
585
- display: flex;
586
- align-items: flex-start;
587
- gap: 12px;
588
- }
589
- .mermaid-controls button,
590
- .mermaid-viewer button {
591
- flex: 0 0 auto;
592
- padding: 1px 5px;
593
- border: 0;
594
- border-radius: 4px;
595
- background: transparent;
596
- color: var(--muted);
597
- font: inherit;
598
- font-size: 12px;
599
- cursor: pointer;
600
- }
601
- .mermaid-controls button:hover,
602
- .mermaid-viewer button:hover {
603
- background: var(--accent-soft);
604
- color: var(--ink);
605
- }
606
- .mermaid-viewer {
607
- width: min(1400px, calc(100vw - 32px));
608
- max-height: calc(100dvh - 32px);
609
- padding: 16px;
610
- border: 1px solid var(--line);
611
- border-radius: var(--radius-md);
612
- background: var(--panel);
613
- color: var(--ink);
614
- }
615
- .mermaid-viewer::backdrop {
616
- background: rgba(23, 38, 48, 0.35);
617
- }
618
- .mermaid-viewer-header {
619
- margin-bottom: 12px;
620
- }
621
- .mermaid-viewer-header strong {
622
- flex: 1;
623
- font-size: 14px;
624
- }
625
- .mermaid-viewer-content {
626
- max-height: calc(100dvh - 110px);
627
- overflow: auto;
628
- }
629
- .mermaid-viewer-content img {
630
- display: block;
631
- max-width: 100%;
632
- height: auto;
633
- margin: auto;
634
- }
635
- .mermaid-viewer-content.actual-size img {
636
- max-width: none;
637
- }
638
- .composer {
639
- position: relative;
640
- min-width: 0;
641
- padding: 8px 20px 10px;
642
- display: grid;
643
- gap: 6px;
644
- align-content: end;
645
- background: var(--panel-raised);
646
- }
647
- .composer-surface {
648
- border: 1px solid var(--line-strong);
649
- border-radius: 10px;
650
- background: var(--panel-raised);
651
- box-shadow: 0 4px 16px rgba(27, 57, 72, 0.04);
652
- transition: border-color 120ms, box-shadow 120ms;
653
- }
654
- .composer-surface:focus-within {
655
- border-color: var(--accent);
656
- box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.08);
657
- }
658
- .composer-box {
659
- min-width: 0;
660
- padding: 4px 10px 4px 6px;
661
- display: flex;
662
- align-items: center;
663
- gap: 10px;
664
- }
665
- .queue-hint {
666
- display: flex;
667
- align-items: center;
668
- gap: var(--space-2);
669
- min-width: 0;
670
- max-width: 65%;
671
- }
672
- .queue-hint[hidden],
673
- .queue-indicator[hidden] {
674
- display: none;
675
- }
676
- .queue-indicator {
677
- display: inline-flex;
678
- align-items: center;
679
- gap: var(--space-2);
680
- min-width: 0;
681
- max-width: 160px;
682
- height: 22px;
683
- padding: 0 7px;
684
- border: 0;
685
- border-radius: var(--radius-sm);
686
- font-size: 12px;
687
- color: var(--muted);
688
- background: var(--panel-deep);
689
- }
690
- .queue-symbol {
691
- flex-shrink: 0;
692
- font-size: 12px;
693
- line-height: 1;
694
- }
695
- .queue-symbol svg {
696
- display: block;
697
- width: 14px;
698
- height: 14px;
699
- }
700
- .queue-preview {
701
- min-width: 0;
702
- overflow: hidden;
703
- text-overflow: ellipsis;
704
- white-space: nowrap;
705
- }
706
- textarea {
707
- min-width: 0;
708
- resize: none;
709
- max-height: 35vh;
710
- width: 100%;
711
- min-height: 28px;
712
- border: 0;
713
- padding: 3px 0;
714
- display: block;
715
- background: transparent;
716
- color: var(--ink);
717
- font: inherit;
718
- line-height: 22px;
719
- overflow-y: auto;
720
- scrollbar-width: none;
721
- }
722
- textarea::-webkit-scrollbar {
723
- display: none;
724
- }
725
- textarea:focus {
726
- outline: 0;
727
- }
728
- .actions {
729
- display: flex;
730
- justify-content: flex-end;
731
- align-items: center;
732
- }
733
- .spinner-wrap {
734
- position: relative;
735
- display: flex;
736
- align-items: center;
737
- gap: var(--space-3);
738
- max-width: 100%;
739
- min-width: 0;
740
- padding: 4px 6px;
741
- border-bottom: 1px solid var(--line);
742
- border-radius: 9px 9px 0 0;
743
- background: var(--panel);
744
- }
745
- .prompt-row {
746
- display: flex;
747
- align-items: center;
748
- gap: var(--space-2);
749
- min-width: 0;
750
- }
751
- .spinner {
752
- position: relative;
753
- display: inline-flex;
754
- align-items: center;
755
- max-width: 100%;
756
- min-width: 0;
757
- height: 22px;
758
- color: var(--muted);
759
- font-size: 12px;
760
- white-space: nowrap;
761
- overflow: hidden;
762
- text-overflow: ellipsis;
763
- padding: var(--space-1) var(--space-4) var(--space-1) 18px;
764
- border-radius: var(--radius-sm);
765
- background: transparent;
766
- border: 1px solid transparent;
767
- font: inherit;
768
- font-size: 12px;
769
- cursor: pointer;
770
- }
771
- .spinner:empty {
772
- display: none;
773
- }
774
- .spinner:hover {
775
- background: var(--panel-deep);
776
- }
777
- .spinner-wrap:not(:has(.spinner:not(:empty), .queue-hint:not([hidden]))) {
778
- display: none;
779
- }
780
- .spinner:focus-visible {
781
- outline: 2px solid var(--accent);
782
- outline-offset: 1px;
783
- }
784
- .spinner:not(:empty)::before {
785
- content: "";
786
- position: absolute;
787
- left: 7px;
788
- top: 50%;
789
- width: 6px;
790
- height: 6px;
791
- margin-top: -3px;
792
- border-radius: 999px;
793
- background: var(--accent);
794
- animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
795
- }
796
- .spinner.notify-enabled {
797
- color: var(--accent);
798
- background: var(--accent-soft);
799
- }
800
- .toast {
801
- position: absolute;
802
- left: 0;
803
- bottom: 100%;
804
- z-index: 5;
805
- width: max-content;
806
- max-width: min(320px, calc(100vw - var(--space-6) * 2));
807
- border: 1px solid var(--line-strong);
808
- border-radius: var(--radius-sm);
809
- background: var(--panel-raised);
810
- color: var(--ink);
811
- box-shadow: 0 8px 20px rgba(27, 57, 72, 0.14);
812
- padding: var(--space-4) var(--space-6);
813
- font-size: 12px;
814
- opacity: 0;
815
- pointer-events: none;
816
- transform: translateY(4px);
817
- transition: opacity 120ms ease, transform 120ms ease;
818
- margin-bottom: var(--space-3);
819
- }
820
- .toast.visible {
821
- opacity: 1;
822
- transform: translateY(0);
823
- }
824
- @keyframes spinner-pulse {
825
- 0%, 100% {
826
- opacity: 0.45;
827
- transform: scale(0.82);
828
- }
829
- 50% {
830
- opacity: 1;
831
- transform: scale(1.08);
832
- }
833
- }
834
- @keyframes activity-breathe {
835
- 0%, 100% { border-left-color: var(--line); }
836
- 50% { border-left-color: var(--accent); }
837
- }
838
- @media (prefers-reduced-motion: reduce) {
839
- .entry.thinking,
840
- .tab-status::before,
841
- .spinner:not(:empty)::before {
842
- animation: none;
843
- }
844
- .tab-status::before,
845
- .spinner:not(:empty)::before {
846
- opacity: 0.8;
847
- transform: none;
848
- }
849
- }
850
- code {
851
- background: var(--code);
852
- border: 1px solid var(--line);
853
- border-radius: var(--radius-sm);
854
- padding: 1px 4px;
855
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
856
- font-size: 12px;
857
- }
858
- @media (max-width: 760px) {
859
- .pane-toggle {
860
- display: inline-grid;
861
- place-items: center;
862
- width: 30px;
863
- height: 30px;
864
- margin-left: auto;
865
- padding: 0;
866
- flex: 0 0 auto;
867
- border: 0;
868
- border-radius: var(--radius-sm);
869
- background: transparent;
870
- color: var(--muted);
871
- cursor: pointer;
872
- }
873
- .pane-toggle:hover { background: var(--panel-deep); color: var(--ink); }
874
- .pane-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
875
- .pane-toggle svg { width: 18px; height: 18px; }
876
- .workspace { height: 100dvh; display: block; }
877
- .board-pane, .chat { height: 100%; }
878
- .splitter { display: none; }
879
- .workspace[data-mobile-pane="chat"] .board-pane,
880
- .workspace[data-mobile-pane="board"] .chat { display: none; }
881
- .topbar { height: 44px; padding: 6px 16px; }
882
- .log { padding: 14px 16px; gap: 14px; }
883
- .composer { padding: 8px 16px max(10px, env(safe-area-inset-bottom)); }
884
- textarea { font-size: 16px; line-height: 24px; padding: 2px 0; }
885
- .tab-title-input { font-size: 16px; }
886
- .empty-chat h1 { font-size: 26px; }
887
- }
888
- </style>
889
- </head>
890
- <body>
891
- <div class="workspace" data-mobile-pane="chat">
892
- <section class="board-pane" aria-label="workspace board">
893
- <div class="boardbar">
894
- <a class="workspace-back" href="/" title="Back to workspaces" aria-label="Back to workspaces">
895
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
896
- stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
897
- <path d="M15 6l-6 6 6 6"/>
898
- </svg>
899
- </a>
900
- <div id="contentTabs" class="tabs content-tabs" role="tablist" aria-label="Board views"
901
- data-board-title="Workspace: __WORKSPACE_TITLE__&#10;Current dir: __CWD_LABEL__&#10;Canvas file: __BOARD_LABEL__"></div>
902
- <span id="viewStatus" class="content-status" role="status" hidden></span>
903
- <button class="pane-toggle" type="button" data-pane="chat" title="Show chat" aria-label="Show chat">
904
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
905
- stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
906
- <path d="M5 4h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"/>
907
- </svg>
908
- </button>
909
- </div>
910
- <div class="content-area">
911
- <iframe id="boardFrame" class="board" src="board" title="Canvas"></iframe>
912
- <div id="contentView" class="content-view" role="tabpanel" hidden></div>
913
- </div>
914
- </section>
915
- <button id="splitter" class="splitter" type="button" aria-label="Resize chat panel"></button>
916
- <section class="chat" aria-label="pycodex chat">
917
- <div class="topbar">
918
- <div class="tabbar">
919
- <div id="tabs" class="tabs" role="tablist"></div>
920
- <button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">
921
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
922
- stroke-linecap="round" aria-hidden="true">
923
- <path d="M12 5v14M5 12h14"/>
924
- </svg>
925
- </button>
926
- <button class="pane-toggle" type="button" data-pane="board" title="Show board" aria-label="Show board">
927
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
928
- stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
929
- <rect x="3" y="3" width="18" height="18" rx="3"/>
930
- <path d="M3 9h18M9 9v12"/>
931
- </svg>
932
- </button>
933
- </div>
934
- </div>
935
- <div id="log" class="log" aria-label="Conversation"></div>
936
- <form id="composer" class="composer">
937
- <div class="composer-surface">
938
- <div class="spinner-wrap">
939
- <div id="toast" class="toast" role="status" aria-live="polite"></div>
940
- <div id="queueHint" class="queue-hint" role="status" aria-live="polite" hidden>
941
- <span id="steerHint" class="queue-indicator" hidden>
942
- <span class="queue-symbol" role="img" aria-label="Steer">↑</span>
943
- <span class="queue-preview"></span>
944
- </span>
945
- <span id="enqueueHint" class="queue-indicator enqueue" hidden>
946
- <span class="queue-symbol" role="img" aria-label="Queue">
947
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"
948
- stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
949
- <circle cx="12" cy="12" r="9"/>
950
- <path d="M12 7v5l3 2"/>
951
- </svg>
952
- </span>
953
- <span class="queue-preview"></span>
954
- </span>
955
- </div>
956
- <button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
957
- </div>
958
- <div class="composer-box">
959
- <div id="contextControl" class="context-control">
960
- <button id="contextMeter" class="context-meter" type="button" aria-label="Context usage"
961
- aria-expanded="false" aria-controls="contextDetails">
962
- <span id="contextMeterValue" class="context-meter-value">—</span>
963
- </button>
964
- <div id="contextDetails" class="context-details" role="region" aria-label="Context usage" hidden>
965
- <div class="context-details-card">
966
- <div class="context-details-heading">Context usage <span>tokens</span></div>
967
- <dl>
968
- <dt>Current</dt><dd id="contextCurrent">—</dd>
969
- <dt>Compact limit</dt><dd id="contextCompact">—</dd>
970
- <dt>Max length</dt><dd id="contextMax">—</dd>
971
- </dl>
972
- <p id="contextNote"></p>
973
- </div>
974
- </div>
975
- </div>
976
- <textarea id="prompt" rows="1" aria-label="Message" title="Enter to send · Shift + Enter for a new line" placeholder="Message…"></textarea>
977
- </div>
978
- </div>
979
- </form>
980
- </section>
981
- </div>
982
- <template id="emptyChat">
983
- <div class="empty-chat">
984
- <div class="entry-label">pycodex</div>
985
- <h1>What are we working on?</h1>
986
- <p>Explore an idea, work through a problem, or build something together. Your conversation starts here.</p>
987
- </div>
988
- </template>
989
- <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
990
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
991
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
992
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
993
- <script type="module">
994
- import {isWorkspaceLink, mountContentViews} from "./assets/content_views.mjs";
995
-
996
- const log = document.getElementById("log");
997
- const spinner = document.getElementById("spinner");
998
- const queueHint = document.getElementById("queueHint");
999
- const steerHint = document.getElementById("steerHint");
1000
- const enqueueHint = document.getElementById("enqueueHint");
1001
- const toast = document.getElementById("toast");
1002
- const prompt = document.getElementById("prompt");
1003
- const form = document.getElementById("composer");
1004
- const boardFrame = document.getElementById("boardFrame");
1005
- const workspace = document.querySelector(".workspace");
1006
- const splitter = document.getElementById("splitter");
1007
- const tabsEl = document.getElementById("tabs");
1008
- const contextMeter = document.getElementById("contextMeter");
1009
- const contextMeterValue = document.getElementById("contextMeterValue");
1010
- const newTabButton = document.getElementById("newTab");
1011
- const workspaceTitle = document.title;
1012
- let pollTimer = null;
1013
- let boardPollTimer = null;
1014
- let sessionPoll = null;
1015
- let boardPollInFlight = false;
1016
- let lastBoardSignature = null;
1017
- let lastRenderedSignature = "";
1018
- let resizeStart = null;
1019
- let spinnerText = "";
1020
- let toastTimer = null;
1021
- let activeSessionId = "";
1022
- let sessions = [];
1023
- let isComposing = false;
1024
- const sessionState = new Map();
1025
- let restoreScrollTop = null;
1026
- let suppressScrollSave = false;
1027
- const markdownRenderer = window.markdownit
1028
- ? window.markdownit({html: false, linkify: true, breaks: false})
1029
- : null;
1030
- if (markdownRenderer) {
1031
- markdownRenderer.renderer.rules.link_open = (tokens, index, options, env, renderer) => {
1032
- const token = tokens[index];
1033
- const href = token.attrGet("href");
1034
- if (isWorkspaceLink(href)) {
1035
- token.attrSet("data-workspace-link", href);
1036
- // Preserve file/URI destinations without widening DOMPurify's protocol allowlist.
1037
- token.attrSet("href", "#");
1038
- }
1039
- return renderer.renderToken(tokens, index, options);
1040
- };
1041
- }
1042
- const mathDelimiters = [
1043
- {left: "$$", right: "$$", display: true},
1044
- {left: "\\[", right: "\\]", display: true},
1045
- {left: "\\(", right: "\\)", display: false},
1046
- {left: "$", right: "$", display: false},
1047
- ];
1048
- const mathTokenPrefix = "\uE000math";
1049
- const mathTokenSuffix = "\uE001";
1050
- const renderMermaidSvg = (() => {
1051
- let rendererPromise = null;
1052
- let diagramId = 0;
1053
- return async (source, figure) => {
1054
- if (!rendererPromise) {
1055
- rendererPromise = import(
1056
- "https://cdn.jsdelivr.net/npm/mermaid@11.12.0/dist/mermaid.esm.min.mjs"
1057
- ).then(({default: renderer}) => {
1058
- renderer.initialize({
1059
- startOnLoad: false,
1060
- securityLevel: "strict",
1061
- suppressErrorRendering: true,
1062
- htmlLabels: false,
1063
- flowchart: {htmlLabels: false},
1064
- theme: "neutral",
1065
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1066
- });
1067
- return renderer;
1068
- });
1069
- }
1070
- const renderer = await rendererPromise;
1071
- if (!figure.isConnected) return null;
1072
- const measure = document.createElement("div");
1073
- measure.style.cssText =
1074
- "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none";
1075
- document.body.appendChild(measure);
1076
- try {
1077
- diagramId += 1;
1078
- return await renderer.render("workspace-diagram-" + diagramId, source, measure);
1079
- } finally {
1080
- measure.remove();
1081
- }
1082
- };
1083
- })();
1084
-
1085
- function relativeUrl(path) {
1086
- return new URL(path, window.location.href).toString();
1087
- }
1088
-
1089
- function addEntry(_role, text, cls, shouldStick = null) {
1090
- const stickToBottom = shouldStick === null ? isNearBottom() : shouldStick;
1091
- const entry = document.createElement("div");
1092
- entry.className = "entry " + (cls || _role);
1093
- const body = document.createElement("div");
1094
- body.className = "text";
1095
- body.textContent = text || "";
1096
- entry.appendChild(body);
1097
- log.appendChild(entry);
1098
- if (stickToBottom) scrollToBottom();
1099
- return body;
1100
- }
1101
-
1102
- function addMarkdownEntry(role, text, cls, shouldStick = null) {
1103
- const stickToBottom = shouldStick === null ? isNearBottom() : shouldStick;
1104
- const entry = document.createElement("div");
1105
- entry.className = "entry " + (cls || role);
1106
- const body = document.createElement("div");
1107
- body.className = "text markdown";
1108
- entry.appendChild(body);
1109
- log.appendChild(entry);
1110
- renderMarkdownInto(body, text || "");
1111
- if (stickToBottom) scrollToBottom();
1112
- return body;
1113
- }
1114
-
1115
- function renderMarkdownInto(root, source) {
1116
- if (!markdownRenderer || !window.DOMPurify) {
1117
- root.textContent = source || "";
1118
- return;
1119
- }
1120
- const protectedMath = protectMath(String(source || ""));
1121
- const dirtyHtml = markdownRenderer.render(protectedMath.source);
1122
- root.innerHTML = window.DOMPurify.sanitize(dirtyHtml, {
1123
- USE_PROFILES: {html: true},
1124
- ADD_ATTR: ["target", "rel"],
1125
- });
1126
- renderProtectedMath(root, protectedMath.expressions);
1127
- root.querySelectorAll("a").forEach((anchor) => {
1128
- const link = anchor.getAttribute("data-workspace-link");
1129
- if (isWorkspaceLink(link)) {
1130
- anchor.setAttribute("href", link);
1131
- } else if (!(anchor.getAttribute("href") || "").startsWith("#")) {
1132
- anchor.target = "_blank";
1133
- anchor.rel = "noopener noreferrer";
1134
- }
1135
- });
1136
- renderMermaidInto(root);
1137
- }
1138
-
1139
- async function renderMermaidInto(root) {
1140
- for (const code of root.querySelectorAll("pre > code.language-mermaid")) {
1141
- if (!code.isConnected) return;
1142
- const source = code.textContent;
1143
- const pre = code.parentElement;
1144
- const figure = document.createElement("figure");
1145
- figure.className = "mermaid-figure";
1146
- const diagram = document.createElement("div");
1147
- diagram.className = "mermaid-diagram";
1148
- diagram.textContent = "Rendering diagram…";
1149
- const details = document.createElement("details");
1150
- details.className = "mermaid-source";
1151
- details.open = true;
1152
- const summary = document.createElement("summary");
1153
- summary.textContent = "Diagram source";
1154
- details.appendChild(summary);
1155
- pre.replaceWith(figure);
1156
- details.appendChild(pre);
1157
- const controls = document.createElement("div");
1158
- controls.className = "mermaid-controls";
1159
- controls.appendChild(details);
1160
- figure.append(diagram, controls);
1161
- try {
1162
- const result = await renderMermaidSvg(source, figure);
1163
- if (!result || !figure.isConnected) return;
1164
- const stickToBottom = isNearBottom();
1165
- diagram.innerHTML = window.DOMPurify.sanitize(result.svg, {
1166
- USE_PROFILES: {svg: true, svgFilters: true},
1167
- });
1168
- const svg = diagram.querySelector("svg");
1169
- svg.style.width = svg.viewBox.baseVal.width + "px";
1170
- svg.style.maxWidth = "100%";
1171
- const expand = document.createElement("button");
1172
- expand.type = "button";
1173
- expand.textContent = "Expand";
1174
- expand.addEventListener("click", () => showMermaidDiagram(svg));
1175
- controls.appendChild(expand);
1176
- details.open = false;
1177
- if (stickToBottom) scrollToBottom();
1178
- } catch (error) {
1179
- if (!figure.isConnected) return;
1180
- diagram.textContent = "Diagram could not be rendered. Source is shown below.";
1181
- details.open = true;
1182
- console.warn("Mermaid diagram rendering failed", error);
1183
- }
1184
- }
1185
- }
1186
-
1187
- function showMermaidDiagram(svg) {
1188
- const copy = svg.cloneNode(true);
1189
- copy.style.removeProperty("width");
1190
- copy.style.removeProperty("max-width");
1191
- copy.setAttribute("width", svg.viewBox.baseVal.width);
1192
- copy.setAttribute("height", svg.viewBox.baseVal.height);
1193
- const imageUrl = URL.createObjectURL(new Blob(
1194
- [new XMLSerializer().serializeToString(copy)],
1195
- {type: "image/svg+xml"},
1196
- ));
1197
- const dialog = document.createElement("dialog");
1198
- dialog.className = "mermaid-viewer";
1199
- dialog.setAttribute("aria-label", "Expanded diagram");
1200
- const header = document.createElement("div");
1201
- header.className = "mermaid-viewer-header";
1202
- const title = document.createElement("strong");
1203
- title.textContent = "Diagram";
1204
- const zoom = document.createElement("button");
1205
- zoom.type = "button";
1206
- zoom.textContent = "Actual size";
1207
- const close = document.createElement("button");
1208
- close.type = "button";
1209
- close.textContent = "Close";
1210
- const content = document.createElement("div");
1211
- content.className = "mermaid-viewer-content";
1212
- const image = document.createElement("img");
1213
- image.src = imageUrl;
1214
- image.alt = "Diagram";
1215
- content.appendChild(image);
1216
- header.append(title, zoom, close);
1217
- dialog.append(header, content);
1218
- zoom.addEventListener("click", () => {
1219
- const actualSize = content.classList.toggle("actual-size");
1220
- zoom.textContent = actualSize ? "Fit width" : "Actual size";
1221
- });
1222
- close.addEventListener("click", () => dialog.close());
1223
- dialog.addEventListener("click", event => {
1224
- if (event.target === dialog) dialog.close();
1225
- });
1226
- dialog.addEventListener("close", () => {
1227
- URL.revokeObjectURL(imageUrl);
1228
- dialog.remove();
1229
- }, {once: true});
1230
- document.body.appendChild(dialog);
1231
- dialog.showModal();
1232
- }
1233
-
1234
- function isEscapedAt(source, index) {
1235
- let slashCount = 0;
1236
- for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) {
1237
- slashCount += 1;
1238
- }
1239
- return slashCount % 2 === 1;
1240
- }
1241
-
1242
- function countBackticks(source, index) {
1243
- let count = 0;
1244
- while (source[index + count] === "`") {
1245
- count += 1;
1246
- }
1247
- return count;
1248
- }
1249
-
1250
- function findMathEnd(source, start, delimiter) {
1251
- let searchFrom = start + delimiter.left.length;
1252
- while (searchFrom < source.length) {
1253
- const end = source.indexOf(delimiter.right, searchFrom);
1254
- if (end < 0) {
1255
- return -1;
1256
- }
1257
- if (!isEscapedAt(source, end)) {
1258
- if (
1259
- delimiter.left === "$" &&
1260
- (source[end - 1] === "$" || source[end + 1] === "$")
1261
- ) {
1262
- searchFrom = end + delimiter.right.length;
1263
- continue;
1264
- }
1265
- return end;
1266
- }
1267
- searchFrom = end + delimiter.right.length;
1268
- }
1269
- return -1;
1270
- }
1271
-
1272
- function protectMath(source) {
1273
- const expressions = [];
1274
- let protectedSource = "";
1275
- let index = 0;
1276
- while (index < source.length) {
1277
- if (source[index] === "`") {
1278
- const tickCount = countBackticks(source, index);
1279
- const fence = "`".repeat(tickCount);
1280
- const close = source.indexOf(fence, index + tickCount);
1281
- if (close >= 0) {
1282
- const end = close + tickCount;
1283
- protectedSource += source.slice(index, end);
1284
- index = end;
1285
- continue;
1286
- }
1287
- }
1288
-
1289
- let matched = null;
1290
- if (!isEscapedAt(source, index)) {
1291
- for (const delimiter of mathDelimiters) {
1292
- if (
1293
- source.indexOf(delimiter.left, index) !== index ||
1294
- (delimiter.left === "$" && source[index + 1] === "$")
1295
- ) {
1296
- continue;
1297
- }
1298
- const end = findMathEnd(source, index, delimiter);
1299
- if (end > index + delimiter.left.length) {
1300
- matched = {
1301
- delimiter: delimiter,
1302
- start: index,
1303
- end: end,
1304
- };
1305
- break;
1306
- }
1307
- }
1308
- }
1309
-
1310
- if (matched) {
1311
- const expressionIndex = expressions.length;
1312
- expressions.push({
1313
- body: source.slice(
1314
- matched.start + matched.delimiter.left.length,
1315
- matched.end,
1316
- ),
1317
- delimiter: matched.delimiter,
1318
- });
1319
- protectedSource += mathTokenPrefix + expressionIndex + mathTokenSuffix;
1320
- index = matched.end + matched.delimiter.right.length;
1321
- continue;
1322
- }
1323
-
1324
- protectedSource += source[index];
1325
- index += 1;
1326
- }
1327
- return {
1328
- source: protectedSource,
1329
- expressions: expressions,
1330
- };
1331
- }
1332
-
1333
- function renderProtectedMath(root, expressions) {
1334
- if (!expressions.length) {
1335
- return;
1336
- }
1337
- const expressionByToken = new Map();
1338
- expressions.forEach((expression, index) => {
1339
- expressionByToken.set(
1340
- mathTokenPrefix + index + mathTokenSuffix,
1341
- expression,
1342
- );
1343
- });
1344
- const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1345
- const textNodes = [];
1346
- let textNode = textWalker.nextNode();
1347
- while (textNode) {
1348
- textNodes.push(textNode);
1349
- textNode = textWalker.nextNode();
1350
- }
1351
- for (const textNode of textNodes) {
1352
- let value = textNode.nodeValue || "";
1353
- let cursor = 0;
1354
- let tokenIndex = value.indexOf(mathTokenPrefix, cursor);
1355
- if (tokenIndex < 0) {
1356
- continue;
1357
- }
1358
- const fragment = document.createDocumentFragment();
1359
- while (tokenIndex >= 0) {
1360
- const tokenEnd = value.indexOf(mathTokenSuffix, tokenIndex);
1361
- if (tokenEnd < 0) {
1362
- break;
1363
- }
1364
- const token = value.slice(tokenIndex, tokenEnd + mathTokenSuffix.length);
1365
- const expression = expressionByToken.get(token);
1366
- if (!expression) {
1367
- break;
1368
- }
1369
- if (tokenIndex > cursor) {
1370
- fragment.appendChild(
1371
- document.createTextNode(value.slice(cursor, tokenIndex)),
1372
- );
1373
- }
1374
- const mathNode = document.createElement("span");
1375
- const sourceText =
1376
- expression.delimiter.left +
1377
- expression.body +
1378
- expression.delimiter.right;
1379
- if (window.renderMathInElement) {
1380
- mathNode.textContent = sourceText;
1381
- window.renderMathInElement(mathNode, {
1382
- delimiters: [
1383
- {
1384
- left: expression.delimiter.left,
1385
- right: expression.delimiter.right,
1386
- display: expression.delimiter.display,
1387
- },
1388
- ],
1389
- throwOnError: false,
1390
- trust: false,
1391
- });
1392
- } else {
1393
- mathNode.textContent = expression.body;
1394
- }
1395
- fragment.appendChild(mathNode);
1396
- cursor = tokenEnd + mathTokenSuffix.length;
1397
- tokenIndex = value.indexOf(mathTokenPrefix, cursor);
1398
- }
1399
- if (cursor > 0) {
1400
- if (cursor < value.length) {
1401
- fragment.appendChild(document.createTextNode(value.slice(cursor)));
1402
- }
1403
- if (textNode.parentNode) {
1404
- textNode.parentNode.replaceChild(fragment, textNode);
1405
- }
1406
- }
1407
- }
1408
- restoreUnrenderedMathTokens(root, expressionByToken);
1409
- }
1410
-
1411
- function restoreUnrenderedMathTokens(root, expressionByToken) {
1412
- const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1413
- let textNode = textWalker.nextNode();
1414
- while (textNode) {
1415
- let value = textNode.nodeValue || "";
1416
- expressionByToken.forEach((expression, token) => {
1417
- const sourceText =
1418
- expression.delimiter.left +
1419
- expression.body +
1420
- expression.delimiter.right;
1421
- value = value.split(token).join(sourceText);
1422
- });
1423
- textNode.nodeValue = value;
1424
- textNode = textWalker.nextNode();
1425
- }
1426
- }
1427
-
1428
- function loadChatWidth() {
1429
- const stored = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
1430
- if (stored > 0) setChatWidth(stored);
1431
- }
1432
-
1433
- function centerSplit() {
1434
- setChatWidth(Math.max(0, (window.innerWidth - 8) / 2));
1435
- }
1436
-
1437
- function setChatWidth(width) {
1438
- const viewportWidth = window.innerWidth || 0;
1439
- const splitterWidth = 8;
1440
- const gutterWidth = Math.min(24, Math.max(12, Math.round(viewportWidth * 0.06)));
1441
- const availableWidth = Math.max(0, viewportWidth - splitterWidth);
1442
- const maxChatWidth = Math.max(gutterWidth, availableWidth - gutterWidth);
1443
- const collapseWidth = Math.min(
1444
- 280,
1445
- Math.max(120, Math.round(viewportWidth * 0.25)),
1446
- );
1447
- let nextWidth = Math.max(gutterWidth, Math.min(maxChatWidth, Math.round(width)));
1448
- let collapsed = false;
1449
- if (nextWidth < collapseWidth) {
1450
- nextWidth = gutterWidth;
1451
- collapsed = true;
1452
- } else if (availableWidth - nextWidth < collapseWidth) {
1453
- nextWidth = maxChatWidth;
1454
- collapsed = true;
1455
- }
1456
- workspace.style.setProperty("--chat-width", `${nextWidth}px`);
1457
- workspace.classList.toggle("collapsed", collapsed);
1458
- window.localStorage.setItem("pycodex.workspace.chatWidth", String(nextWidth));
1459
- }
1460
-
1461
- function startResize(event) {
1462
- if (!workspace) return;
1463
- const chatWidth = document.querySelector(".chat").getBoundingClientRect().width;
1464
- resizeStart = { pointerId: event.pointerId, startX: event.clientX, startWidth: chatWidth };
1465
- splitter.setPointerCapture(event.pointerId);
1466
- workspace.classList.add("resizing");
1467
- event.preventDefault();
1468
- }
1469
-
1470
- function updateResize(event) {
1471
- if (!resizeStart) return;
1472
- const delta = resizeStart.startX - event.clientX;
1473
- setChatWidth(resizeStart.startWidth + delta);
1474
- event.preventDefault();
1475
- }
1476
-
1477
- function endResize(event) {
1478
- if (!resizeStart) return;
1479
- try {
1480
- splitter.releasePointerCapture(resizeStart.pointerId);
1481
- } catch (_error) {
1482
- // Pointer capture can already be released by the browser.
1483
- }
1484
- resizeStart = null;
1485
- workspace.classList.remove("resizing");
1486
- event.preventDefault();
1487
- }
1488
-
1489
- function setSpinner(text, options = {}) {
1490
- const previousText = spinnerText;
1491
- const nextText = String(text || "").trim();
1492
- const wasRunning = Boolean(previousText);
1493
- const isRunning = Boolean(nextText);
1494
- spinnerText = nextText;
1495
- spinner.textContent = spinnerText;
1496
- if (wasRunning && !isRunning && !options.suppressDoneNotification) {
1497
- const state = stateForSession(activeSessionId);
1498
- if (state.wasRunning) notifySessionDone(activeSessionId);
1499
- }
1500
- updateSpinnerNotifyUi();
1501
- }
1502
-
1503
- function showToast(text) {
1504
- if (toastTimer) clearTimeout(toastTimer);
1505
- toast.textContent = text;
1506
- toast.classList.add("visible");
1507
- toastTimer = setTimeout(() => {
1508
- toastTimer = null;
1509
- toast.classList.remove("visible");
1510
- }, 1400);
1511
- }
1512
-
1513
- function updateSpinnerNotifyUi() {
1514
- const state = stateForSession(activeSessionId);
1515
- const enabled = Boolean(spinnerText && state.notifyWhenDone);
1516
- spinner.classList.toggle("notify-enabled", enabled);
1517
- spinner.setAttribute("aria-checked", enabled ? "true" : "false");
1518
- if (!spinnerText) {
1519
- spinner.title = "Spinner is idle";
1520
- } else if (enabled) {
1521
- spinner.title = "Notify when done is on";
1522
- } else {
1523
- spinner.title = "Notify when done is off";
1524
- }
1525
- }
1526
-
1527
- function notificationAvailable() {
1528
- return "Notification" in window;
1529
- }
1530
-
1531
- function requestNotificationPermission() {
1532
- try {
1533
- const requestResult = Notification.requestPermission();
1534
- if (requestResult && typeof requestResult.then === "function") {
1535
- return requestResult;
1536
- }
1537
- return Promise.resolve(Notification.permission);
1538
- } catch (_error) {
1539
- return Promise.resolve(Notification.permission);
1540
- }
1541
- }
1542
-
1543
- function notifySessionDone(sessionId, sessionSummary = null) {
1544
- if (!sessionId) return;
1545
- const state = stateForSession(sessionId);
1546
- if (!state.notifyWhenDone) return;
1547
- state.notifyWhenDone = false;
1548
- if (!notificationAvailable() || Notification.permission !== "granted") return;
1549
- const session = sessionSummary || sessionById(sessionId);
1550
- const title = (session && session.title) || "pycodex";
1551
- const lastAssistant = (session && session.last_assistant) || "Done.";
1552
- try {
1553
- new Notification(`session: ${title}`, {
1554
- body: lastAssistant,
1555
- tag: `pycodex-workspace-done-${sessionId}`,
1556
- });
1557
- } catch (_error) {
1558
- return;
1559
- }
1560
- }
1561
-
1562
- function sessionById(sessionId) {
1563
- return sessions.find((session) => (session.id || "") === sessionId) || null;
1564
- }
1565
-
1566
- function isSessionRunning(sessionId) {
1567
- if (sessionId === activeSessionId && spinnerText) return true;
1568
- const session = sessionById(sessionId);
1569
- if (session) return Boolean(session.running);
1570
- return false;
1571
- }
1572
-
1573
- async function toggleSpinnerNotification() {
1574
- const sessionId = activeSessionId;
1575
- if (!sessionId || !isSessionRunning(sessionId)) {
1576
- showToast("notify-on-done unavailable");
1577
- return;
1578
- }
1579
- const state = stateForSession(sessionId);
1580
- if (state.notifyWhenDone) {
1581
- state.notifyWhenDone = false;
1582
- showToast("notify-on-done canceled");
1583
- updateSpinnerNotifyUi();
1584
- return;
1585
- }
1586
- if (!notificationAvailable()) {
1587
- showToast("browser notifications unavailable");
1588
- return;
1589
- }
1590
- if (Notification.permission === "denied") {
1591
- showToast("browser notifications blocked");
1592
- return;
1593
- }
1594
- state.notifyWhenDone = true;
1595
- showToast("notify-on-done enabled");
1596
- updateSpinnerNotifyUi();
1597
- if (Notification.permission !== "granted") {
1598
- const permission = await requestNotificationPermission();
1599
- if (permission !== "granted") {
1600
- state.notifyWhenDone = false;
1601
- showToast("notify-on-done canceled");
1602
- updateSpinnerNotifyUi();
1603
- return;
1604
- }
1605
- }
1606
- if (!isSessionRunning(sessionId)) {
1607
- state.notifyWhenDone = false;
1608
- showToast("notify-on-done canceled");
1609
- }
1610
- updateSpinnerNotifyUi();
1611
- }
1612
-
1613
- function isNearBottom() {
1614
- return log.scrollHeight - log.scrollTop - log.clientHeight < 80;
1615
- }
1616
-
1617
- function scrollToBottom() {
1618
- log.scrollTop = log.scrollHeight;
1619
- }
1620
-
1621
- function resizePrompt() {
1622
- prompt.style.height = "auto";
1623
- const borderHeight = prompt.offsetHeight - prompt.clientHeight;
1624
- const maxHeight = Math.round(window.innerHeight * 0.35);
1625
- const nextHeight = Math.min(prompt.scrollHeight + borderHeight, maxHeight);
1626
- prompt.style.height = `${nextHeight}px`;
1627
- }
1628
-
1629
- function stateForSession(sessionId) {
1630
- const key = String(sessionId || "");
1631
- if (!sessionState.has(key)) {
1632
- sessionState.set(key, {
1633
- lastRenderedSignature: "",
1634
- scrollTop: null,
1635
- draft: "",
1636
- notifyWhenDone: false,
1637
- wasRunning: false,
1638
- inputRequest: null,
1639
- });
1640
- }
1641
- return sessionState.get(key);
1642
- }
1643
-
1644
- function saveActiveSessionUiState() {
1645
- if (!activeSessionId) return;
1646
- const state = stateForSession(activeSessionId);
1647
- if (log.scrollHeight > log.clientHeight) {
1648
- state.scrollTop = log.scrollTop;
1649
- }
1650
- state.draft = prompt.value;
1651
- state.lastRenderedSignature = lastRenderedSignature;
1652
- }
1653
-
1654
- function updateSessions(nextSessions) {
1655
- const incomingSessions = Array.isArray(nextSessions) ? nextSessions : sessions;
1656
- incomingSessions.forEach((session) => {
1657
- const sessionId = session.id || "";
1658
- if (!sessionId) return;
1659
- const state = stateForSession(sessionId);
1660
- const wasRunning = Boolean(state.wasRunning);
1661
- const isRunning = Boolean(session.running);
1662
- if (!wasRunning && isRunning) {
1663
- state.notifyWhenDone = false;
1664
- } else if (wasRunning && !isRunning) {
1665
- notifySessionDone(sessionId, session);
1666
- }
1667
- state.wasRunning = isRunning;
1668
- });
1669
- sessions = incomingSessions;
1670
- document.title = (sessions.some(session => session.running) ? "● " : "") + workspaceTitle;
1671
- if (!activeSessionId && sessions.length) {
1672
- activeSessionId = sessions[0].id || "";
1673
- }
1674
- renderTabs();
1675
- updatePromptPlaceholder();
1676
- updateContextMeter();
1677
- updateSpinnerNotifyUi();
1678
- }
1679
-
1680
- function renderTabs() {
1681
- const existingTabs = new Map(
1682
- Array.from(tabsEl.children, tab => [tab.dataset.sessionId, tab])
1683
- );
1684
- sessions.forEach((session, index) => {
1685
- const sessionId = session.id || "";
1686
- let tab = existingTabs.get(sessionId);
1687
- if (!tab) {
1688
- tab = document.createElement("div");
1689
- tab.dataset.sessionId = sessionId;
1690
- tab.setAttribute("role", "tab");
1691
- tab.addEventListener("click", () => switchSession(sessionId));
1692
-
1693
- const status = document.createElement("span");
1694
- status.className = "tab-status";
1695
- status.setAttribute("aria-hidden", "true");
1696
- tab.appendChild(status);
1697
-
1698
- const label = document.createElement("button");
1699
- label.type = "button";
1700
- label.className = "tab-label";
1701
- label.addEventListener("click", event => {
1702
- if (sessionId !== activeSessionId) return;
1703
- event.stopPropagation();
1704
- editSessionTitle(tab, sessionId);
1705
- });
1706
- tab.appendChild(label);
1707
-
1708
- const close = document.createElement("button");
1709
- close.type = "button";
1710
- close.className = "tab-close";
1711
- close.title = "Close session";
1712
- close.setAttribute("aria-label", "Close session");
1713
- close.innerHTML = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" ' +
1714
- 'stroke-width="1.5" stroke-linecap="round" aria-hidden="true">' +
1715
- '<path d="M4 4l8 8M12 4l-8 8"/></svg>';
1716
- close.addEventListener("click", event => {
1717
- event.stopPropagation();
1718
- closeSession(sessionId);
1719
- });
1720
- tab.appendChild(close);
1721
- }
1722
- existingTabs.delete(sessionId);
1723
- tab.className =
1724
- "tab" +
1725
- (sessionId === activeSessionId ? " active" : "") +
1726
- (session.running ? " running" : "");
1727
- tab.title = session.title || `Session ${index + 1}`;
1728
- tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
1729
- tab.setAttribute("aria-busy", String(Boolean(session.running)));
1730
- tab.querySelector(".tab-status").hidden = !session.running;
1731
- const label = tab.querySelector(".tab-label");
1732
- label.textContent = session.title || `Session ${index + 1}`;
1733
- label.title = sessionId === activeSessionId ? "Click to rename session" : tab.title;
1734
- tab.querySelector(".tab-close").disabled = sessions.length <= 1;
1735
- if (tabsEl.children[index] !== tab) {
1736
- tabsEl.insertBefore(tab, tabsEl.children[index] || null);
1737
- }
1738
- });
1739
- existingTabs.forEach(tab => tab.remove());
1740
- }
1741
-
1742
- function editSessionTitle(tab, sessionId) {
1743
- const session = sessionById(sessionId);
1744
- const label = tab.querySelector(".tab-label");
1745
- const input = document.createElement("input");
1746
- input.type = "text";
1747
- input.className = "tab-title-input";
1748
- input.setAttribute("aria-label", "Session title");
1749
- input.value = session.title || "";
1750
- input.placeholder = label.textContent;
1751
- tab.style.width = `${tab.getBoundingClientRect().width}px`;
1752
- label.hidden = true;
1753
- tab.insertBefore(input, label);
1754
- let finished = false;
1755
- const finish = save => {
1756
- if (finished) return;
1757
- finished = true;
1758
- const title = input.value.trim();
1759
- input.remove();
1760
- label.hidden = false;
1761
- tab.style.width = "";
1762
- if (save && title && title !== session.title) {
1763
- renameSession(sessionId, title);
1764
- }
1765
- };
1766
- input.addEventListener("click", event => event.stopPropagation());
1767
- input.addEventListener("keydown", event => {
1768
- if (event.isComposing) return;
1769
- if (event.key === "Enter" || event.key === "Escape") {
1770
- event.preventDefault();
1771
- finish(event.key === "Enter");
1772
- label.focus();
1773
- }
1774
- });
1775
- input.addEventListener("blur", () => finish(true));
1776
- input.focus();
1777
- input.select();
1778
- }
1779
-
1780
- async function renameSession(sessionId, title) {
1781
- try {
1782
- const response = await fetch(relativeUrl("api/session/message"), {
1783
- method: "POST",
1784
- headers: {"Content-Type": "application/json"},
1785
- body: JSON.stringify({prompt: `/title ${title}`, sender: "web", session_id: sessionId}),
1786
- });
1787
- const payload = await response.json();
1788
- if (!response.ok || !payload.ok) {
1789
- throw new Error(payload.error || `HTTP ${response.status}`);
1790
- }
1791
- const session = sessionById(sessionId);
1792
- if (session) {
1793
- session.title = payload.snapshot.title;
1794
- renderTabs();
1795
- }
1796
- } catch (error) {
1797
- addEntry("error", `Could not rename session: ${error.message}`, "error");
1798
- }
1799
- }
1800
-
1801
- function updatePromptPlaceholder(snapshot = null) {
1802
- const session = snapshot || sessionById(activeSessionId);
1803
- const label = session && session.model ? `Message to ${session.model}` : "Message";
1804
- prompt.placeholder = `${label}…`;
1805
- prompt.setAttribute("aria-label", label);
1806
- }
1807
-
1808
- function updateContextMeter(snapshot = null) {
1809
- const session = snapshot || sessionById(activeSessionId);
1810
- if (!session) return;
1811
- const percent = session.context_remaining_percent;
1812
- const compact = session.auto_compact_token_limit;
1813
- const formatTokens = value => value == null ? "—" : value.toLocaleString("en-US");
1814
- const remainingLabel = compact == null ? "context remaining" : "remaining until compact";
1815
- contextMeter.style.setProperty("--context-remaining", `${percent == null ? 0 : percent}%`);
1816
- contextMeterValue.textContent = percent == null ? "—" : percent >= 100 ? "" : String(percent);
1817
- contextMeter.setAttribute("aria-label", percent == null
1818
- ? "Context usage details"
1819
- : `${percent}% ${remainingLabel}. Context usage details`);
1820
- document.getElementById("contextCurrent").textContent = formatTokens(session.usage_tokens);
1821
- document.getElementById("contextCompact").textContent = compact == null ? "Off" : formatTokens(compact);
1822
- document.getElementById("contextMax").textContent = formatTokens(session.max_context_window);
1823
- const note = compact == null
1824
- ? "Auto-compact is off. The ring shows remaining context."
1825
- : "The ring reaches 0% at the compact limit. Compaction runs before the next model request.";
1826
- document.getElementById("contextNote").textContent = session.usage_tokens == null
1827
- ? `Waiting for current usage. ${note}`
1828
- : note;
1829
- }
1830
-
1831
- function setupContextDetails() {
1832
- const control = document.getElementById("contextControl");
1833
- const details = document.getElementById("contextDetails");
1834
- let pinned = false;
1835
- const show = visible => {
1836
- details.hidden = !visible;
1837
- contextMeter.setAttribute("aria-expanded", String(visible));
1838
- };
1839
- const close = () => {
1840
- pinned = false;
1841
- show(false);
1842
- };
1843
- control.addEventListener("pointerenter", event => {
1844
- if (event.pointerType !== "touch") show(true);
1845
- });
1846
- control.addEventListener("pointerleave", () => {
1847
- if (!pinned) show(false);
1848
- });
1849
- contextMeter.addEventListener("focus", () => show(true));
1850
- control.addEventListener("focusout", event => {
1851
- if (!pinned && !control.contains(event.relatedTarget)) show(false);
1852
- });
1853
- contextMeter.addEventListener("click", () => {
1854
- pinned = !pinned;
1855
- show(pinned);
1856
- });
1857
- document.addEventListener("pointerdown", event => {
1858
- if (!control.contains(event.target)) close();
1859
- });
1860
- document.addEventListener("keydown", event => {
1861
- if (event.key === "Escape") close();
1862
- });
1863
- tabsEl.addEventListener("click", close);
1864
- newTabButton.addEventListener("click", close);
1865
- }
1866
-
1867
- function messageSignature(snapshot) {
1868
- const turns = (snapshot && snapshot.turns) || [];
1869
- return JSON.stringify([
1870
- turns.map((item) => [
1871
- item.submission_id || "",
1872
- item.turn_id || "",
1873
- item.prompt || "",
1874
- item.response || "",
1875
- item.thinking || "",
1876
- item.status || "",
1877
- item.error || "",
1878
- item.kind || "",
1879
- ]),
1880
- snapshot.input_request || null,
1881
- ]);
1882
- }
1883
-
1884
- async function pollSession() {
1885
- const requestedSessionId = activeSessionId;
1886
- if (sessionPoll) {
1887
- if (sessionPoll.sessionId === requestedSessionId) return;
1888
- sessionPoll.controller.abort();
1889
- }
1890
- const request = {sessionId: requestedSessionId, controller: new AbortController()};
1891
- sessionPoll = request;
1892
- try {
1893
- const path = requestedSessionId
1894
- ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
1895
- : "api/session";
1896
- const response = await fetch(relativeUrl(path), {signal: request.controller.signal});
1897
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1898
- const payload = await response.json();
1899
- if (sessionPoll !== request || requestedSessionId !== activeSessionId) {
1900
- return;
1901
- }
1902
- if (payload.session_id && payload.session_id !== activeSessionId) {
1903
- activeSessionId = payload.session_id;
1904
- }
1905
- updateSessions(payload.sessions || []);
1906
- renderSnapshot(payload.snapshot);
1907
- } catch (error) {
1908
- if (sessionPoll === request && requestedSessionId === activeSessionId) {
1909
- setSpinner(`poll error: ${error.message}`);
1910
- }
1911
- } finally {
1912
- if (sessionPoll === request) sessionPoll = null;
1913
- }
1914
- }
1915
-
1916
- function startPolling() {
1917
- if (pollTimer) return;
1918
- pollSession();
1919
- pollTimer = setInterval(pollSession, 2000);
1920
- }
1921
-
1922
- function stopPolling() {
1923
- if (!pollTimer) return;
1924
- clearInterval(pollTimer);
1925
- pollTimer = null;
1926
- }
1927
-
1928
- async function pollBoard() {
1929
- if (boardPollInFlight) return;
1930
- boardPollInFlight = true;
1931
- try {
1932
- const response = await fetch(relativeUrl("api/board"));
1933
- if (!response.ok) return;
1934
- const payload = await response.json();
1935
- if (!payload.exists) {
1936
- lastBoardSignature = null;
1937
- return;
1938
- }
1939
- const signature = `${payload.mtime_ns || ""}:${payload.size || ""}`;
1940
- if (lastBoardSignature === null) {
1941
- lastBoardSignature = signature;
1942
- boardFrame.src = `board?t=${Date.now()}`;
1943
- return;
1944
- }
1945
- if (signature !== lastBoardSignature) {
1946
- lastBoardSignature = signature;
1947
- boardFrame.src = `board?t=${Date.now()}`;
1948
- }
1949
- } catch (_error) {
1950
- return;
1951
- } finally {
1952
- boardPollInFlight = false;
1953
- }
1954
- }
1955
-
1956
- function startBoardPolling() {
1957
- if (boardPollTimer) return;
1958
- pollBoard();
1959
- boardPollTimer = setInterval(pollBoard, 2000);
1960
- }
1961
-
1962
- function renderQueueHint(inputs) {
1963
- queueHint.hidden = inputs.length === 0;
1964
- for (const [kind, label, badge] of [
1965
- ["steer", "[soon]", steerHint],
1966
- ["enqueue", "[on done]", enqueueHint],
1967
- ]) {
1968
- const text = inputs.filter(item => item.queue === kind).map(item => item.prompt).join("\n");
1969
- badge.hidden = !text;
1970
- badge.querySelector(".queue-preview").textContent = text;
1971
- badge.title = label + "\n" + text;
1972
- }
1973
- }
1974
-
1975
- function renderSnapshot(snapshot) {
1976
- if (!snapshot) return;
1977
- updatePromptPlaceholder(snapshot);
1978
- const state = stateForSession(activeSessionId);
1979
- state.inputRequest = snapshot.input_request || null;
1980
- renderQueueHint(snapshot.queued_inputs);
1981
- const signature = messageSignature(snapshot);
1982
- if (signature === state.lastRenderedSignature) {
1983
- setSpinner(snapshot.spinner || "");
1984
- updateContextMeter(snapshot);
1985
- return;
1986
- }
1987
- state.lastRenderedSignature = signature;
1988
- lastRenderedSignature = signature;
1989
- const shouldStick = isNearBottom();
1990
- const previousScrollTop = log.scrollTop;
1991
- suppressScrollSave = true;
1992
- try {
1993
- log.textContent = "";
1994
- const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
1995
- if (turns.length) {
1996
- turns.forEach((turn) => renderTurn(turn, shouldStick));
1997
- } else if (!state.inputRequest) {
1998
- log.appendChild(document.getElementById("emptyChat").content.cloneNode(true));
1999
- }
2000
- if (state.inputRequest) {
2001
- addEntry("input requested", state.inputRequest.text, "control", shouldStick);
2002
- }
2003
- setSpinner(snapshot.spinner || "");
2004
- updateContextMeter(snapshot);
2005
- if (restoreScrollTop !== null) {
2006
- log.scrollTop = Math.min(
2007
- restoreScrollTop,
2008
- Math.max(0, log.scrollHeight - log.clientHeight),
2009
- );
2010
- restoreScrollTop = null;
2011
- } else if (shouldStick) {
2012
- scrollToBottom();
2013
- } else {
2014
- log.scrollTop = Math.min(
2015
- previousScrollTop,
2016
- Math.max(0, log.scrollHeight - log.clientHeight),
2017
- );
2018
- }
2019
- } finally {
2020
- suppressScrollSave = false;
2021
- }
2022
- state.scrollTop = log.scrollTop;
2023
- }
2024
-
2025
- function renderTurn(turn, shouldStick) {
2026
- const isControl = turn.kind === "control";
2027
- if (turn.prompt) {
2028
- addEntry(
2029
- isControl ? "command" : "prompt",
2030
- turn.prompt || "",
2031
- isControl ? "control" : "user",
2032
- shouldStick,
2033
- );
2034
- }
2035
- if (turn.response) {
2036
- if (isControl) {
2037
- addEntry("result", turn.response, "control", shouldStick);
2038
- } else {
2039
- addMarkdownEntry("response", turn.response, "response", shouldStick);
2040
- }
2041
- }
2042
- if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
2043
- if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
2044
- }
2045
-
2046
- async function submitPrompt(text) {
2047
- const response = await fetch(relativeUrl("api/session/message"), {
2048
- method: "POST",
2049
- headers: {"Content-Type": "application/json"},
2050
- body: JSON.stringify({prompt: text, sender: "web", session_id: activeSessionId}),
2051
- });
2052
- const payload = await response.json();
2053
- if (!response.ok || !payload.ok) {
2054
- addEntry("error", payload.error || `HTTP ${response.status}`, "error");
2055
- setSpinner("");
2056
- return;
2057
- }
2058
- if (payload.snapshot) {
2059
- if (payload.sessions) updateSessions(payload.sessions);
2060
- renderSnapshot(payload.snapshot);
2061
- } else {
2062
- setSpinner("");
2063
- }
2064
- startPolling();
2065
- }
2066
-
2067
- async function switchSession(sessionId) {
2068
- if (!sessionId || sessionId === activeSessionId) return;
2069
- saveActiveSessionUiState();
2070
- activeSessionId = sessionId;
2071
- renderTabs();
2072
- updatePromptPlaceholder();
2073
- updateContextMeter();
2074
- const state = stateForSession(sessionId);
2075
- state.lastRenderedSignature = "";
2076
- lastRenderedSignature = "";
2077
- prompt.value = state.draft || "";
2078
- resizePrompt();
2079
- suppressScrollSave = true;
2080
- log.textContent = "";
2081
- renderQueueHint([]);
2082
- suppressScrollSave = false;
2083
- setSpinner("", {suppressDoneNotification: true});
2084
- restoreScrollTop = state.scrollTop;
2085
- await pollSession();
2086
- renderTabs();
2087
- }
2088
-
2089
- async function createSession() {
2090
- saveActiveSessionUiState();
2091
- const response = await fetch(relativeUrl("api/sessions"), {method: "POST"});
2092
- const payload = await response.json();
2093
- if (!response.ok || !payload.ok) {
2094
- addEntry("error", payload.error || `HTTP ${response.status}`, "error");
2095
- return;
2096
- }
2097
- activeSessionId = payload.session_id || "";
2098
- const state = stateForSession(activeSessionId);
2099
- state.draft = "";
2100
- state.scrollTop = null;
2101
- state.lastRenderedSignature = "";
2102
- lastRenderedSignature = "";
2103
- prompt.value = "";
2104
- resizePrompt();
2105
- restoreScrollTop = null;
2106
- updateSessions(payload.sessions || []);
2107
- renderSnapshot(payload.snapshot);
2108
- }
2109
-
2110
- async function closeSession(sessionId) {
2111
- if (!sessionId || sessions.length <= 1) return;
2112
- const response = await fetch(relativeUrl(`api/sessions/${encodeURIComponent(sessionId)}`), {
2113
- method: "DELETE",
2114
- });
2115
- const payload = await response.json();
2116
- if (!response.ok || !payload.ok) {
2117
- addEntry("error", payload.error || `HTTP ${response.status}`, "error");
2118
- return;
2119
- }
2120
- updateSessions(payload.sessions || []);
2121
- if (activeSessionId === sessionId) {
2122
- await switchSession(sessions[0].id);
2123
- }
2124
- sessionState.delete(sessionId);
2125
- }
2126
-
2127
- form.addEventListener("submit", (event) => {
2128
- event.preventDefault();
2129
- const text = prompt.value.trim();
2130
- if (!text && !stateForSession(activeSessionId).inputRequest) return;
2131
- prompt.value = "";
2132
- resizePrompt();
2133
- stateForSession(activeSessionId).draft = "";
2134
- submitPrompt(text);
2135
- });
2136
-
2137
- prompt.addEventListener("input", () => {
2138
- stateForSession(activeSessionId).draft = prompt.value;
2139
- resizePrompt();
2140
- });
2141
-
2142
- prompt.addEventListener("compositionstart", () => {
2143
- isComposing = true;
2144
- });
2145
-
2146
- prompt.addEventListener("compositionend", () => {
2147
- isComposing = false;
2148
- });
2149
-
2150
- log.addEventListener("scroll", () => {
2151
- if (suppressScrollSave) return;
2152
- if (!activeSessionId) return;
2153
- stateForSession(activeSessionId).scrollTop = log.scrollTop;
2154
- });
2155
-
2156
- prompt.addEventListener("keydown", (event) => {
2157
- if (event.isComposing || isComposing || event.keyCode === 229) return;
2158
- if (event.key === "Enter" && !event.shiftKey) {
2159
- event.preventDefault();
2160
- form.requestSubmit();
2161
- }
2162
- });
2163
-
2164
- splitter.addEventListener("pointerdown", startResize);
2165
- splitter.addEventListener("pointermove", updateResize);
2166
- splitter.addEventListener("pointerup", endResize);
2167
- splitter.addEventListener("pointercancel", endResize);
2168
- splitter.addEventListener("dblclick", centerSplit);
2169
- spinner.addEventListener("click", toggleSpinnerNotification);
2170
- newTabButton.addEventListener("click", createSession);
2171
- document.querySelectorAll("[data-pane]").forEach((button) => {
2172
- button.addEventListener("click", () => {
2173
- workspace.dataset.mobilePane = button.dataset.pane;
2174
- if (button.dataset.pane === "chat") resizePrompt();
2175
- });
2176
- });
2177
- window.addEventListener("resize", () => {
2178
- const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
2179
- if (current > 0) setChatWidth(current);
2180
- resizePrompt();
2181
- });
2182
- document.addEventListener("visibilitychange", () => {
2183
- if (document.visibilityState !== "visible") return;
2184
- pollSession();
2185
- pollBoard();
2186
- });
2187
-
2188
- loadChatWidth();
2189
- setupContextDetails();
2190
- resizePrompt();
2191
- startPolling();
2192
- startBoardPolling();
2193
- document.querySelectorAll(".tabs").forEach(tabs => {
2194
- tabs.addEventListener("wheel", event => {
2195
- if (event.ctrlKey || event.shiftKey || event.deltaX !== 0 || event.deltaY === 0) return;
2196
- const previous = tabs.scrollLeft;
2197
- const unit = event.deltaMode === 1 ? tabs.clientHeight : event.deltaMode === 2 ? tabs.clientWidth : 1;
2198
- tabs.scrollLeft += event.deltaY * unit;
2199
- if (tabs.scrollLeft !== previous) event.preventDefault();
2200
- }, {passive: false});
2201
- });
2202
-
2203
- const contentTabs = mountContentViews({
2204
- tabs: document.getElementById("contentTabs"),
2205
- frame: document.getElementById("boardFrame"),
2206
- content: document.getElementById("contentView"),
2207
- status: document.getElementById("viewStatus"),
2208
- storage: window.localStorage,
2209
- storageKey: "pycodex.workspace.contentTab:" + window.location.pathname,
2210
- renderMarkdown: renderMarkdownInto,
2211
- request: async (path, options = {}) => {
2212
- const response = await fetch(new URL(path, window.location.href), options);
2213
- const payload = await response.json();
2214
- if (!response.ok) throw new Error(payload.detail || "HTTP " + response.status);
2215
- return payload;
2216
- },
2217
- });
2218
- let canvasDocument = null;
2219
-
2220
- function followViewLink(event, href) {
2221
- if (event.defaultPrevented || event.button !== 0) return;
2222
- event.preventDefault();
2223
- contentTabs.open(href).catch(error => showToast("Unable to open view: " + error.message));
2224
- }
2225
-
2226
- function followMarkdownLink(event) {
2227
- const anchor = event.target.closest("a[data-workspace-link]");
2228
- if (anchor) followViewLink(event, anchor.getAttribute("data-workspace-link"));
2229
- }
2230
-
2231
- function followCanvasLink(event) {
2232
- const anchor = event.target.closest("a[href]");
2233
- if (anchor && isWorkspaceLink(anchor.getAttribute("href"), false)) {
2234
- followViewLink(event, anchor.getAttribute("href"));
2235
- }
2236
- }
2237
-
2238
- function bindCanvasLinks() {
2239
- if (canvasDocument) canvasDocument.removeEventListener("click", followCanvasLink);
2240
- canvasDocument = boardFrame.contentDocument;
2241
- if (canvasDocument) canvasDocument.addEventListener("click", followCanvasLink);
2242
- }
2243
-
2244
- function startContentViews() {
2245
- document.addEventListener("click", followMarkdownLink);
2246
- boardFrame.addEventListener("load", bindCanvasLinks);
2247
- bindCanvasLinks();
2248
- contentTabs.start();
2249
- }
2250
-
2251
- function stopContentViews() {
2252
- document.removeEventListener("click", followMarkdownLink);
2253
- boardFrame.removeEventListener("load", bindCanvasLinks);
2254
- if (canvasDocument) canvasDocument.removeEventListener("click", followCanvasLink);
2255
- canvasDocument = null;
2256
- contentTabs.stop();
2257
- }
2258
-
2259
- startContentViews();
2260
- window.addEventListener("pagehide", stopContentViews);
2261
- window.addEventListener("pageshow", startContentViews);
2262
- document.addEventListener("visibilitychange", () => {
2263
- if (document.visibilityState === "visible") contentTabs.poll();
2264
- });
2265
- </script>
2266
- </body>
2267
- </html>