python-codex 0.2.7__py3-none-any.whl → 0.3.1__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 (85) 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 +321 -883
  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.1.dist-info/METADATA +743 -0
  56. python_codex-0.3.1.dist-info/RECORD +91 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +34 -33
  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 +209 -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 +465 -377
  71. workspace_server/owned_process.py +27 -0
  72. workspace_server/workspace.html +854 -228
  73. workspace_server/workspaces.html +94 -95
  74. workspace_server/workspaces.py +141 -84
  75. pycodex/collaboration.py +0 -20
  76. pycodex/interactive_session.py +0 -415
  77. pycodex/prompts/collaboration_default.md +0 -11
  78. pycodex/prompts/collaboration_plan.md +0 -128
  79. pycodex/utils/toolcall_visualize.py +0 -713
  80. pycodex/utils/visualize.py +0 -560
  81. python_codex-0.2.7.dist-info/METADATA +0 -455
  82. python_codex-0.2.7.dist-info/RECORD +0 -93
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.1.dist-info}/WHEEL +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.1.dist-info}/entry_points.txt +0 -0
  85. {python_codex-0.2.7.dist-info → python_codex-0.3.1.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");
@@ -689,9 +1041,10 @@
689
1041
  const contextMeter = document.getElementById("contextMeter");
690
1042
  const contextMeterValue = document.getElementById("contextMeterValue");
691
1043
  const newTabButton = document.getElementById("newTab");
1044
+ const workspaceTitle = document.title;
692
1045
  let pollTimer = null;
693
1046
  let boardPollTimer = null;
694
- let pollInFlight = false;
1047
+ let sessionPoll = null;
695
1048
  let boardPollInFlight = false;
696
1049
  let lastBoardSignature = null;
697
1050
  let lastRenderedSignature = "";
@@ -715,6 +1068,40 @@
715
1068
  ];
716
1069
  const mathTokenPrefix = "\uE000math";
717
1070
  const mathTokenSuffix = "\uE001";
1071
+ const renderMermaidSvg = (() => {
1072
+ let rendererPromise = null;
1073
+ let diagramId = 0;
1074
+ return async (source, figure) => {
1075
+ if (!rendererPromise) {
1076
+ rendererPromise = import(
1077
+ "https://cdn.jsdelivr.net/npm/mermaid@11.12.0/dist/mermaid.esm.min.mjs"
1078
+ ).then(({default: renderer}) => {
1079
+ renderer.initialize({
1080
+ startOnLoad: false,
1081
+ securityLevel: "strict",
1082
+ suppressErrorRendering: true,
1083
+ htmlLabels: false,
1084
+ flowchart: {htmlLabels: false},
1085
+ theme: "neutral",
1086
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1087
+ });
1088
+ return renderer;
1089
+ });
1090
+ }
1091
+ const renderer = await rendererPromise;
1092
+ if (!figure.isConnected) return null;
1093
+ const measure = document.createElement("div");
1094
+ measure.style.cssText =
1095
+ "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none";
1096
+ document.body.appendChild(measure);
1097
+ try {
1098
+ diagramId += 1;
1099
+ return await renderer.render("workspace-diagram-" + diagramId, source, measure);
1100
+ } finally {
1101
+ measure.remove();
1102
+ }
1103
+ };
1104
+ })();
718
1105
 
719
1106
  function relativeUrl(path) {
720
1107
  return new URL(path, window.location.href).toString();
@@ -739,9 +1126,9 @@
739
1126
  entry.className = "entry " + (cls || role);
740
1127
  const body = document.createElement("div");
741
1128
  body.className = "text markdown";
742
- renderMarkdownInto(body, text || "");
743
1129
  entry.appendChild(body);
744
1130
  log.appendChild(entry);
1131
+ renderMarkdownInto(body, text || "");
745
1132
  if (stickToBottom) scrollToBottom();
746
1133
  return body;
747
1134
  }
@@ -762,6 +1149,102 @@
762
1149
  anchor.target = "_blank";
763
1150
  anchor.rel = "noopener noreferrer";
764
1151
  });
1152
+ renderMermaidInto(root);
1153
+ }
1154
+
1155
+ async function renderMermaidInto(root) {
1156
+ for (const code of root.querySelectorAll("pre > code.language-mermaid")) {
1157
+ if (!code.isConnected) return;
1158
+ const source = code.textContent;
1159
+ const pre = code.parentElement;
1160
+ const figure = document.createElement("figure");
1161
+ figure.className = "mermaid-figure";
1162
+ const diagram = document.createElement("div");
1163
+ diagram.className = "mermaid-diagram";
1164
+ diagram.textContent = "Rendering diagram…";
1165
+ const details = document.createElement("details");
1166
+ details.className = "mermaid-source";
1167
+ details.open = true;
1168
+ const summary = document.createElement("summary");
1169
+ summary.textContent = "Diagram source";
1170
+ details.appendChild(summary);
1171
+ pre.replaceWith(figure);
1172
+ details.appendChild(pre);
1173
+ const controls = document.createElement("div");
1174
+ controls.className = "mermaid-controls";
1175
+ controls.appendChild(details);
1176
+ figure.append(diagram, controls);
1177
+ try {
1178
+ const result = await renderMermaidSvg(source, figure);
1179
+ if (!result || !figure.isConnected) return;
1180
+ const stickToBottom = isNearBottom();
1181
+ diagram.innerHTML = window.DOMPurify.sanitize(result.svg, {
1182
+ USE_PROFILES: {svg: true, svgFilters: true},
1183
+ });
1184
+ const svg = diagram.querySelector("svg");
1185
+ svg.style.width = svg.viewBox.baseVal.width + "px";
1186
+ svg.style.maxWidth = "100%";
1187
+ const expand = document.createElement("button");
1188
+ expand.type = "button";
1189
+ expand.textContent = "Expand";
1190
+ expand.addEventListener("click", () => showMermaidDiagram(svg));
1191
+ controls.appendChild(expand);
1192
+ details.open = false;
1193
+ if (stickToBottom) scrollToBottom();
1194
+ } catch (error) {
1195
+ if (!figure.isConnected) return;
1196
+ diagram.textContent = "Diagram could not be rendered. Source is shown below.";
1197
+ details.open = true;
1198
+ console.warn("Mermaid diagram rendering failed", error);
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ function showMermaidDiagram(svg) {
1204
+ const copy = svg.cloneNode(true);
1205
+ copy.style.removeProperty("width");
1206
+ copy.style.removeProperty("max-width");
1207
+ copy.setAttribute("width", svg.viewBox.baseVal.width);
1208
+ copy.setAttribute("height", svg.viewBox.baseVal.height);
1209
+ const imageUrl = URL.createObjectURL(new Blob(
1210
+ [new XMLSerializer().serializeToString(copy)],
1211
+ {type: "image/svg+xml"},
1212
+ ));
1213
+ const dialog = document.createElement("dialog");
1214
+ dialog.className = "mermaid-viewer";
1215
+ dialog.setAttribute("aria-label", "Expanded diagram");
1216
+ const header = document.createElement("div");
1217
+ header.className = "mermaid-viewer-header";
1218
+ const title = document.createElement("strong");
1219
+ title.textContent = "Diagram";
1220
+ const zoom = document.createElement("button");
1221
+ zoom.type = "button";
1222
+ zoom.textContent = "Actual size";
1223
+ const close = document.createElement("button");
1224
+ close.type = "button";
1225
+ close.textContent = "Close";
1226
+ const content = document.createElement("div");
1227
+ content.className = "mermaid-viewer-content";
1228
+ const image = document.createElement("img");
1229
+ image.src = imageUrl;
1230
+ image.alt = "Diagram";
1231
+ content.appendChild(image);
1232
+ header.append(title, zoom, close);
1233
+ dialog.append(header, content);
1234
+ zoom.addEventListener("click", () => {
1235
+ const actualSize = content.classList.toggle("actual-size");
1236
+ zoom.textContent = actualSize ? "Fit width" : "Actual size";
1237
+ });
1238
+ close.addEventListener("click", () => dialog.close());
1239
+ dialog.addEventListener("click", event => {
1240
+ if (event.target === dialog) dialog.close();
1241
+ });
1242
+ dialog.addEventListener("close", () => {
1243
+ URL.revokeObjectURL(imageUrl);
1244
+ dialog.remove();
1245
+ }, {once: true});
1246
+ document.body.appendChild(dialog);
1247
+ dialog.showModal();
765
1248
  }
766
1249
 
767
1250
  function isEscapedAt(source, index) {
@@ -1168,6 +1651,7 @@
1168
1651
  draft: "",
1169
1652
  notifyWhenDone: false,
1170
1653
  wasRunning: false,
1654
+ inputRequest: null,
1171
1655
  });
1172
1656
  }
1173
1657
  return sessionState.get(key);
@@ -1199,92 +1683,206 @@
1199
1683
  state.wasRunning = isRunning;
1200
1684
  });
1201
1685
  sessions = incomingSessions;
1686
+ document.title = (sessions.some(session => session.running) ? "● " : "") + workspaceTitle;
1202
1687
  if (!activeSessionId && sessions.length) {
1203
1688
  activeSessionId = sessions[0].id || "";
1204
1689
  }
1205
1690
  renderTabs();
1691
+ updatePromptPlaceholder();
1206
1692
  updateContextMeter();
1207
1693
  updateSpinnerNotifyUi();
1208
1694
  }
1209
1695
 
1210
1696
  function renderTabs() {
1211
- tabsEl.textContent = "";
1697
+ const existingTabs = new Map(
1698
+ Array.from(tabsEl.children, tab => [tab.dataset.sessionId, tab])
1699
+ );
1212
1700
  sessions.forEach((session, index) => {
1213
1701
  const sessionId = session.id || "";
1214
- const tab = document.createElement("div");
1702
+ let tab = existingTabs.get(sessionId);
1703
+ if (!tab) {
1704
+ tab = document.createElement("div");
1705
+ tab.dataset.sessionId = sessionId;
1706
+ tab.setAttribute("role", "tab");
1707
+ tab.addEventListener("click", () => switchSession(sessionId));
1708
+
1709
+ const status = document.createElement("span");
1710
+ status.className = "tab-status";
1711
+ status.setAttribute("aria-hidden", "true");
1712
+ tab.appendChild(status);
1713
+
1714
+ const label = document.createElement("button");
1715
+ label.type = "button";
1716
+ label.className = "tab-label";
1717
+ label.addEventListener("click", event => {
1718
+ if (sessionId !== activeSessionId) return;
1719
+ event.stopPropagation();
1720
+ editSessionTitle(tab, sessionId);
1721
+ });
1722
+ tab.appendChild(label);
1723
+
1724
+ const close = document.createElement("button");
1725
+ close.type = "button";
1726
+ close.className = "tab-close";
1727
+ close.title = "Close session";
1728
+ close.setAttribute("aria-label", "Close session");
1729
+ close.innerHTML = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" ' +
1730
+ 'stroke-width="1.5" stroke-linecap="round" aria-hidden="true">' +
1731
+ '<path d="M4 4l8 8M12 4l-8 8"/></svg>';
1732
+ close.addEventListener("click", event => {
1733
+ event.stopPropagation();
1734
+ closeSession(sessionId);
1735
+ });
1736
+ tab.appendChild(close);
1737
+ }
1738
+ existingTabs.delete(sessionId);
1215
1739
  tab.className =
1216
1740
  "tab" +
1217
1741
  (sessionId === activeSessionId ? " active" : "") +
1218
1742
  (session.running ? " running" : "");
1219
1743
  tab.title = session.title || `Session ${index + 1}`;
1220
- tab.setAttribute("role", "tab");
1221
1744
  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);
1745
+ tab.setAttribute("aria-busy", String(Boolean(session.running)));
1746
+ tab.querySelector(".tab-status").hidden = !session.running;
1747
+ const label = tab.querySelector(".tab-label");
1748
+ label.textContent = session.title || `Session ${index + 1}`;
1749
+ label.title = sessionId === activeSessionId ? "Click to rename session" : tab.title;
1750
+ tab.querySelector(".tab-close").disabled = sessions.length <= 1;
1751
+ if (tabsEl.children[index] !== tab) {
1752
+ tabsEl.insertBefore(tab, tabsEl.children[index] || null);
1230
1753
  }
1754
+ });
1755
+ existingTabs.forEach(tab => tab.remove());
1756
+ }
1231
1757
 
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);
1758
+ function editSessionTitle(tab, sessionId) {
1759
+ const session = sessionById(sessionId);
1760
+ const label = tab.querySelector(".tab-label");
1761
+ const input = document.createElement("input");
1762
+ input.type = "text";
1763
+ input.className = "tab-title-input";
1764
+ input.setAttribute("aria-label", "Session title");
1765
+ input.value = session.title || "";
1766
+ input.placeholder = label.textContent;
1767
+ tab.style.width = `${tab.getBoundingClientRect().width}px`;
1768
+ label.hidden = true;
1769
+ tab.insertBefore(input, label);
1770
+ let finished = false;
1771
+ const finish = save => {
1772
+ if (finished) return;
1773
+ finished = true;
1774
+ const title = input.value.trim();
1775
+ input.remove();
1776
+ label.hidden = false;
1777
+ tab.style.width = "";
1778
+ if (save && title && title !== session.title) {
1779
+ renameSession(sessionId, title);
1780
+ }
1781
+ };
1782
+ input.addEventListener("click", event => event.stopPropagation());
1783
+ input.addEventListener("keydown", event => {
1784
+ if (event.isComposing) return;
1785
+ if (event.key === "Enter" || event.key === "Escape") {
1786
+ event.preventDefault();
1787
+ finish(event.key === "Enter");
1788
+ label.focus();
1789
+ }
1250
1790
  });
1791
+ input.addEventListener("blur", () => finish(true));
1792
+ input.focus();
1793
+ input.select();
1251
1794
  }
1252
1795
 
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;
1796
+ async function renameSession(sessionId, title) {
1797
+ try {
1798
+ const response = await fetch(relativeUrl("api/session/message"), {
1799
+ method: "POST",
1800
+ headers: {"Content-Type": "application/json"},
1801
+ body: JSON.stringify({prompt: `/title ${title}`, sender: "web", session_id: sessionId}),
1802
+ });
1803
+ const payload = await response.json();
1804
+ if (!response.ok || !payload.ok) {
1805
+ throw new Error(payload.error || `HTTP ${response.status}`);
1261
1806
  }
1807
+ const session = sessionById(sessionId);
1808
+ if (session) {
1809
+ session.title = payload.snapshot.title;
1810
+ renderTabs();
1811
+ }
1812
+ } catch (error) {
1813
+ addEntry("error", `Could not rename session: ${error.message}`, "error");
1262
1814
  }
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);
1815
+ }
1816
+
1817
+ function updatePromptPlaceholder(snapshot = null) {
1818
+ const session = snapshot || sessionById(activeSessionId);
1819
+ const label = session && session.model ? `Message to ${session.model}` : "Message";
1820
+ prompt.placeholder = `${label}…`;
1821
+ prompt.setAttribute("aria-label", label);
1822
+ }
1823
+
1824
+ function updateContextMeter(snapshot = null) {
1825
+ const session = snapshot || sessionById(activeSessionId);
1826
+ if (!session) return;
1827
+ const percent = session.context_remaining_percent;
1828
+ const compact = session.auto_compact_token_limit;
1829
+ const formatTokens = value => value == null ? "—" : value.toLocaleString("en-US");
1830
+ const remainingLabel = compact == null ? "context remaining" : "remaining until compact";
1831
+ contextMeter.style.setProperty("--context-remaining", `${percent == null ? 0 : percent}%`);
1832
+ contextMeterValue.textContent = percent == null ? "—" : percent >= 100 ? "" : String(percent);
1833
+ contextMeter.setAttribute("aria-label", percent == null
1834
+ ? "Context usage details"
1835
+ : `${percent}% ${remainingLabel}. Context usage details`);
1836
+ document.getElementById("contextCurrent").textContent = formatTokens(session.usage_tokens);
1837
+ document.getElementById("contextCompact").textContent = compact == null ? "Off" : formatTokens(compact);
1838
+ document.getElementById("contextMax").textContent = formatTokens(session.max_context_window);
1839
+ const note = compact == null
1840
+ ? "Auto-compact is off. The ring shows remaining context."
1841
+ : "The ring reaches 0% at the compact limit. Compaction runs before the next model request.";
1842
+ document.getElementById("contextNote").textContent = session.usage_tokens == null
1843
+ ? `Waiting for current usage. ${note}`
1844
+ : note;
1845
+ }
1846
+
1847
+ function setupContextDetails() {
1848
+ const control = document.getElementById("contextControl");
1849
+ const details = document.getElementById("contextDetails");
1850
+ let pinned = false;
1851
+ const show = visible => {
1852
+ details.hidden = !visible;
1853
+ contextMeter.setAttribute("aria-expanded", String(visible));
1854
+ };
1855
+ const close = () => {
1856
+ pinned = false;
1857
+ show(false);
1858
+ };
1859
+ control.addEventListener("pointerenter", event => {
1860
+ if (event.pointerType !== "touch") show(true);
1861
+ });
1862
+ control.addEventListener("pointerleave", () => {
1863
+ if (!pinned) show(false);
1864
+ });
1865
+ contextMeter.addEventListener("focus", () => show(true));
1866
+ control.addEventListener("focusout", event => {
1867
+ if (!pinned && !control.contains(event.relatedTarget)) show(false);
1868
+ });
1869
+ contextMeter.addEventListener("click", () => {
1870
+ pinned = !pinned;
1871
+ show(pinned);
1872
+ });
1873
+ document.addEventListener("pointerdown", event => {
1874
+ if (!control.contains(event.target)) close();
1875
+ });
1876
+ document.addEventListener("keydown", event => {
1877
+ if (event.key === "Escape") close();
1878
+ });
1879
+ tabsEl.addEventListener("click", close);
1880
+ newTabButton.addEventListener("click", close);
1283
1881
  }
1284
1882
 
1285
1883
  function messageSignature(snapshot) {
1286
1884
  const turns = (snapshot && snapshot.turns) || [];
1287
- return JSON.stringify(
1885
+ return JSON.stringify([
1288
1886
  turns.map((item) => [
1289
1887
  item.submission_id || "",
1290
1888
  item.turn_id || "",
@@ -1295,21 +1893,26 @@
1295
1893
  item.error || "",
1296
1894
  item.kind || "",
1297
1895
  ]),
1298
- );
1896
+ snapshot.input_request || null,
1897
+ ]);
1299
1898
  }
1300
1899
 
1301
1900
  async function pollSession() {
1302
- if (pollInFlight) return;
1303
- pollInFlight = true;
1304
1901
  const requestedSessionId = activeSessionId;
1902
+ if (sessionPoll) {
1903
+ if (sessionPoll.sessionId === requestedSessionId) return;
1904
+ sessionPoll.controller.abort();
1905
+ }
1906
+ const request = {sessionId: requestedSessionId, controller: new AbortController()};
1907
+ sessionPoll = request;
1305
1908
  try {
1306
1909
  const path = requestedSessionId
1307
1910
  ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
1308
1911
  : "api/session";
1309
- const response = await fetch(relativeUrl(path));
1912
+ const response = await fetch(relativeUrl(path), {signal: request.controller.signal});
1310
1913
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1311
1914
  const payload = await response.json();
1312
- if (requestedSessionId && requestedSessionId !== activeSessionId) {
1915
+ if (sessionPoll !== request || requestedSessionId !== activeSessionId) {
1313
1916
  return;
1314
1917
  }
1315
1918
  if (payload.session_id && payload.session_id !== activeSessionId) {
@@ -1318,9 +1921,11 @@
1318
1921
  updateSessions(payload.sessions || []);
1319
1922
  renderSnapshot(payload.snapshot);
1320
1923
  } catch (error) {
1321
- setSpinner(`poll error: ${error.message}`);
1924
+ if (sessionPoll === request && requestedSessionId === activeSessionId) {
1925
+ setSpinner(`poll error: ${error.message}`);
1926
+ }
1322
1927
  } finally {
1323
- pollInFlight = false;
1928
+ if (sessionPoll === request) sessionPoll = null;
1324
1929
  }
1325
1930
  }
1326
1931
 
@@ -1370,9 +1975,25 @@
1370
1975
  boardPollTimer = setInterval(pollBoard, 2000);
1371
1976
  }
1372
1977
 
1978
+ function renderQueueHint(inputs) {
1979
+ queueHint.hidden = inputs.length === 0;
1980
+ for (const [kind, label, badge] of [
1981
+ ["steer", "[soon]", steerHint],
1982
+ ["enqueue", "[on done]", enqueueHint],
1983
+ ]) {
1984
+ const text = inputs.filter(item => item.queue === kind).map(item => item.prompt).join("\n");
1985
+ badge.hidden = !text;
1986
+ badge.querySelector(".queue-preview").textContent = text;
1987
+ badge.title = label + "\n" + text;
1988
+ }
1989
+ }
1990
+
1373
1991
  function renderSnapshot(snapshot) {
1374
1992
  if (!snapshot) return;
1993
+ updatePromptPlaceholder(snapshot);
1375
1994
  const state = stateForSession(activeSessionId);
1995
+ state.inputRequest = snapshot.input_request || null;
1996
+ renderQueueHint(snapshot.queued_inputs);
1376
1997
  const signature = messageSignature(snapshot);
1377
1998
  if (signature === state.lastRenderedSignature) {
1378
1999
  setSpinner(snapshot.spinner || "");
@@ -1389,6 +2010,11 @@
1389
2010
  const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
1390
2011
  if (turns.length) {
1391
2012
  turns.forEach((turn) => renderTurn(turn, shouldStick));
2013
+ } else if (!state.inputRequest) {
2014
+ log.appendChild(document.getElementById("emptyChat").content.cloneNode(true));
2015
+ }
2016
+ if (state.inputRequest) {
2017
+ addEntry("input requested", state.inputRequest.text, "control", shouldStick);
1392
2018
  }
1393
2019
  setSpinner(snapshot.spinner || "");
1394
2020
  updateContextMeter(snapshot);
@@ -1423,12 +2049,11 @@
1423
2049
  );
1424
2050
  }
1425
2051
  if (turn.response) {
1426
- addMarkdownEntry(
1427
- isControl ? "result" : "response",
1428
- turn.response || "",
1429
- isControl ? "control" : "response",
1430
- shouldStick,
1431
- );
2052
+ if (isControl) {
2053
+ addEntry("result", turn.response, "control", shouldStick);
2054
+ } else {
2055
+ addMarkdownEntry("response", turn.response, "response", shouldStick);
2056
+ }
1432
2057
  }
1433
2058
  if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
1434
2059
  if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
@@ -1460,6 +2085,8 @@
1460
2085
  saveActiveSessionUiState();
1461
2086
  activeSessionId = sessionId;
1462
2087
  renderTabs();
2088
+ updatePromptPlaceholder();
2089
+ updateContextMeter();
1463
2090
  const state = stateForSession(sessionId);
1464
2091
  state.lastRenderedSignature = "";
1465
2092
  lastRenderedSignature = "";
@@ -1467,6 +2094,7 @@
1467
2094
  resizePrompt();
1468
2095
  suppressScrollSave = true;
1469
2096
  log.textContent = "";
2097
+ renderQueueHint([]);
1470
2098
  suppressScrollSave = false;
1471
2099
  setSpinner("", {suppressDoneNotification: true});
1472
2100
  restoreScrollTop = state.scrollTop;
@@ -1505,26 +2133,17 @@
1505
2133
  addEntry("error", payload.error || `HTTP ${response.status}`, "error");
1506
2134
  return;
1507
2135
  }
1508
- sessionState.delete(sessionId);
1509
2136
  updateSessions(payload.sessions || []);
1510
2137
  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();
2138
+ await switchSession(sessions[0].id);
1521
2139
  }
2140
+ sessionState.delete(sessionId);
1522
2141
  }
1523
2142
 
1524
2143
  form.addEventListener("submit", (event) => {
1525
2144
  event.preventDefault();
1526
2145
  const text = prompt.value.trim();
1527
- if (!text) return;
2146
+ if (!text && !stateForSession(activeSessionId).inputRequest) return;
1528
2147
  prompt.value = "";
1529
2148
  resizePrompt();
1530
2149
  stateForSession(activeSessionId).draft = "";
@@ -1565,6 +2184,12 @@
1565
2184
  splitter.addEventListener("dblclick", centerSplit);
1566
2185
  spinner.addEventListener("click", toggleSpinnerNotification);
1567
2186
  newTabButton.addEventListener("click", createSession);
2187
+ document.querySelectorAll("[data-pane]").forEach((button) => {
2188
+ button.addEventListener("click", () => {
2189
+ workspace.dataset.mobilePane = button.dataset.pane;
2190
+ if (button.dataset.pane === "chat") resizePrompt();
2191
+ });
2192
+ });
1568
2193
  window.addEventListener("resize", () => {
1569
2194
  const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
1570
2195
  if (current > 0) setChatWidth(current);
@@ -1577,6 +2202,7 @@
1577
2202
  });
1578
2203
 
1579
2204
  loadChatWidth();
2205
+ setupContextDetails();
1580
2206
  resizePrompt();
1581
2207
  startPolling();
1582
2208
  startBoardPolling();