python-codex 0.2.7__py3-none-any.whl → 0.3.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 (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -42,8 +42,8 @@
42
42
  --space-5: 10px;
43
43
  --space-6: 12px;
44
44
  --space-7: 14px;
45
- --radius-sm: 4px;
46
- --radius-md: 6px;
45
+ --radius-sm: 6px;
46
+ --radius-md: 10px;
47
47
  --chat-width: min(42vw, 760px);
48
48
  --splitter-width: 8px;
49
49
  }
@@ -54,8 +54,10 @@
54
54
  overflow: hidden;
55
55
  background: var(--bg);
56
56
  color: var(--ink);
57
- font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
57
+ font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
58
58
  }
59
+ button, textarea { font: inherit; }
60
+ .pane-toggle { display: none; }
59
61
  .workspace {
60
62
  display: grid;
61
63
  grid-template-columns: minmax(0, 1fr) var(--splitter-width) var(--chat-width);
@@ -75,23 +77,31 @@
75
77
  .boardbar {
76
78
  min-width: 0;
77
79
  border-bottom: 1px solid var(--line);
78
- padding: 0;
80
+ height: 44px;
81
+ padding: 0 12px;
79
82
  color: var(--muted);
80
83
  font-size: 12px;
81
84
  display: flex;
82
85
  align-items: center;
83
- gap: var(--space-5);
86
+ gap: 10px;
84
87
  white-space: nowrap;
85
88
  overflow: hidden;
86
89
  }
87
90
  .boardbar-paths {
88
91
  min-width: 0;
89
92
  display: flex;
90
- align-items: center;
91
- gap: var(--space-4);
93
+ align-items: baseline;
94
+ gap: 12px;
92
95
  overflow: hidden;
93
- line-height: 20px;
96
+ line-height: 1.4;
97
+ }
98
+ .boardbar-title {
99
+ color: var(--ink);
100
+ font-size: 14px;
101
+ font-weight: 600;
94
102
  }
103
+ .boardbar-paths code { border: 0; padding: 0; background: none; font-size: 11px; }
104
+ .board-location { margin-left: auto; font-size: 11px; overflow: hidden; text-overflow: ellipsis; }
95
105
  .boardbar-paths span {
96
106
  min-width: 0;
97
107
  overflow: hidden;
@@ -102,8 +112,8 @@
102
112
  display: inline-flex;
103
113
  align-items: center;
104
114
  justify-content: center;
105
- width: 28px;
106
- height: 20px;
115
+ width: 32px;
116
+ height: 32px;
107
117
  border: 0;
108
118
  border-radius: var(--radius-sm);
109
119
  background: transparent;
@@ -118,13 +128,10 @@
118
128
  outline: 2px solid var(--accent);
119
129
  outline-offset: 1px;
120
130
  }
121
- .workspace-back::before {
122
- content: "";
123
- width: 7px;
124
- height: 7px;
125
- border-left: 2px solid currentColor;
126
- border-bottom: 2px solid currentColor;
127
- transform: rotate(45deg);
131
+ .workspace-back svg {
132
+ display: block;
133
+ width: 18px;
134
+ height: 18px;
128
135
  }
129
136
  .board {
130
137
  border: 0;
@@ -138,9 +145,8 @@
138
145
  width: 8px;
139
146
  height: 100dvh;
140
147
  border: 0;
141
- border-left: 1px solid var(--line);
142
148
  border-right: 1px solid var(--line);
143
- background: var(--panel-hover);
149
+ background: var(--board-chrome);
144
150
  cursor: col-resize;
145
151
  padding: 0;
146
152
  touch-action: none;
@@ -150,10 +156,10 @@
150
156
  position: absolute;
151
157
  left: 50%;
152
158
  top: 50%;
153
- width: 3px;
154
- height: 42px;
159
+ width: 2px;
160
+ height: 28px;
155
161
  border-radius: 999px;
156
- background: var(--muted);
162
+ background: var(--line-strong);
157
163
  transform: translate(-50%, -50%);
158
164
  }
159
165
  .splitter:hover,
@@ -177,55 +183,107 @@
177
183
  overflow: hidden;
178
184
  display: grid;
179
185
  grid-template-rows: auto minmax(0, 1fr) auto;
180
- background: var(--panel);
186
+ background: var(--panel-raised);
181
187
  }
182
188
  .topbar {
183
189
  min-width: 0;
184
- padding: var(--space-4) var(--space-7);
190
+ height: 44px;
191
+ padding: 6px 20px;
185
192
  border-bottom: 1px solid var(--line);
186
193
  display: grid;
187
- gap: var(--space-4);
194
+ align-content: center;
188
195
  }
189
196
  .tabbar {
190
197
  display: flex;
191
198
  align-items: center;
192
- gap: var(--space-2);
199
+ gap: 8px;
193
200
  min-width: 0;
194
201
  }
202
+ .context-control {
203
+ position: relative;
204
+ display: flex;
205
+ flex: 0 0 auto;
206
+ }
195
207
  .context-meter {
196
208
  --context-remaining: 0%;
197
- --context-meter-size: 22px;
209
+ --context-meter-size: 24px;
198
210
  position: relative;
199
211
  display: inline-grid;
200
212
  place-items: center;
201
213
  width: var(--context-meter-size);
202
214
  height: var(--context-meter-size);
215
+ border: 0;
216
+ padding: 0;
203
217
  border-radius: 999px;
204
- background: conic-gradient(var(--accent) 0 var(--context-remaining), var(--line-strong) 0 100%);
205
- color: var(--muted);
218
+ background: conic-gradient(var(--accent) 0 var(--context-remaining), var(--line) 0 100%);
219
+ color: var(--ink);
206
220
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
207
- font-size: 9px;
221
+ font-size: 10px;
222
+ font-weight: 600;
208
223
  line-height: 1;
209
224
  flex: 0 0 auto;
225
+ cursor: pointer;
210
226
  }
211
- .context-meter.hidden {
212
- display: none;
227
+ .context-meter:focus-visible {
228
+ outline: 2px solid var(--accent);
229
+ outline-offset: 3px;
213
230
  }
214
231
  .context-meter::before {
215
232
  content: "";
216
233
  position: absolute;
217
- inset: 4px;
234
+ inset: 3px;
218
235
  border-radius: 999px;
219
- background: var(--panel);
236
+ background: var(--panel-raised);
220
237
  }
221
238
  .context-meter-value {
222
239
  position: relative;
223
240
  z-index: 1;
224
241
  }
242
+ .context-details {
243
+ position: absolute;
244
+ bottom: 100%;
245
+ left: -6px;
246
+ z-index: 5;
247
+ width: 264px;
248
+ max-width: calc(100vw - 48px);
249
+ padding-bottom: 10px;
250
+ color: var(--ink);
251
+ font-size: 12px;
252
+ line-height: 1.5;
253
+ }
254
+ .context-details-card {
255
+ padding: 12px 14px;
256
+ border: 1px solid var(--line);
257
+ border-radius: 10px;
258
+ background: var(--panel-raised);
259
+ box-shadow: 0 6px 24px rgba(27, 57, 72, 0.12);
260
+ }
261
+ .context-details-heading {
262
+ display: flex;
263
+ justify-content: space-between;
264
+ font-weight: 600;
265
+ }
266
+ .context-details-heading span {
267
+ color: var(--muted);
268
+ font-weight: 400;
269
+ }
270
+ .context-details dl {
271
+ display: grid;
272
+ grid-template-columns: 1fr auto;
273
+ gap: 4px 12px;
274
+ margin: 8px 0;
275
+ }
276
+ .context-details dt { color: var(--muted); }
277
+ .context-details dd {
278
+ margin: 0;
279
+ font-variant-numeric: tabular-nums;
280
+ text-align: right;
281
+ }
282
+ .context-details p { margin: 0; color: var(--muted); }
225
283
  .tabs {
226
284
  display: flex;
227
285
  align-items: center;
228
- gap: var(--space-1);
286
+ gap: 4px;
229
287
  min-width: 0;
230
288
  overflow-x: auto;
231
289
  scrollbar-width: thin;
@@ -237,25 +295,28 @@
237
295
  gap: var(--space-2);
238
296
  min-width: 88px;
239
297
  max-width: 180px;
240
- height: 31px;
241
- border: 1px solid var(--line);
242
- border-radius: var(--radius-sm);
243
- background: var(--panel-raised);
244
- color: var(--ink);
245
- padding: 0 var(--space-3) 0 var(--space-4);
246
- font: inherit;
298
+ height: 30px;
299
+ border: 0;
300
+ border-radius: var(--radius-md);
301
+ background: var(--panel-deep);
302
+ color: var(--muted);
303
+ padding: 0 8px 0 12px;
304
+ font-size: 13px;
247
305
  cursor: pointer;
248
306
  user-select: none;
249
307
  }
250
- .tab:focus-within,
251
- .tab-new:focus {
308
+ .tab:hover {
309
+ background: var(--panel-hover);
310
+ color: var(--ink);
311
+ }
312
+ .tab-new:focus-visible {
252
313
  outline: 2px solid var(--accent);
253
314
  outline-offset: 1px;
254
315
  }
255
316
  .tab.active {
256
- border-color: var(--accent);
257
317
  background: var(--accent-soft);
258
- box-shadow: inset 0 -2px 0 var(--accent);
318
+ color: var(--accent);
319
+ font-weight: 600;
259
320
  }
260
321
  .tab-status {
261
322
  position: relative;
@@ -278,6 +339,10 @@
278
339
  background: var(--accent);
279
340
  animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
280
341
  }
342
+ .tab-status[hidden],
343
+ .tab-label[hidden] {
344
+ display: none;
345
+ }
281
346
  .tab-label {
282
347
  min-width: 0;
283
348
  overflow: hidden;
@@ -285,12 +350,35 @@
285
350
  white-space: nowrap;
286
351
  flex: 1;
287
352
  text-align: left;
353
+ border: 0;
354
+ padding: 0;
355
+ background: transparent;
356
+ color: inherit;
357
+ font: inherit;
358
+ cursor: inherit;
359
+ }
360
+ .tab-label:focus-visible {
361
+ outline: 0;
362
+ text-decoration: underline;
363
+ text-underline-offset: 3px;
364
+ }
365
+ .tab-title-input {
366
+ min-width: 0;
367
+ width: 0;
368
+ flex: 1;
369
+ border: 0;
370
+ padding: 0;
371
+ outline: 0;
372
+ background: transparent;
373
+ color: inherit;
374
+ font: inherit;
375
+ user-select: text;
288
376
  }
289
377
  .tab-close,
290
378
  .tab-new {
291
379
  width: 31px;
292
380
  height: 31px;
293
- border: 1px solid var(--line);
381
+ border: 0;
294
382
  border-radius: var(--radius-md);
295
383
  background: var(--panel-raised);
296
384
  color: var(--muted);
@@ -300,6 +388,8 @@
300
388
  flex: 0 0 auto;
301
389
  }
302
390
  .tab-close {
391
+ display: grid;
392
+ place-items: center;
303
393
  width: 18px;
304
394
  height: 18px;
305
395
  border: 0;
@@ -307,14 +397,23 @@
307
397
  opacity: 0;
308
398
  visibility: hidden;
309
399
  pointer-events: none;
310
- font-size: 17px;
311
- font-weight: 500;
312
- line-height: 18px;
400
+ }
401
+ .tab-close svg {
402
+ display: block;
403
+ width: 12px;
404
+ height: 12px;
313
405
  }
314
406
  .tab-new {
315
- font-size: 22px;
316
- font-weight: 400;
317
- line-height: 31px;
407
+ display: grid;
408
+ place-items: center;
409
+ width: 30px;
410
+ height: 30px;
411
+ background: var(--panel-deep);
412
+ }
413
+ .tab-new svg {
414
+ display: block;
415
+ width: 18px;
416
+ height: 18px;
318
417
  }
319
418
  .tab.active .tab-close {
320
419
  opacity: 0.85;
@@ -334,52 +433,70 @@
334
433
  min-height: 0;
335
434
  overflow-y: auto;
336
435
  overflow-x: hidden;
337
- padding: var(--space-6) var(--space-7);
436
+ padding: 16px 20px 20px;
338
437
  display: flex;
339
438
  flex-direction: column;
340
- gap: var(--space-4);
341
- background: var(--bg);
439
+ gap: 14px;
440
+ background: var(--panel-raised);
441
+ scrollbar-width: thin;
442
+ scrollbar-color: var(--line) transparent;
342
443
  }
343
444
  .entry {
344
445
  min-width: 0;
345
- border: 1px solid var(--line);
346
- border-radius: var(--radius-sm);
347
- padding: var(--space-4) var(--space-5);
348
- background: var(--panel-raised);
349
- box-shadow: var(--entry-shadow);
446
+ flex: 0 0 auto;
447
+ line-height: 1.6;
448
+ }
449
+ .entry-label {
450
+ margin-bottom: 8px;
451
+ font-size: 12px;
452
+ font-weight: 600;
453
+ color: var(--muted);
350
454
  }
351
455
  .entry.user {
352
- border-color: var(--warn);
353
- background: var(--warn-soft);
354
- color: var(--warn-ink);
456
+ align-self: flex-end;
457
+ max-width: 92%;
458
+ padding: 7px 12px;
459
+ border-radius: 10px 10px 3px 10px;
460
+ background: var(--success-soft);
355
461
  }
356
462
  .entry.response,
357
463
  .entry.assistant {
358
- border-color: var(--line);
359
- background: var(--panel-raised);
464
+ padding: 0;
465
+ }
466
+ .entry.thinking,
467
+ .entry.control,
468
+ .entry.tool {
469
+ border-left: 2px solid var(--line);
470
+ padding: 2px 0 2px 10px;
471
+ color: var(--muted);
472
+ font-size: 13px;
360
473
  }
361
474
  .entry.thinking {
362
- border-color: var(--success-line);
363
- background: var(--success-soft);
364
- color: var(--success);
365
- animation: thinking-breathe 4.8s ease-in-out infinite;
475
+ border-left-color: var(--accent);
476
+ animation: activity-breathe 1.6s ease-in-out infinite;
366
477
  }
367
478
  .entry.control {
368
- border-color: #b7d3ee;
369
- background: var(--info-soft);
370
- color: var(--info);
479
+ white-space: pre-wrap;
480
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
371
481
  }
372
482
  .entry.tool {
373
- border-color: #d8cdf1;
374
- background: var(--violet-soft);
375
- color: var(--violet);
376
483
  font-size: 12px;
377
484
  }
378
485
  .entry.error {
379
- border-color: #efb8c1;
486
+ border-left: 2px solid var(--danger);
487
+ border-radius: var(--radius-sm);
488
+ padding: 12px 16px;
380
489
  background: var(--danger-soft);
381
490
  color: var(--danger);
382
491
  }
492
+ .empty-chat {
493
+ margin: auto 0;
494
+ padding: 36px 0 56px;
495
+ max-width: 360px;
496
+ }
497
+ .empty-chat .entry-label { color: var(--accent); }
498
+ .empty-chat h1 { margin: 12px 0; font-size: 28px; line-height: 1.25; font-weight: 600; letter-spacing: -0.6px; }
499
+ .empty-chat p { margin: 0; color: var(--muted); line-height: 1.75; }
383
500
  .text {
384
501
  white-space: pre-wrap;
385
502
  overflow-wrap: anywhere;
@@ -389,7 +506,7 @@
389
506
  overflow-wrap: anywhere;
390
507
  }
391
508
  .markdown p {
392
- margin: 0 0 var(--space-4);
509
+ margin: 0 0 8px;
393
510
  }
394
511
  .markdown p:last-child,
395
512
  .markdown ul:last-child,
@@ -400,17 +517,17 @@
400
517
  }
401
518
  .markdown ul,
402
519
  .markdown ol {
403
- margin: 0 0 var(--space-4) 18px;
520
+ margin: 0 0 8px 18px;
404
521
  padding: 0;
405
522
  }
406
523
  .markdown li {
407
524
  margin: 2px 0;
408
525
  }
409
526
  .markdown pre {
410
- margin: 0 0 var(--space-4);
411
- padding: var(--space-4) var(--space-5);
412
- border: 1px solid var(--line);
413
- border-radius: var(--radius-sm);
527
+ margin: 0 0 10px;
528
+ padding: 10px 12px;
529
+ border: 0;
530
+ border-radius: var(--radius-md);
414
531
  background: var(--code);
415
532
  overflow-x: auto;
416
533
  white-space: pre;
@@ -445,6 +562,13 @@
445
562
  .markdown a:hover {
446
563
  text-decoration: underline;
447
564
  }
565
+ .markdown h1, .markdown h2, .markdown h3 { line-height: 1.4; margin: 16px 0 8px; font-weight: 600; }
566
+ .markdown h1 { font-size: 20px; }
567
+ .markdown h2 { font-size: 18px; }
568
+ .markdown h3 { font-size: 16px; }
569
+ .markdown > :first-child { margin-top: 0; }
570
+ .markdown blockquote { margin: 16px 0; padding-left: 16px; border-left: 2px solid var(--line); color: var(--muted); }
571
+ .markdown img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }
448
572
  .markdown .katex-display {
449
573
  margin: 0 0 var(--space-4);
450
574
  overflow-x: auto;
@@ -464,32 +588,175 @@
464
588
  .markdown .katex-display:last-child {
465
589
  margin-bottom: 0;
466
590
  }
591
+ .markdown .mermaid-figure {
592
+ margin: 0 0 var(--space-4);
593
+ }
594
+ .markdown .mermaid-diagram {
595
+ overflow-x: auto;
596
+ padding: 8px 0;
597
+ color: var(--muted);
598
+ font-size: 13px;
599
+ }
600
+ .markdown .mermaid-diagram svg {
601
+ display: block;
602
+ height: auto;
603
+ }
604
+ .markdown .mermaid-source {
605
+ flex: 1;
606
+ min-width: 0;
607
+ color: var(--muted);
608
+ font-size: 12px;
609
+ }
610
+ .markdown .mermaid-source summary {
611
+ width: fit-content;
612
+ cursor: pointer;
613
+ }
614
+ .markdown .mermaid-source pre {
615
+ margin-top: 8px;
616
+ }
617
+ .mermaid-controls,
618
+ .mermaid-viewer-header {
619
+ display: flex;
620
+ align-items: flex-start;
621
+ gap: 12px;
622
+ }
623
+ .mermaid-controls button,
624
+ .mermaid-viewer button {
625
+ flex: 0 0 auto;
626
+ padding: 1px 5px;
627
+ border: 0;
628
+ border-radius: 4px;
629
+ background: transparent;
630
+ color: var(--muted);
631
+ font: inherit;
632
+ font-size: 12px;
633
+ cursor: pointer;
634
+ }
635
+ .mermaid-controls button:hover,
636
+ .mermaid-viewer button:hover {
637
+ background: var(--accent-soft);
638
+ color: var(--ink);
639
+ }
640
+ .mermaid-viewer {
641
+ width: min(1400px, calc(100vw - 32px));
642
+ max-height: calc(100dvh - 32px);
643
+ padding: 16px;
644
+ border: 1px solid var(--line);
645
+ border-radius: var(--radius-md);
646
+ background: var(--panel);
647
+ color: var(--ink);
648
+ }
649
+ .mermaid-viewer::backdrop {
650
+ background: rgba(23, 38, 48, 0.35);
651
+ }
652
+ .mermaid-viewer-header {
653
+ margin-bottom: 12px;
654
+ }
655
+ .mermaid-viewer-header strong {
656
+ flex: 1;
657
+ font-size: 14px;
658
+ }
659
+ .mermaid-viewer-content {
660
+ max-height: calc(100dvh - 110px);
661
+ overflow: auto;
662
+ }
663
+ .mermaid-viewer-content img {
664
+ display: block;
665
+ max-width: 100%;
666
+ height: auto;
667
+ margin: auto;
668
+ }
669
+ .mermaid-viewer-content.actual-size img {
670
+ max-width: none;
671
+ }
467
672
  .composer {
468
673
  position: relative;
469
674
  min-width: 0;
470
- padding: var(--space-4);
675
+ padding: 8px 20px 10px;
471
676
  display: grid;
472
- gap: var(--space-3);
677
+ gap: 6px;
473
678
  align-content: end;
474
- background: var(--panel);
679
+ background: var(--panel-raised);
680
+ }
681
+ .composer-surface {
682
+ border: 1px solid var(--line-strong);
683
+ border-radius: 10px;
684
+ background: var(--panel-raised);
685
+ box-shadow: 0 4px 16px rgba(27, 57, 72, 0.04);
686
+ transition: border-color 120ms, box-shadow 120ms;
687
+ }
688
+ .composer-surface:focus-within {
689
+ border-color: var(--accent);
690
+ box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.08);
691
+ }
692
+ .composer-box {
693
+ min-width: 0;
694
+ padding: 4px 10px 4px 6px;
695
+ display: flex;
696
+ align-items: center;
697
+ gap: 10px;
698
+ }
699
+ .queue-hint {
700
+ display: flex;
701
+ align-items: center;
702
+ gap: var(--space-2);
703
+ min-width: 0;
704
+ max-width: 65%;
705
+ }
706
+ .queue-hint[hidden],
707
+ .queue-indicator[hidden] {
708
+ display: none;
709
+ }
710
+ .queue-indicator {
711
+ display: inline-flex;
712
+ align-items: center;
713
+ gap: var(--space-2);
714
+ min-width: 0;
715
+ max-width: 160px;
716
+ height: 22px;
717
+ padding: 0 7px;
718
+ border: 0;
719
+ border-radius: var(--radius-sm);
720
+ font-size: 12px;
721
+ color: var(--muted);
722
+ background: var(--panel-deep);
723
+ }
724
+ .queue-symbol {
725
+ flex-shrink: 0;
726
+ font-size: 12px;
727
+ line-height: 1;
728
+ }
729
+ .queue-symbol svg {
730
+ display: block;
731
+ width: 14px;
732
+ height: 14px;
733
+ }
734
+ .queue-preview {
735
+ min-width: 0;
736
+ overflow: hidden;
737
+ text-overflow: ellipsis;
738
+ white-space: nowrap;
475
739
  }
476
740
  textarea {
477
741
  min-width: 0;
478
- resize: vertical;
742
+ resize: none;
479
743
  max-height: 35vh;
480
744
  width: 100%;
481
- border: 1px solid var(--line);
482
- border-radius: var(--radius-sm);
483
- padding: var(--space-4) var(--space-5);
484
- background: var(--panel-deep);
745
+ min-height: 28px;
746
+ border: 0;
747
+ padding: 3px 0;
748
+ display: block;
749
+ background: transparent;
485
750
  color: var(--ink);
486
751
  font: inherit;
487
- line-height: 1.4;
752
+ line-height: 22px;
488
753
  overflow-y: auto;
754
+ scrollbar-width: none;
755
+ }
756
+ textarea::-webkit-scrollbar {
757
+ display: none;
489
758
  }
490
759
  textarea:focus {
491
- border-color: var(--accent);
492
- box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.18);
493
760
  outline: 0;
494
761
  }
495
762
  .actions {
@@ -499,11 +766,15 @@
499
766
  }
500
767
  .spinner-wrap {
501
768
  position: relative;
502
- display: inline-flex;
769
+ display: flex;
503
770
  align-items: center;
504
- justify-self: start;
505
- align-self: end;
771
+ gap: var(--space-3);
506
772
  max-width: 100%;
773
+ min-width: 0;
774
+ padding: 4px 6px;
775
+ border-bottom: 1px solid var(--line);
776
+ border-radius: 9px 9px 0 0;
777
+ background: var(--panel);
507
778
  }
508
779
  .prompt-row {
509
780
  display: flex;
@@ -512,21 +783,21 @@
512
783
  min-width: 0;
513
784
  }
514
785
  .spinner {
515
- --spinner-pill-height: 22px;
516
786
  position: relative;
517
787
  display: inline-flex;
518
788
  align-items: center;
519
789
  max-width: 100%;
520
- height: var(--spinner-pill-height);
790
+ min-width: 0;
791
+ height: 22px;
521
792
  color: var(--muted);
522
793
  font-size: 12px;
523
794
  white-space: nowrap;
524
795
  overflow: hidden;
525
796
  text-overflow: ellipsis;
526
797
  padding: var(--space-1) var(--space-4) var(--space-1) 18px;
527
- border-radius: 999px;
528
- background: rgba(230, 246, 248, 0.96);
529
- border: 1px solid rgba(8, 127, 154, 0.35);
798
+ border-radius: var(--radius-sm);
799
+ background: transparent;
800
+ border: 1px solid transparent;
530
801
  font: inherit;
531
802
  font-size: 12px;
532
803
  cursor: pointer;
@@ -534,16 +805,15 @@
534
805
  .spinner:empty {
535
806
  display: none;
536
807
  }
537
- .spinner-wrap:not(:has(.spinner:not(:empty))) {
808
+ .spinner:hover {
809
+ background: var(--panel-deep);
810
+ }
811
+ .spinner-wrap:not(:has(.spinner:not(:empty), .queue-hint:not([hidden]))) {
538
812
  display: none;
539
813
  }
540
814
  .spinner:focus-visible {
541
- border-color: var(--accent);
542
- box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.18);
543
- outline: 0;
544
- }
545
- .spinner-wrap:has(.spinner:not(:empty)) {
546
- margin-bottom: var(--space-1);
815
+ outline: 2px solid var(--accent);
816
+ outline-offset: 1px;
547
817
  }
548
818
  .spinner:not(:empty)::before {
549
819
  content: "";
@@ -558,12 +828,8 @@
558
828
  animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
559
829
  }
560
830
  .spinner.notify-enabled {
561
- color: var(--success);
562
- background: var(--success-soft);
563
- border-color: var(--success-line);
564
- box-shadow:
565
- inset 0 1px 2px rgba(27, 57, 72, 0.08),
566
- inset 0 0 0 1px rgba(36, 122, 82, 0.1);
831
+ color: var(--accent);
832
+ background: var(--accent-soft);
567
833
  }
568
834
  .toast {
569
835
  position: absolute;
@@ -599,17 +865,9 @@
599
865
  transform: scale(1.08);
600
866
  }
601
867
  }
602
- @keyframes thinking-breathe {
603
- 0%, 100% {
604
- border-color: var(--success-line);
605
- box-shadow: var(--entry-shadow);
606
- }
607
- 50% {
608
- border-color: var(--success);
609
- box-shadow:
610
- 0 0 0 2px rgba(36, 122, 82, 0.14),
611
- 0 2px 7px rgba(27, 57, 72, 0.08);
612
- }
868
+ @keyframes activity-breathe {
869
+ 0%, 100% { border-left-color: var(--line); }
870
+ 50% { border-left-color: var(--accent); }
613
871
  }
614
872
  @media (prefers-reduced-motion: reduce) {
615
873
  .entry.thinking,
@@ -631,21 +889,61 @@
631
889
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
632
890
  font-size: 12px;
633
891
  }
634
- @media (max-width: 900px) {
635
- body { overflow: auto; height: auto; }
636
- .workspace { min-width: 0; }
892
+ @media (max-width: 760px) {
893
+ .pane-toggle {
894
+ display: inline-grid;
895
+ place-items: center;
896
+ width: 30px;
897
+ height: 30px;
898
+ margin-left: auto;
899
+ padding: 0;
900
+ flex: 0 0 auto;
901
+ border: 0;
902
+ border-radius: var(--radius-sm);
903
+ background: transparent;
904
+ color: var(--muted);
905
+ cursor: pointer;
906
+ }
907
+ .pane-toggle:hover { background: var(--panel-deep); color: var(--ink); }
908
+ .pane-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
909
+ .pane-toggle svg { width: 18px; height: 18px; }
910
+ .workspace { height: 100dvh; display: block; }
911
+ .board-pane, .chat { height: 100%; }
912
+ .splitter { display: none; }
913
+ .workspace[data-mobile-pane="chat"] .board-pane,
914
+ .workspace[data-mobile-pane="board"] .chat { display: none; }
915
+ .topbar { height: 44px; padding: 6px 16px; }
916
+ .boardbar { padding: 0 16px; }
917
+ .board-location { display: none; }
918
+ .log { padding: 14px 16px; gap: 14px; }
919
+ .composer { padding: 8px 16px max(10px, env(safe-area-inset-bottom)); }
920
+ textarea { font-size: 16px; line-height: 24px; padding: 2px 0; }
921
+ .tab-title-input { font-size: 16px; }
922
+ .empty-chat h1 { font-size: 26px; }
637
923
  }
638
924
  </style>
639
925
  </head>
640
926
  <body>
641
- <div class="workspace">
927
+ <div class="workspace" data-mobile-pane="chat">
642
928
  <section class="board-pane" aria-label="workspace board">
643
929
  <div class="boardbar">
644
- <a class="workspace-back" href="/" title="Back to workspaces" aria-label="Back to workspaces"></a>
930
+ <a class="workspace-back" href="/" title="Back to workspaces" aria-label="Back to workspaces">
931
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
932
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
933
+ <path d="M15 6l-6 6 6 6"/>
934
+ </svg>
935
+ </a>
645
936
  <div class="boardbar-paths">
646
- <span>@<code>__CWD_LABEL__</code></span>
647
- <span>Board: <code>__BOARD_LABEL__</code></span>
937
+ <span class="boardbar-title">__WORKSPACE_TITLE__</span>
938
+ <span title="__CWD_LABEL__"><code>__CWD_LABEL__</code></span>
648
939
  </div>
940
+ <span class="board-location" title="__BOARD_LABEL__">Board</span>
941
+ <button class="pane-toggle" type="button" data-pane="chat" title="Show chat" aria-label="Show chat">
942
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
943
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
944
+ <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"/>
945
+ </svg>
946
+ </button>
649
947
  </div>
650
948
  <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
651
949
  </section>
@@ -654,24 +952,75 @@
654
952
  <div class="topbar">
655
953
  <div class="tabbar">
656
954
  <div id="tabs" class="tabs" role="tablist"></div>
657
- <button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">+</button>
955
+ <button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">
956
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
957
+ stroke-linecap="round" aria-hidden="true">
958
+ <path d="M12 5v14M5 12h14"/>
959
+ </svg>
960
+ </button>
961
+ <button class="pane-toggle" type="button" data-pane="board" title="Show board" aria-label="Show board">
962
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
963
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
964
+ <rect x="3" y="3" width="18" height="18" rx="3"/>
965
+ <path d="M3 9h18M9 9v12"/>
966
+ </svg>
967
+ </button>
658
968
  </div>
659
969
  </div>
660
- <div id="log" class="log"></div>
970
+ <div id="log" class="log" aria-label="Conversation"></div>
661
971
  <form id="composer" class="composer">
662
- <div class="spinner-wrap">
663
- <div id="toast" class="toast" role="status" aria-live="polite"></div>
664
- <button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
665
- </div>
666
- <div class="prompt-row">
667
- <div id="contextMeter" class="context-meter hidden" aria-hidden="true">
668
- <span id="contextMeterValue" class="context-meter-value"></span>
972
+ <div class="composer-surface">
973
+ <div class="spinner-wrap">
974
+ <div id="toast" class="toast" role="status" aria-live="polite"></div>
975
+ <div id="queueHint" class="queue-hint" role="status" aria-live="polite" hidden>
976
+ <span id="steerHint" class="queue-indicator" hidden>
977
+ <span class="queue-symbol" role="img" aria-label="Steer">↑</span>
978
+ <span class="queue-preview"></span>
979
+ </span>
980
+ <span id="enqueueHint" class="queue-indicator enqueue" hidden>
981
+ <span class="queue-symbol" role="img" aria-label="Queue">
982
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"
983
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
984
+ <circle cx="12" cy="12" r="9"/>
985
+ <path d="M12 7v5l3 2"/>
986
+ </svg>
987
+ </span>
988
+ <span class="queue-preview"></span>
989
+ </span>
990
+ </div>
991
+ <button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
992
+ </div>
993
+ <div class="composer-box">
994
+ <div id="contextControl" class="context-control">
995
+ <button id="contextMeter" class="context-meter" type="button" aria-label="Context usage"
996
+ aria-expanded="false" aria-controls="contextDetails">
997
+ <span id="contextMeterValue" class="context-meter-value">—</span>
998
+ </button>
999
+ <div id="contextDetails" class="context-details" role="region" aria-label="Context usage" hidden>
1000
+ <div class="context-details-card">
1001
+ <div class="context-details-heading">Context usage <span>tokens</span></div>
1002
+ <dl>
1003
+ <dt>Current</dt><dd id="contextCurrent">—</dd>
1004
+ <dt>Compact limit</dt><dd id="contextCompact">—</dd>
1005
+ <dt>Max length</dt><dd id="contextMax">—</dd>
1006
+ </dl>
1007
+ <p id="contextNote"></p>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+ <textarea id="prompt" rows="1" aria-label="Message" title="Enter to send · Shift + Enter for a new line" placeholder="Message…"></textarea>
669
1012
  </div>
670
- <textarea id="prompt" rows="1" placeholder="Ask pycodex or type /help..."></textarea>
671
1013
  </div>
672
1014
  </form>
673
1015
  </section>
674
1016
  </div>
1017
+ <template id="emptyChat">
1018
+ <div class="empty-chat">
1019
+ <div class="entry-label">pycodex</div>
1020
+ <h1>What are we working on?</h1>
1021
+ <p>Explore an idea, work through a problem, or build something together. Your conversation starts here.</p>
1022
+ </div>
1023
+ </template>
675
1024
  <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
676
1025
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
677
1026
  <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
@@ -679,6 +1028,9 @@
679
1028
  <script>
680
1029
  const log = document.getElementById("log");
681
1030
  const spinner = document.getElementById("spinner");
1031
+ const queueHint = document.getElementById("queueHint");
1032
+ const steerHint = document.getElementById("steerHint");
1033
+ const enqueueHint = document.getElementById("enqueueHint");
682
1034
  const toast = document.getElementById("toast");
683
1035
  const prompt = document.getElementById("prompt");
684
1036
  const form = document.getElementById("composer");
@@ -691,7 +1043,7 @@
691
1043
  const newTabButton = document.getElementById("newTab");
692
1044
  let pollTimer = null;
693
1045
  let boardPollTimer = null;
694
- let pollInFlight = false;
1046
+ let sessionPoll = null;
695
1047
  let boardPollInFlight = false;
696
1048
  let lastBoardSignature = null;
697
1049
  let lastRenderedSignature = "";
@@ -715,6 +1067,40 @@
715
1067
  ];
716
1068
  const mathTokenPrefix = "\uE000math";
717
1069
  const mathTokenSuffix = "\uE001";
1070
+ const renderMermaidSvg = (() => {
1071
+ let rendererPromise = null;
1072
+ let diagramId = 0;
1073
+ return async (source, figure) => {
1074
+ if (!rendererPromise) {
1075
+ rendererPromise = import(
1076
+ "https://cdn.jsdelivr.net/npm/mermaid@11.12.0/dist/mermaid.esm.min.mjs"
1077
+ ).then(({default: renderer}) => {
1078
+ renderer.initialize({
1079
+ startOnLoad: false,
1080
+ securityLevel: "strict",
1081
+ suppressErrorRendering: true,
1082
+ htmlLabels: false,
1083
+ flowchart: {htmlLabels: false},
1084
+ theme: "neutral",
1085
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1086
+ });
1087
+ return renderer;
1088
+ });
1089
+ }
1090
+ const renderer = await rendererPromise;
1091
+ if (!figure.isConnected) return null;
1092
+ const measure = document.createElement("div");
1093
+ measure.style.cssText =
1094
+ "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none";
1095
+ document.body.appendChild(measure);
1096
+ try {
1097
+ diagramId += 1;
1098
+ return await renderer.render("workspace-diagram-" + diagramId, source, measure);
1099
+ } finally {
1100
+ measure.remove();
1101
+ }
1102
+ };
1103
+ })();
718
1104
 
719
1105
  function relativeUrl(path) {
720
1106
  return new URL(path, window.location.href).toString();
@@ -739,9 +1125,9 @@
739
1125
  entry.className = "entry " + (cls || role);
740
1126
  const body = document.createElement("div");
741
1127
  body.className = "text markdown";
742
- renderMarkdownInto(body, text || "");
743
1128
  entry.appendChild(body);
744
1129
  log.appendChild(entry);
1130
+ renderMarkdownInto(body, text || "");
745
1131
  if (stickToBottom) scrollToBottom();
746
1132
  return body;
747
1133
  }
@@ -762,6 +1148,102 @@
762
1148
  anchor.target = "_blank";
763
1149
  anchor.rel = "noopener noreferrer";
764
1150
  });
1151
+ renderMermaidInto(root);
1152
+ }
1153
+
1154
+ async function renderMermaidInto(root) {
1155
+ for (const code of root.querySelectorAll("pre > code.language-mermaid")) {
1156
+ if (!code.isConnected) return;
1157
+ const source = code.textContent;
1158
+ const pre = code.parentElement;
1159
+ const figure = document.createElement("figure");
1160
+ figure.className = "mermaid-figure";
1161
+ const diagram = document.createElement("div");
1162
+ diagram.className = "mermaid-diagram";
1163
+ diagram.textContent = "Rendering diagram…";
1164
+ const details = document.createElement("details");
1165
+ details.className = "mermaid-source";
1166
+ details.open = true;
1167
+ const summary = document.createElement("summary");
1168
+ summary.textContent = "Diagram source";
1169
+ details.appendChild(summary);
1170
+ pre.replaceWith(figure);
1171
+ details.appendChild(pre);
1172
+ const controls = document.createElement("div");
1173
+ controls.className = "mermaid-controls";
1174
+ controls.appendChild(details);
1175
+ figure.append(diagram, controls);
1176
+ try {
1177
+ const result = await renderMermaidSvg(source, figure);
1178
+ if (!result || !figure.isConnected) return;
1179
+ const stickToBottom = isNearBottom();
1180
+ diagram.innerHTML = window.DOMPurify.sanitize(result.svg, {
1181
+ USE_PROFILES: {svg: true, svgFilters: true},
1182
+ });
1183
+ const svg = diagram.querySelector("svg");
1184
+ svg.style.width = svg.viewBox.baseVal.width + "px";
1185
+ svg.style.maxWidth = "100%";
1186
+ const expand = document.createElement("button");
1187
+ expand.type = "button";
1188
+ expand.textContent = "Expand";
1189
+ expand.addEventListener("click", () => showMermaidDiagram(svg));
1190
+ controls.appendChild(expand);
1191
+ details.open = false;
1192
+ if (stickToBottom) scrollToBottom();
1193
+ } catch (error) {
1194
+ if (!figure.isConnected) return;
1195
+ diagram.textContent = "Diagram could not be rendered. Source is shown below.";
1196
+ details.open = true;
1197
+ console.warn("Mermaid diagram rendering failed", error);
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ function showMermaidDiagram(svg) {
1203
+ const copy = svg.cloneNode(true);
1204
+ copy.style.removeProperty("width");
1205
+ copy.style.removeProperty("max-width");
1206
+ copy.setAttribute("width", svg.viewBox.baseVal.width);
1207
+ copy.setAttribute("height", svg.viewBox.baseVal.height);
1208
+ const imageUrl = URL.createObjectURL(new Blob(
1209
+ [new XMLSerializer().serializeToString(copy)],
1210
+ {type: "image/svg+xml"},
1211
+ ));
1212
+ const dialog = document.createElement("dialog");
1213
+ dialog.className = "mermaid-viewer";
1214
+ dialog.setAttribute("aria-label", "Expanded diagram");
1215
+ const header = document.createElement("div");
1216
+ header.className = "mermaid-viewer-header";
1217
+ const title = document.createElement("strong");
1218
+ title.textContent = "Diagram";
1219
+ const zoom = document.createElement("button");
1220
+ zoom.type = "button";
1221
+ zoom.textContent = "Actual size";
1222
+ const close = document.createElement("button");
1223
+ close.type = "button";
1224
+ close.textContent = "Close";
1225
+ const content = document.createElement("div");
1226
+ content.className = "mermaid-viewer-content";
1227
+ const image = document.createElement("img");
1228
+ image.src = imageUrl;
1229
+ image.alt = "Diagram";
1230
+ content.appendChild(image);
1231
+ header.append(title, zoom, close);
1232
+ dialog.append(header, content);
1233
+ zoom.addEventListener("click", () => {
1234
+ const actualSize = content.classList.toggle("actual-size");
1235
+ zoom.textContent = actualSize ? "Fit width" : "Actual size";
1236
+ });
1237
+ close.addEventListener("click", () => dialog.close());
1238
+ dialog.addEventListener("click", event => {
1239
+ if (event.target === dialog) dialog.close();
1240
+ });
1241
+ dialog.addEventListener("close", () => {
1242
+ URL.revokeObjectURL(imageUrl);
1243
+ dialog.remove();
1244
+ }, {once: true});
1245
+ document.body.appendChild(dialog);
1246
+ dialog.showModal();
765
1247
  }
766
1248
 
767
1249
  function isEscapedAt(source, index) {
@@ -1168,6 +1650,7 @@
1168
1650
  draft: "",
1169
1651
  notifyWhenDone: false,
1170
1652
  wasRunning: false,
1653
+ inputRequest: null,
1171
1654
  });
1172
1655
  }
1173
1656
  return sessionState.get(key);
@@ -1203,88 +1686,201 @@
1203
1686
  activeSessionId = sessions[0].id || "";
1204
1687
  }
1205
1688
  renderTabs();
1689
+ updatePromptPlaceholder();
1206
1690
  updateContextMeter();
1207
1691
  updateSpinnerNotifyUi();
1208
1692
  }
1209
1693
 
1210
1694
  function renderTabs() {
1211
- tabsEl.textContent = "";
1695
+ const existingTabs = new Map(
1696
+ Array.from(tabsEl.children, tab => [tab.dataset.sessionId, tab])
1697
+ );
1212
1698
  sessions.forEach((session, index) => {
1213
1699
  const sessionId = session.id || "";
1214
- const tab = document.createElement("div");
1700
+ let tab = existingTabs.get(sessionId);
1701
+ if (!tab) {
1702
+ tab = document.createElement("div");
1703
+ tab.dataset.sessionId = sessionId;
1704
+ tab.setAttribute("role", "tab");
1705
+ tab.addEventListener("click", () => switchSession(sessionId));
1706
+
1707
+ const status = document.createElement("span");
1708
+ status.className = "tab-status";
1709
+ status.setAttribute("aria-hidden", "true");
1710
+ tab.appendChild(status);
1711
+
1712
+ const label = document.createElement("button");
1713
+ label.type = "button";
1714
+ label.className = "tab-label";
1715
+ label.addEventListener("click", event => {
1716
+ if (sessionId !== activeSessionId) return;
1717
+ event.stopPropagation();
1718
+ editSessionTitle(tab, sessionId);
1719
+ });
1720
+ tab.appendChild(label);
1721
+
1722
+ const close = document.createElement("button");
1723
+ close.type = "button";
1724
+ close.className = "tab-close";
1725
+ close.title = "Close session";
1726
+ close.setAttribute("aria-label", "Close session");
1727
+ close.innerHTML = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" ' +
1728
+ 'stroke-width="1.5" stroke-linecap="round" aria-hidden="true">' +
1729
+ '<path d="M4 4l8 8M12 4l-8 8"/></svg>';
1730
+ close.addEventListener("click", event => {
1731
+ event.stopPropagation();
1732
+ closeSession(sessionId);
1733
+ });
1734
+ tab.appendChild(close);
1735
+ }
1736
+ existingTabs.delete(sessionId);
1215
1737
  tab.className =
1216
1738
  "tab" +
1217
1739
  (sessionId === activeSessionId ? " active" : "") +
1218
1740
  (session.running ? " running" : "");
1219
1741
  tab.title = session.title || `Session ${index + 1}`;
1220
- tab.setAttribute("role", "tab");
1221
1742
  tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
1222
- if (session.running) tab.setAttribute("aria-busy", "true");
1223
- tab.addEventListener("click", () => switchSession(sessionId));
1224
-
1225
- if (session.running) {
1226
- const status = document.createElement("span");
1227
- status.className = "tab-status";
1228
- status.setAttribute("aria-hidden", "true");
1229
- tab.appendChild(status);
1743
+ tab.setAttribute("aria-busy", String(Boolean(session.running)));
1744
+ tab.querySelector(".tab-status").hidden = !session.running;
1745
+ const label = tab.querySelector(".tab-label");
1746
+ label.textContent = session.title || `Session ${index + 1}`;
1747
+ label.title = sessionId === activeSessionId ? "Click to rename session" : tab.title;
1748
+ tab.querySelector(".tab-close").disabled = sessions.length <= 1;
1749
+ if (tabsEl.children[index] !== tab) {
1750
+ tabsEl.insertBefore(tab, tabsEl.children[index] || null);
1230
1751
  }
1752
+ });
1753
+ existingTabs.forEach(tab => tab.remove());
1754
+ }
1231
1755
 
1232
- const label = document.createElement("span");
1233
- label.className = "tab-label";
1234
- label.textContent = session.title || `Session ${index + 1}`;
1235
- tab.appendChild(label);
1236
-
1237
- const close = document.createElement("button");
1238
- close.type = "button";
1239
- close.className = "tab-close";
1240
- close.title = "Close session";
1241
- close.setAttribute("aria-label", "Close session");
1242
- close.textContent = "×";
1243
- close.disabled = sessions.length <= 1;
1244
- close.addEventListener("click", (event) => {
1245
- event.stopPropagation();
1246
- closeSession(sessionId);
1247
- });
1248
- tab.appendChild(close);
1249
- tabsEl.appendChild(tab);
1756
+ function editSessionTitle(tab, sessionId) {
1757
+ const session = sessionById(sessionId);
1758
+ const label = tab.querySelector(".tab-label");
1759
+ const input = document.createElement("input");
1760
+ input.type = "text";
1761
+ input.className = "tab-title-input";
1762
+ input.setAttribute("aria-label", "Session title");
1763
+ input.value = session.title || "";
1764
+ input.placeholder = label.textContent;
1765
+ tab.style.width = `${tab.getBoundingClientRect().width}px`;
1766
+ label.hidden = true;
1767
+ tab.insertBefore(input, label);
1768
+ let finished = false;
1769
+ const finish = save => {
1770
+ if (finished) return;
1771
+ finished = true;
1772
+ const title = input.value.trim();
1773
+ input.remove();
1774
+ label.hidden = false;
1775
+ tab.style.width = "";
1776
+ if (save && title && title !== session.title) {
1777
+ renameSession(sessionId, title);
1778
+ }
1779
+ };
1780
+ input.addEventListener("click", event => event.stopPropagation());
1781
+ input.addEventListener("keydown", event => {
1782
+ if (event.isComposing) return;
1783
+ if (event.key === "Enter" || event.key === "Escape") {
1784
+ event.preventDefault();
1785
+ finish(event.key === "Enter");
1786
+ label.focus();
1787
+ }
1250
1788
  });
1789
+ input.addEventListener("blur", () => finish(true));
1790
+ input.focus();
1791
+ input.select();
1251
1792
  }
1252
1793
 
1253
- function updateContextMeter(snapshot = null) {
1254
- let percent = null;
1255
- if (snapshot && snapshot.context_remaining_percent !== undefined) {
1256
- percent = snapshot.context_remaining_percent;
1257
- } else {
1258
- const session = sessionById(activeSessionId);
1259
- if (session && session.context_remaining_percent !== undefined) {
1260
- percent = session.context_remaining_percent;
1794
+ async function renameSession(sessionId, title) {
1795
+ try {
1796
+ const response = await fetch(relativeUrl("api/session/message"), {
1797
+ method: "POST",
1798
+ headers: {"Content-Type": "application/json"},
1799
+ body: JSON.stringify({prompt: `/title ${title}`, sender: "web", session_id: sessionId}),
1800
+ });
1801
+ const payload = await response.json();
1802
+ if (!response.ok || !payload.ok) {
1803
+ throw new Error(payload.error || `HTTP ${response.status}`);
1261
1804
  }
1805
+ const session = sessionById(sessionId);
1806
+ if (session) {
1807
+ session.title = payload.snapshot.title;
1808
+ renderTabs();
1809
+ }
1810
+ } catch (error) {
1811
+ addEntry("error", `Could not rename session: ${error.message}`, "error");
1262
1812
  }
1263
- if (percent === null || percent === undefined || percent === "") {
1264
- contextMeter.classList.add("hidden");
1265
- contextMeterValue.textContent = "";
1266
- return;
1267
- }
1268
- const value = Math.max(0, Math.min(100, Math.round(Number(percent))));
1269
- if (!Number.isFinite(value)) {
1270
- contextMeter.classList.add("hidden");
1271
- contextMeterValue.textContent = "";
1272
- return;
1273
- }
1274
- if (value >= 100) {
1275
- contextMeter.classList.remove("hidden");
1276
- contextMeter.style.setProperty("--context-remaining", `${value}%`);
1277
- contextMeterValue.textContent = "";
1278
- return;
1279
- }
1280
- contextMeter.classList.remove("hidden");
1281
- contextMeter.style.setProperty("--context-remaining", `${value}%`);
1282
- contextMeterValue.textContent = String(value);
1813
+ }
1814
+
1815
+ function updatePromptPlaceholder(snapshot = null) {
1816
+ const session = snapshot || sessionById(activeSessionId);
1817
+ const label = session && session.model ? `Message to ${session.model}` : "Message";
1818
+ prompt.placeholder = `${label}…`;
1819
+ prompt.setAttribute("aria-label", label);
1820
+ }
1821
+
1822
+ function updateContextMeter(snapshot = null) {
1823
+ const session = snapshot || sessionById(activeSessionId);
1824
+ if (!session) return;
1825
+ const percent = session.context_remaining_percent;
1826
+ const compact = session.auto_compact_token_limit;
1827
+ const formatTokens = value => value == null ? "—" : value.toLocaleString("en-US");
1828
+ const remainingLabel = compact == null ? "context remaining" : "remaining until compact";
1829
+ contextMeter.style.setProperty("--context-remaining", `${percent == null ? 0 : percent}%`);
1830
+ contextMeterValue.textContent = percent == null ? "—" : percent >= 100 ? "" : String(percent);
1831
+ contextMeter.setAttribute("aria-label", percent == null
1832
+ ? "Context usage details"
1833
+ : `${percent}% ${remainingLabel}. Context usage details`);
1834
+ document.getElementById("contextCurrent").textContent = formatTokens(session.usage_tokens);
1835
+ document.getElementById("contextCompact").textContent = compact == null ? "Off" : formatTokens(compact);
1836
+ document.getElementById("contextMax").textContent = formatTokens(session.max_context_window);
1837
+ const note = compact == null
1838
+ ? "Auto-compact is off. The ring shows remaining context."
1839
+ : "The ring reaches 0% at the compact limit. Compaction runs before the next model request.";
1840
+ document.getElementById("contextNote").textContent = session.usage_tokens == null
1841
+ ? `Waiting for current usage. ${note}`
1842
+ : note;
1843
+ }
1844
+
1845
+ function setupContextDetails() {
1846
+ const control = document.getElementById("contextControl");
1847
+ const details = document.getElementById("contextDetails");
1848
+ let pinned = false;
1849
+ const show = visible => {
1850
+ details.hidden = !visible;
1851
+ contextMeter.setAttribute("aria-expanded", String(visible));
1852
+ };
1853
+ const close = () => {
1854
+ pinned = false;
1855
+ show(false);
1856
+ };
1857
+ control.addEventListener("pointerenter", event => {
1858
+ if (event.pointerType !== "touch") show(true);
1859
+ });
1860
+ control.addEventListener("pointerleave", () => {
1861
+ if (!pinned) show(false);
1862
+ });
1863
+ contextMeter.addEventListener("focus", () => show(true));
1864
+ control.addEventListener("focusout", event => {
1865
+ if (!pinned && !control.contains(event.relatedTarget)) show(false);
1866
+ });
1867
+ contextMeter.addEventListener("click", () => {
1868
+ pinned = !pinned;
1869
+ show(pinned);
1870
+ });
1871
+ document.addEventListener("pointerdown", event => {
1872
+ if (!control.contains(event.target)) close();
1873
+ });
1874
+ document.addEventListener("keydown", event => {
1875
+ if (event.key === "Escape") close();
1876
+ });
1877
+ tabsEl.addEventListener("click", close);
1878
+ newTabButton.addEventListener("click", close);
1283
1879
  }
1284
1880
 
1285
1881
  function messageSignature(snapshot) {
1286
1882
  const turns = (snapshot && snapshot.turns) || [];
1287
- return JSON.stringify(
1883
+ return JSON.stringify([
1288
1884
  turns.map((item) => [
1289
1885
  item.submission_id || "",
1290
1886
  item.turn_id || "",
@@ -1295,21 +1891,26 @@
1295
1891
  item.error || "",
1296
1892
  item.kind || "",
1297
1893
  ]),
1298
- );
1894
+ snapshot.input_request || null,
1895
+ ]);
1299
1896
  }
1300
1897
 
1301
1898
  async function pollSession() {
1302
- if (pollInFlight) return;
1303
- pollInFlight = true;
1304
1899
  const requestedSessionId = activeSessionId;
1900
+ if (sessionPoll) {
1901
+ if (sessionPoll.sessionId === requestedSessionId) return;
1902
+ sessionPoll.controller.abort();
1903
+ }
1904
+ const request = {sessionId: requestedSessionId, controller: new AbortController()};
1905
+ sessionPoll = request;
1305
1906
  try {
1306
1907
  const path = requestedSessionId
1307
1908
  ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
1308
1909
  : "api/session";
1309
- const response = await fetch(relativeUrl(path));
1910
+ const response = await fetch(relativeUrl(path), {signal: request.controller.signal});
1310
1911
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1311
1912
  const payload = await response.json();
1312
- if (requestedSessionId && requestedSessionId !== activeSessionId) {
1913
+ if (sessionPoll !== request || requestedSessionId !== activeSessionId) {
1313
1914
  return;
1314
1915
  }
1315
1916
  if (payload.session_id && payload.session_id !== activeSessionId) {
@@ -1318,9 +1919,11 @@
1318
1919
  updateSessions(payload.sessions || []);
1319
1920
  renderSnapshot(payload.snapshot);
1320
1921
  } catch (error) {
1321
- setSpinner(`poll error: ${error.message}`);
1922
+ if (sessionPoll === request && requestedSessionId === activeSessionId) {
1923
+ setSpinner(`poll error: ${error.message}`);
1924
+ }
1322
1925
  } finally {
1323
- pollInFlight = false;
1926
+ if (sessionPoll === request) sessionPoll = null;
1324
1927
  }
1325
1928
  }
1326
1929
 
@@ -1370,9 +1973,25 @@
1370
1973
  boardPollTimer = setInterval(pollBoard, 2000);
1371
1974
  }
1372
1975
 
1976
+ function renderQueueHint(inputs) {
1977
+ queueHint.hidden = inputs.length === 0;
1978
+ for (const [kind, label, badge] of [
1979
+ ["steer", "[soon]", steerHint],
1980
+ ["enqueue", "[on done]", enqueueHint],
1981
+ ]) {
1982
+ const text = inputs.filter(item => item.queue === kind).map(item => item.prompt).join("\n");
1983
+ badge.hidden = !text;
1984
+ badge.querySelector(".queue-preview").textContent = text;
1985
+ badge.title = label + "\n" + text;
1986
+ }
1987
+ }
1988
+
1373
1989
  function renderSnapshot(snapshot) {
1374
1990
  if (!snapshot) return;
1991
+ updatePromptPlaceholder(snapshot);
1375
1992
  const state = stateForSession(activeSessionId);
1993
+ state.inputRequest = snapshot.input_request || null;
1994
+ renderQueueHint(snapshot.queued_inputs);
1376
1995
  const signature = messageSignature(snapshot);
1377
1996
  if (signature === state.lastRenderedSignature) {
1378
1997
  setSpinner(snapshot.spinner || "");
@@ -1389,6 +2008,11 @@
1389
2008
  const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
1390
2009
  if (turns.length) {
1391
2010
  turns.forEach((turn) => renderTurn(turn, shouldStick));
2011
+ } else if (!state.inputRequest) {
2012
+ log.appendChild(document.getElementById("emptyChat").content.cloneNode(true));
2013
+ }
2014
+ if (state.inputRequest) {
2015
+ addEntry("input requested", state.inputRequest.text, "control", shouldStick);
1392
2016
  }
1393
2017
  setSpinner(snapshot.spinner || "");
1394
2018
  updateContextMeter(snapshot);
@@ -1423,12 +2047,11 @@
1423
2047
  );
1424
2048
  }
1425
2049
  if (turn.response) {
1426
- addMarkdownEntry(
1427
- isControl ? "result" : "response",
1428
- turn.response || "",
1429
- isControl ? "control" : "response",
1430
- shouldStick,
1431
- );
2050
+ if (isControl) {
2051
+ addEntry("result", turn.response, "control", shouldStick);
2052
+ } else {
2053
+ addMarkdownEntry("response", turn.response, "response", shouldStick);
2054
+ }
1432
2055
  }
1433
2056
  if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
1434
2057
  if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
@@ -1460,6 +2083,8 @@
1460
2083
  saveActiveSessionUiState();
1461
2084
  activeSessionId = sessionId;
1462
2085
  renderTabs();
2086
+ updatePromptPlaceholder();
2087
+ updateContextMeter();
1463
2088
  const state = stateForSession(sessionId);
1464
2089
  state.lastRenderedSignature = "";
1465
2090
  lastRenderedSignature = "";
@@ -1467,6 +2092,7 @@
1467
2092
  resizePrompt();
1468
2093
  suppressScrollSave = true;
1469
2094
  log.textContent = "";
2095
+ renderQueueHint([]);
1470
2096
  suppressScrollSave = false;
1471
2097
  setSpinner("", {suppressDoneNotification: true});
1472
2098
  restoreScrollTop = state.scrollTop;
@@ -1505,26 +2131,17 @@
1505
2131
  addEntry("error", payload.error || `HTTP ${response.status}`, "error");
1506
2132
  return;
1507
2133
  }
1508
- sessionState.delete(sessionId);
1509
2134
  updateSessions(payload.sessions || []);
1510
2135
  if (activeSessionId === sessionId) {
1511
- activeSessionId = sessions.length ? sessions[0].id || "" : "";
1512
- lastRenderedSignature = "";
1513
- prompt.value = "";
1514
- resizePrompt();
1515
- suppressScrollSave = true;
1516
- log.textContent = "";
1517
- suppressScrollSave = false;
1518
- await pollSession();
1519
- } else {
1520
- renderTabs();
2136
+ await switchSession(sessions[0].id);
1521
2137
  }
2138
+ sessionState.delete(sessionId);
1522
2139
  }
1523
2140
 
1524
2141
  form.addEventListener("submit", (event) => {
1525
2142
  event.preventDefault();
1526
2143
  const text = prompt.value.trim();
1527
- if (!text) return;
2144
+ if (!text && !stateForSession(activeSessionId).inputRequest) return;
1528
2145
  prompt.value = "";
1529
2146
  resizePrompt();
1530
2147
  stateForSession(activeSessionId).draft = "";
@@ -1565,6 +2182,12 @@
1565
2182
  splitter.addEventListener("dblclick", centerSplit);
1566
2183
  spinner.addEventListener("click", toggleSpinnerNotification);
1567
2184
  newTabButton.addEventListener("click", createSession);
2185
+ document.querySelectorAll("[data-pane]").forEach((button) => {
2186
+ button.addEventListener("click", () => {
2187
+ workspace.dataset.mobilePane = button.dataset.pane;
2188
+ if (button.dataset.pane === "chat") resizePrompt();
2189
+ });
2190
+ });
1568
2191
  window.addEventListener("resize", () => {
1569
2192
  const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
1570
2193
  if (current > 0) setChatWidth(current);
@@ -1577,6 +2200,7 @@
1577
2200
  });
1578
2201
 
1579
2202
  loadChatWidth();
2203
+ setupContextDetails();
1580
2204
  resizePrompt();
1581
2205
  startPolling();
1582
2206
  startBoardPolling();