python-codex 0.2.6__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 +18 -14
  2. pycodex/agent.py +468 -462
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -436
  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 +329 -252
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +90 -52
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +235 -803
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -174
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +4 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +201 -0
  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 +13 -13
  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 +50 -66
  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/utils/image_utils.py +76 -0
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +263 -161
  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 +25 -22
  61. responses_server/messages_api.py +96 -49
  62. responses_server/payload_processors.py +25 -19
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +196 -107
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +51 -13
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +470 -384
  71. workspace_server/workspace.html +859 -232
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +168 -100
  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 -553
  80. python_codex-0.2.6.dist-info/METADATA +0 -441
  81. python_codex-0.2.6.dist-info/RECORD +0 -91
  82. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.6.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;
94
97
  }
98
+ .boardbar-title {
99
+ color: var(--ink);
100
+ font-size: 14px;
101
+ font-weight: 600;
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,26 +388,39 @@
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;
306
396
  background: transparent;
307
- opacity: 0.42;
308
- font-size: 17px;
309
- font-weight: 500;
310
- line-height: 18px;
397
+ opacity: 0;
398
+ visibility: hidden;
399
+ pointer-events: none;
400
+ }
401
+ .tab-close svg {
402
+ display: block;
403
+ width: 12px;
404
+ height: 12px;
311
405
  }
312
406
  .tab-new {
313
- font-size: 22px;
314
- font-weight: 400;
315
- 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;
316
417
  }
317
- .tab:hover .tab-close,
318
- .tab:focus-within .tab-close,
319
418
  .tab.active .tab-close {
320
419
  opacity: 0.85;
420
+ visibility: visible;
421
+ pointer-events: auto;
321
422
  }
322
- .tab-close:disabled {
423
+ .tab.active .tab-close:disabled {
323
424
  opacity: 0.22;
324
425
  cursor: default;
325
426
  }
@@ -332,52 +433,70 @@
332
433
  min-height: 0;
333
434
  overflow-y: auto;
334
435
  overflow-x: hidden;
335
- padding: var(--space-6) var(--space-7);
436
+ padding: 16px 20px 20px;
336
437
  display: flex;
337
438
  flex-direction: column;
338
- gap: var(--space-4);
339
- background: var(--bg);
439
+ gap: 14px;
440
+ background: var(--panel-raised);
441
+ scrollbar-width: thin;
442
+ scrollbar-color: var(--line) transparent;
340
443
  }
341
444
  .entry {
342
445
  min-width: 0;
343
- border: 1px solid var(--line);
344
- border-radius: var(--radius-sm);
345
- padding: var(--space-4) var(--space-5);
346
- background: var(--panel-raised);
347
- 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);
348
454
  }
349
455
  .entry.user {
350
- border-color: var(--warn);
351
- background: var(--warn-soft);
352
- 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);
353
461
  }
354
462
  .entry.response,
355
463
  .entry.assistant {
356
- border-color: var(--line);
357
- 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;
358
473
  }
359
474
  .entry.thinking {
360
- border-color: var(--success-line);
361
- background: var(--success-soft);
362
- color: var(--success);
363
- 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;
364
477
  }
365
478
  .entry.control {
366
- border-color: #b7d3ee;
367
- background: var(--info-soft);
368
- color: var(--info);
479
+ white-space: pre-wrap;
480
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
369
481
  }
370
482
  .entry.tool {
371
- border-color: #d8cdf1;
372
- background: var(--violet-soft);
373
- color: var(--violet);
374
483
  font-size: 12px;
375
484
  }
376
485
  .entry.error {
377
- border-color: #efb8c1;
486
+ border-left: 2px solid var(--danger);
487
+ border-radius: var(--radius-sm);
488
+ padding: 12px 16px;
378
489
  background: var(--danger-soft);
379
490
  color: var(--danger);
380
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; }
381
500
  .text {
382
501
  white-space: pre-wrap;
383
502
  overflow-wrap: anywhere;
@@ -387,7 +506,7 @@
387
506
  overflow-wrap: anywhere;
388
507
  }
389
508
  .markdown p {
390
- margin: 0 0 var(--space-4);
509
+ margin: 0 0 8px;
391
510
  }
392
511
  .markdown p:last-child,
393
512
  .markdown ul:last-child,
@@ -398,17 +517,17 @@
398
517
  }
399
518
  .markdown ul,
400
519
  .markdown ol {
401
- margin: 0 0 var(--space-4) 18px;
520
+ margin: 0 0 8px 18px;
402
521
  padding: 0;
403
522
  }
404
523
  .markdown li {
405
524
  margin: 2px 0;
406
525
  }
407
526
  .markdown pre {
408
- margin: 0 0 var(--space-4);
409
- padding: var(--space-4) var(--space-5);
410
- border: 1px solid var(--line);
411
- border-radius: var(--radius-sm);
527
+ margin: 0 0 10px;
528
+ padding: 10px 12px;
529
+ border: 0;
530
+ border-radius: var(--radius-md);
412
531
  background: var(--code);
413
532
  overflow-x: auto;
414
533
  white-space: pre;
@@ -443,6 +562,13 @@
443
562
  .markdown a:hover {
444
563
  text-decoration: underline;
445
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); }
446
572
  .markdown .katex-display {
447
573
  margin: 0 0 var(--space-4);
448
574
  overflow-x: auto;
@@ -462,32 +588,175 @@
462
588
  .markdown .katex-display:last-child {
463
589
  margin-bottom: 0;
464
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
+ }
465
672
  .composer {
466
673
  position: relative;
467
674
  min-width: 0;
468
- padding: var(--space-4);
675
+ padding: 8px 20px 10px;
469
676
  display: grid;
470
- gap: var(--space-3);
677
+ gap: 6px;
471
678
  align-content: end;
472
- 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;
473
739
  }
474
740
  textarea {
475
741
  min-width: 0;
476
- resize: vertical;
742
+ resize: none;
477
743
  max-height: 35vh;
478
744
  width: 100%;
479
- border: 1px solid var(--line);
480
- border-radius: var(--radius-sm);
481
- padding: var(--space-4) var(--space-5);
482
- background: var(--panel-deep);
745
+ min-height: 28px;
746
+ border: 0;
747
+ padding: 3px 0;
748
+ display: block;
749
+ background: transparent;
483
750
  color: var(--ink);
484
751
  font: inherit;
485
- line-height: 1.4;
752
+ line-height: 22px;
486
753
  overflow-y: auto;
754
+ scrollbar-width: none;
755
+ }
756
+ textarea::-webkit-scrollbar {
757
+ display: none;
487
758
  }
488
759
  textarea:focus {
489
- border-color: var(--accent);
490
- box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.18);
491
760
  outline: 0;
492
761
  }
493
762
  .actions {
@@ -497,11 +766,15 @@
497
766
  }
498
767
  .spinner-wrap {
499
768
  position: relative;
500
- display: inline-flex;
769
+ display: flex;
501
770
  align-items: center;
502
- justify-self: start;
503
- align-self: end;
771
+ gap: var(--space-3);
504
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);
505
778
  }
506
779
  .prompt-row {
507
780
  display: flex;
@@ -510,21 +783,21 @@
510
783
  min-width: 0;
511
784
  }
512
785
  .spinner {
513
- --spinner-pill-height: 22px;
514
786
  position: relative;
515
787
  display: inline-flex;
516
788
  align-items: center;
517
789
  max-width: 100%;
518
- height: var(--spinner-pill-height);
790
+ min-width: 0;
791
+ height: 22px;
519
792
  color: var(--muted);
520
793
  font-size: 12px;
521
794
  white-space: nowrap;
522
795
  overflow: hidden;
523
796
  text-overflow: ellipsis;
524
797
  padding: var(--space-1) var(--space-4) var(--space-1) 18px;
525
- border-radius: 999px;
526
- background: rgba(230, 246, 248, 0.96);
527
- border: 1px solid rgba(8, 127, 154, 0.35);
798
+ border-radius: var(--radius-sm);
799
+ background: transparent;
800
+ border: 1px solid transparent;
528
801
  font: inherit;
529
802
  font-size: 12px;
530
803
  cursor: pointer;
@@ -532,16 +805,15 @@
532
805
  .spinner:empty {
533
806
  display: none;
534
807
  }
535
- .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]))) {
536
812
  display: none;
537
813
  }
538
814
  .spinner:focus-visible {
539
- border-color: var(--accent);
540
- box-shadow: 0 0 0 3px rgba(8, 127, 154, 0.18);
541
- outline: 0;
542
- }
543
- .spinner-wrap:has(.spinner:not(:empty)) {
544
- margin-bottom: var(--space-1);
815
+ outline: 2px solid var(--accent);
816
+ outline-offset: 1px;
545
817
  }
546
818
  .spinner:not(:empty)::before {
547
819
  content: "";
@@ -556,12 +828,8 @@
556
828
  animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
557
829
  }
558
830
  .spinner.notify-enabled {
559
- color: var(--success);
560
- background: var(--success-soft);
561
- border-color: var(--success-line);
562
- box-shadow:
563
- inset 0 1px 2px rgba(27, 57, 72, 0.08),
564
- inset 0 0 0 1px rgba(36, 122, 82, 0.1);
831
+ color: var(--accent);
832
+ background: var(--accent-soft);
565
833
  }
566
834
  .toast {
567
835
  position: absolute;
@@ -597,17 +865,9 @@
597
865
  transform: scale(1.08);
598
866
  }
599
867
  }
600
- @keyframes thinking-breathe {
601
- 0%, 100% {
602
- border-color: var(--success-line);
603
- box-shadow: var(--entry-shadow);
604
- }
605
- 50% {
606
- border-color: var(--success);
607
- box-shadow:
608
- 0 0 0 2px rgba(36, 122, 82, 0.14),
609
- 0 2px 7px rgba(27, 57, 72, 0.08);
610
- }
868
+ @keyframes activity-breathe {
869
+ 0%, 100% { border-left-color: var(--line); }
870
+ 50% { border-left-color: var(--accent); }
611
871
  }
612
872
  @media (prefers-reduced-motion: reduce) {
613
873
  .entry.thinking,
@@ -629,21 +889,61 @@
629
889
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
630
890
  font-size: 12px;
631
891
  }
632
- @media (max-width: 900px) {
633
- body { overflow: auto; height: auto; }
634
- .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; }
635
923
  }
636
924
  </style>
637
925
  </head>
638
926
  <body>
639
- <div class="workspace">
927
+ <div class="workspace" data-mobile-pane="chat">
640
928
  <section class="board-pane" aria-label="workspace board">
641
929
  <div class="boardbar">
642
- <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>
643
936
  <div class="boardbar-paths">
644
- <span>@<code>__CWD_LABEL__</code></span>
645
- <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>
646
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>
647
947
  </div>
648
948
  <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
649
949
  </section>
@@ -652,24 +952,75 @@
652
952
  <div class="topbar">
653
953
  <div class="tabbar">
654
954
  <div id="tabs" class="tabs" role="tablist"></div>
655
- <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>
656
968
  </div>
657
969
  </div>
658
- <div id="log" class="log"></div>
970
+ <div id="log" class="log" aria-label="Conversation"></div>
659
971
  <form id="composer" class="composer">
660
- <div class="spinner-wrap">
661
- <div id="toast" class="toast" role="status" aria-live="polite"></div>
662
- <button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
663
- </div>
664
- <div class="prompt-row">
665
- <div id="contextMeter" class="context-meter hidden" aria-hidden="true">
666
- <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>
667
1012
  </div>
668
- <textarea id="prompt" rows="1" placeholder="Ask pycodex or type /help..."></textarea>
669
1013
  </div>
670
1014
  </form>
671
1015
  </section>
672
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>
673
1024
  <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
674
1025
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
675
1026
  <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
@@ -677,6 +1028,9 @@
677
1028
  <script>
678
1029
  const log = document.getElementById("log");
679
1030
  const spinner = document.getElementById("spinner");
1031
+ const queueHint = document.getElementById("queueHint");
1032
+ const steerHint = document.getElementById("steerHint");
1033
+ const enqueueHint = document.getElementById("enqueueHint");
680
1034
  const toast = document.getElementById("toast");
681
1035
  const prompt = document.getElementById("prompt");
682
1036
  const form = document.getElementById("composer");
@@ -689,7 +1043,7 @@
689
1043
  const newTabButton = document.getElementById("newTab");
690
1044
  let pollTimer = null;
691
1045
  let boardPollTimer = null;
692
- let pollInFlight = false;
1046
+ let sessionPoll = null;
693
1047
  let boardPollInFlight = false;
694
1048
  let lastBoardSignature = null;
695
1049
  let lastRenderedSignature = "";
@@ -713,6 +1067,40 @@
713
1067
  ];
714
1068
  const mathTokenPrefix = "\uE000math";
715
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
+ })();
716
1104
 
717
1105
  function relativeUrl(path) {
718
1106
  return new URL(path, window.location.href).toString();
@@ -737,9 +1125,9 @@
737
1125
  entry.className = "entry " + (cls || role);
738
1126
  const body = document.createElement("div");
739
1127
  body.className = "text markdown";
740
- renderMarkdownInto(body, text || "");
741
1128
  entry.appendChild(body);
742
1129
  log.appendChild(entry);
1130
+ renderMarkdownInto(body, text || "");
743
1131
  if (stickToBottom) scrollToBottom();
744
1132
  return body;
745
1133
  }
@@ -760,6 +1148,102 @@
760
1148
  anchor.target = "_blank";
761
1149
  anchor.rel = "noopener noreferrer";
762
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();
763
1247
  }
764
1248
 
765
1249
  function isEscapedAt(source, index) {
@@ -1166,6 +1650,7 @@
1166
1650
  draft: "",
1167
1651
  notifyWhenDone: false,
1168
1652
  wasRunning: false,
1653
+ inputRequest: null,
1169
1654
  });
1170
1655
  }
1171
1656
  return sessionState.get(key);
@@ -1201,88 +1686,201 @@
1201
1686
  activeSessionId = sessions[0].id || "";
1202
1687
  }
1203
1688
  renderTabs();
1689
+ updatePromptPlaceholder();
1204
1690
  updateContextMeter();
1205
1691
  updateSpinnerNotifyUi();
1206
1692
  }
1207
1693
 
1208
1694
  function renderTabs() {
1209
- tabsEl.textContent = "";
1695
+ const existingTabs = new Map(
1696
+ Array.from(tabsEl.children, tab => [tab.dataset.sessionId, tab])
1697
+ );
1210
1698
  sessions.forEach((session, index) => {
1211
1699
  const sessionId = session.id || "";
1212
- 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);
1213
1737
  tab.className =
1214
1738
  "tab" +
1215
1739
  (sessionId === activeSessionId ? " active" : "") +
1216
1740
  (session.running ? " running" : "");
1217
1741
  tab.title = session.title || `Session ${index + 1}`;
1218
- tab.setAttribute("role", "tab");
1219
1742
  tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
1220
- if (session.running) tab.setAttribute("aria-busy", "true");
1221
- tab.addEventListener("click", () => switchSession(sessionId));
1222
-
1223
- if (session.running) {
1224
- const status = document.createElement("span");
1225
- status.className = "tab-status";
1226
- status.setAttribute("aria-hidden", "true");
1227
- 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);
1228
1751
  }
1752
+ });
1753
+ existingTabs.forEach(tab => tab.remove());
1754
+ }
1229
1755
 
1230
- const label = document.createElement("span");
1231
- label.className = "tab-label";
1232
- label.textContent = session.title || `Session ${index + 1}`;
1233
- tab.appendChild(label);
1234
-
1235
- const close = document.createElement("button");
1236
- close.type = "button";
1237
- close.className = "tab-close";
1238
- close.title = "Close session";
1239
- close.setAttribute("aria-label", "Close session");
1240
- close.textContent = "×";
1241
- close.disabled = sessions.length <= 1;
1242
- close.addEventListener("click", (event) => {
1243
- event.stopPropagation();
1244
- closeSession(sessionId);
1245
- });
1246
- tab.appendChild(close);
1247
- 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
+ }
1248
1788
  });
1789
+ input.addEventListener("blur", () => finish(true));
1790
+ input.focus();
1791
+ input.select();
1249
1792
  }
1250
1793
 
1251
- function updateContextMeter(snapshot = null) {
1252
- let percent = null;
1253
- if (snapshot && snapshot.context_remaining_percent !== undefined) {
1254
- percent = snapshot.context_remaining_percent;
1255
- } else {
1256
- const session = sessionById(activeSessionId);
1257
- if (session && session.context_remaining_percent !== undefined) {
1258
- 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}`);
1259
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");
1260
1812
  }
1261
- if (percent === null || percent === undefined || percent === "") {
1262
- contextMeter.classList.add("hidden");
1263
- contextMeterValue.textContent = "";
1264
- return;
1265
- }
1266
- const value = Math.max(0, Math.min(100, Math.round(Number(percent))));
1267
- if (!Number.isFinite(value)) {
1268
- contextMeter.classList.add("hidden");
1269
- contextMeterValue.textContent = "";
1270
- return;
1271
- }
1272
- if (value >= 100) {
1273
- contextMeter.classList.remove("hidden");
1274
- contextMeter.style.setProperty("--context-remaining", `${value}%`);
1275
- contextMeterValue.textContent = "";
1276
- return;
1277
- }
1278
- contextMeter.classList.remove("hidden");
1279
- contextMeter.style.setProperty("--context-remaining", `${value}%`);
1280
- 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);
1281
1879
  }
1282
1880
 
1283
1881
  function messageSignature(snapshot) {
1284
1882
  const turns = (snapshot && snapshot.turns) || [];
1285
- return JSON.stringify(
1883
+ return JSON.stringify([
1286
1884
  turns.map((item) => [
1287
1885
  item.submission_id || "",
1288
1886
  item.turn_id || "",
@@ -1293,21 +1891,26 @@
1293
1891
  item.error || "",
1294
1892
  item.kind || "",
1295
1893
  ]),
1296
- );
1894
+ snapshot.input_request || null,
1895
+ ]);
1297
1896
  }
1298
1897
 
1299
1898
  async function pollSession() {
1300
- if (pollInFlight) return;
1301
- pollInFlight = true;
1302
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;
1303
1906
  try {
1304
1907
  const path = requestedSessionId
1305
1908
  ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
1306
1909
  : "api/session";
1307
- const response = await fetch(relativeUrl(path));
1910
+ const response = await fetch(relativeUrl(path), {signal: request.controller.signal});
1308
1911
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1309
1912
  const payload = await response.json();
1310
- if (requestedSessionId && requestedSessionId !== activeSessionId) {
1913
+ if (sessionPoll !== request || requestedSessionId !== activeSessionId) {
1311
1914
  return;
1312
1915
  }
1313
1916
  if (payload.session_id && payload.session_id !== activeSessionId) {
@@ -1316,9 +1919,11 @@
1316
1919
  updateSessions(payload.sessions || []);
1317
1920
  renderSnapshot(payload.snapshot);
1318
1921
  } catch (error) {
1319
- setSpinner(`poll error: ${error.message}`);
1922
+ if (sessionPoll === request && requestedSessionId === activeSessionId) {
1923
+ setSpinner(`poll error: ${error.message}`);
1924
+ }
1320
1925
  } finally {
1321
- pollInFlight = false;
1926
+ if (sessionPoll === request) sessionPoll = null;
1322
1927
  }
1323
1928
  }
1324
1929
 
@@ -1368,9 +1973,25 @@
1368
1973
  boardPollTimer = setInterval(pollBoard, 2000);
1369
1974
  }
1370
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
+
1371
1989
  function renderSnapshot(snapshot) {
1372
1990
  if (!snapshot) return;
1991
+ updatePromptPlaceholder(snapshot);
1373
1992
  const state = stateForSession(activeSessionId);
1993
+ state.inputRequest = snapshot.input_request || null;
1994
+ renderQueueHint(snapshot.queued_inputs);
1374
1995
  const signature = messageSignature(snapshot);
1375
1996
  if (signature === state.lastRenderedSignature) {
1376
1997
  setSpinner(snapshot.spinner || "");
@@ -1387,6 +2008,11 @@
1387
2008
  const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
1388
2009
  if (turns.length) {
1389
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);
1390
2016
  }
1391
2017
  setSpinner(snapshot.spinner || "");
1392
2018
  updateContextMeter(snapshot);
@@ -1421,12 +2047,11 @@
1421
2047
  );
1422
2048
  }
1423
2049
  if (turn.response) {
1424
- addMarkdownEntry(
1425
- isControl ? "result" : "response",
1426
- turn.response || "",
1427
- isControl ? "control" : "response",
1428
- shouldStick,
1429
- );
2050
+ if (isControl) {
2051
+ addEntry("result", turn.response, "control", shouldStick);
2052
+ } else {
2053
+ addMarkdownEntry("response", turn.response, "response", shouldStick);
2054
+ }
1430
2055
  }
1431
2056
  if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
1432
2057
  if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
@@ -1457,6 +2082,9 @@
1457
2082
  if (!sessionId || sessionId === activeSessionId) return;
1458
2083
  saveActiveSessionUiState();
1459
2084
  activeSessionId = sessionId;
2085
+ renderTabs();
2086
+ updatePromptPlaceholder();
2087
+ updateContextMeter();
1460
2088
  const state = stateForSession(sessionId);
1461
2089
  state.lastRenderedSignature = "";
1462
2090
  lastRenderedSignature = "";
@@ -1464,6 +2092,7 @@
1464
2092
  resizePrompt();
1465
2093
  suppressScrollSave = true;
1466
2094
  log.textContent = "";
2095
+ renderQueueHint([]);
1467
2096
  suppressScrollSave = false;
1468
2097
  setSpinner("", {suppressDoneNotification: true});
1469
2098
  restoreScrollTop = state.scrollTop;
@@ -1502,26 +2131,17 @@
1502
2131
  addEntry("error", payload.error || `HTTP ${response.status}`, "error");
1503
2132
  return;
1504
2133
  }
1505
- sessionState.delete(sessionId);
1506
2134
  updateSessions(payload.sessions || []);
1507
2135
  if (activeSessionId === sessionId) {
1508
- activeSessionId = sessions.length ? sessions[0].id || "" : "";
1509
- lastRenderedSignature = "";
1510
- prompt.value = "";
1511
- resizePrompt();
1512
- suppressScrollSave = true;
1513
- log.textContent = "";
1514
- suppressScrollSave = false;
1515
- await pollSession();
1516
- } else {
1517
- renderTabs();
2136
+ await switchSession(sessions[0].id);
1518
2137
  }
2138
+ sessionState.delete(sessionId);
1519
2139
  }
1520
2140
 
1521
2141
  form.addEventListener("submit", (event) => {
1522
2142
  event.preventDefault();
1523
2143
  const text = prompt.value.trim();
1524
- if (!text) return;
2144
+ if (!text && !stateForSession(activeSessionId).inputRequest) return;
1525
2145
  prompt.value = "";
1526
2146
  resizePrompt();
1527
2147
  stateForSession(activeSessionId).draft = "";
@@ -1562,6 +2182,12 @@
1562
2182
  splitter.addEventListener("dblclick", centerSplit);
1563
2183
  spinner.addEventListener("click", toggleSpinnerNotification);
1564
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
+ });
1565
2191
  window.addEventListener("resize", () => {
1566
2192
  const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
1567
2193
  if (current > 0) setChatWidth(current);
@@ -1574,6 +2200,7 @@
1574
2200
  });
1575
2201
 
1576
2202
  loadChatWidth();
2203
+ setupContextDetails();
1577
2204
  resizePrompt();
1578
2205
  startPolling();
1579
2206
  startBoardPolling();