python-codex 0.1.14__py3-none-any.whl → 0.2.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.
@@ -7,20 +7,38 @@
7
7
  <style>
8
8
  :root {
9
9
  color-scheme: light;
10
- --bg: #f5f5f2;
10
+ --bg: #f2f6f3;
11
11
  --panel: #ffffff;
12
- --ink: #1f2528;
13
- --muted: #667178;
14
- --line: #d8ddd8;
15
- --accent: #126b43;
12
+ --ink: #1e2529;
13
+ --muted: #5f6f75;
14
+ --line: #d2ddd5;
15
+ --accent: #0f8f55;
16
16
  --accent-ink: #ffffff;
17
- --code: #f6f8fa;
17
+ --warn-soft: #fff5cf;
18
+ --info: #1f6fa4;
19
+ --info-soft: #edf8ff;
20
+ --danger: #b23a3a;
21
+ --danger-soft: #fff1ef;
22
+ --violet: #6b4fb0;
23
+ --violet-soft: #f5f0ff;
24
+ --code: #f3f7f8;
25
+ --entry-shadow: 0 1px 2px rgba(25, 35, 31, 0.04);
26
+ --space-1: 2px;
27
+ --space-2: 4px;
28
+ --space-3: 6px;
29
+ --space-4: 8px;
30
+ --space-5: 10px;
31
+ --space-6: 12px;
32
+ --space-7: 14px;
33
+ --radius-sm: 4px;
34
+ --radius-md: 6px;
18
35
  --chat-width: min(42vw, 760px);
36
+ --splitter-width: 8px;
19
37
  }
20
38
  * { box-sizing: border-box; }
21
39
  body {
22
40
  margin: 0;
23
- height: 100vh;
41
+ height: 100dvh;
24
42
  overflow: hidden;
25
43
  background: var(--bg);
26
44
  color: var(--ink);
@@ -28,35 +46,68 @@
28
46
  }
29
47
  .workspace {
30
48
  display: grid;
31
- grid-template-columns: minmax(360px, 1fr) 8px minmax(340px, var(--chat-width));
32
- height: 100vh;
33
- min-width: 760px;
49
+ grid-template-columns: minmax(0, 1fr) var(--splitter-width) var(--chat-width);
50
+ height: 100dvh;
51
+ min-width: 0;
52
+ }
53
+ .workspace.collapsed .splitter::after {
54
+ background: #4f6f60;
55
+ }
56
+ .board-pane {
57
+ min-width: 0;
58
+ height: 100dvh;
59
+ display: grid;
60
+ grid-template-rows: auto minmax(0, 1fr);
61
+ background: #fff;
62
+ }
63
+ .boardbar {
64
+ min-width: 0;
65
+ border-bottom: 1px solid var(--line);
66
+ padding: var(--space-4) var(--space-6);
67
+ color: var(--muted);
68
+ font-size: 12px;
69
+ white-space: nowrap;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
34
72
  }
35
73
  .board {
36
74
  border: 0;
75
+ min-width: 0;
37
76
  width: 100%;
38
77
  height: 100%;
39
78
  background: #fff;
40
79
  }
41
80
  .splitter {
81
+ position: relative;
42
82
  width: 8px;
43
- height: 100vh;
83
+ height: 100dvh;
44
84
  border: 0;
45
- border-left: 1px solid var(--line);
46
- border-right: 1px solid var(--line);
47
- background: #eef1ed;
85
+ border-left: 1px solid #c1cec6;
86
+ border-right: 1px solid #c1cec6;
87
+ background: #dfe8e2;
48
88
  cursor: col-resize;
49
89
  padding: 0;
50
90
  touch-action: none;
51
91
  }
52
92
  .splitter::after {
53
93
  content: "";
54
- display: block;
55
- width: 2px;
94
+ position: absolute;
95
+ left: 50%;
96
+ top: 50%;
97
+ width: 3px;
56
98
  height: 42px;
57
- margin: calc(50vh - 21px) auto 0;
58
99
  border-radius: 999px;
59
- background: #b9c1ba;
100
+ background: #7d9588;
101
+ transform: translate(-50%, -50%);
102
+ }
103
+ .splitter:hover,
104
+ .workspace.resizing .splitter {
105
+ background: #d3dfd7;
106
+ border-color: #aebeb4;
107
+ }
108
+ .splitter:hover::after,
109
+ .workspace.resizing .splitter::after {
110
+ background: #527465;
60
111
  }
61
112
  .workspace.resizing,
62
113
  .workspace.resizing * {
@@ -64,45 +115,48 @@
64
115
  user-select: none;
65
116
  }
66
117
  .chat {
118
+ position: relative;
67
119
  min-width: 0;
68
- height: 100vh;
120
+ height: 100dvh;
69
121
  overflow: hidden;
70
122
  display: grid;
71
123
  grid-template-rows: auto minmax(0, 1fr) auto;
72
124
  background: var(--panel);
73
125
  }
74
126
  .topbar {
75
- padding: 12px 14px;
127
+ min-width: 0;
128
+ padding: var(--space-4) var(--space-7);
76
129
  border-bottom: 1px solid var(--line);
77
130
  display: grid;
78
- gap: 10px;
131
+ gap: var(--space-4);
79
132
  }
80
133
  .tabbar {
81
134
  display: flex;
82
135
  align-items: center;
83
- gap: 6px;
136
+ gap: var(--space-2);
84
137
  min-width: 0;
85
138
  }
86
139
  .tabs {
87
140
  display: flex;
88
141
  align-items: center;
89
- gap: 6px;
142
+ gap: var(--space-1);
90
143
  min-width: 0;
91
144
  overflow-x: auto;
92
145
  scrollbar-width: thin;
93
146
  }
94
147
  .tab {
148
+ position: relative;
95
149
  display: inline-flex;
96
150
  align-items: center;
97
- gap: 6px;
151
+ gap: var(--space-2);
98
152
  min-width: 88px;
99
153
  max-width: 180px;
100
- height: 28px;
101
- border: 1px solid var(--line);
102
- border-radius: 6px;
103
- background: #f8faf8;
154
+ height: 31px;
155
+ border: 1px solid #d5ddd7;
156
+ border-radius: var(--radius-sm);
157
+ background: #f6f8f7;
104
158
  color: var(--ink);
105
- padding: 0 7px;
159
+ padding: 0 var(--space-3) 0 var(--space-4);
106
160
  font: inherit;
107
161
  cursor: pointer;
108
162
  user-select: none;
@@ -113,8 +167,24 @@
113
167
  outline-offset: 1px;
114
168
  }
115
169
  .tab.active {
116
- border-color: #9bb7a6;
117
- background: #edf6f0;
170
+ border-color: #afd6bd;
171
+ background: #eef7f1;
172
+ box-shadow: inset 0 -2px 0 var(--accent);
173
+ }
174
+ .tab.running::before {
175
+ content: "";
176
+ position: absolute;
177
+ left: 6px;
178
+ top: 50%;
179
+ width: 5px;
180
+ height: 5px;
181
+ margin-top: -2.5px;
182
+ border-radius: 999px;
183
+ background: var(--accent);
184
+ animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
185
+ }
186
+ .tab.running .tab-label {
187
+ padding-left: var(--space-4);
118
188
  }
119
189
  .tab-label {
120
190
  min-width: 0;
@@ -126,11 +196,11 @@
126
196
  }
127
197
  .tab-close,
128
198
  .tab-new {
129
- width: 24px;
130
- height: 24px;
199
+ width: 31px;
200
+ height: 31px;
131
201
  border: 1px solid var(--line);
132
- border-radius: 6px;
133
- background: #fff;
202
+ border-radius: var(--radius-md);
203
+ background: #eef7f1;
134
204
  color: var(--muted);
135
205
  padding: 0;
136
206
  line-height: 1;
@@ -142,59 +212,80 @@
142
212
  height: 18px;
143
213
  border: 0;
144
214
  background: transparent;
215
+ opacity: 0.42;
216
+ font-size: 17px;
217
+ font-weight: 500;
218
+ line-height: 18px;
219
+ }
220
+ .tab-new {
221
+ font-size: 22px;
222
+ font-weight: 400;
223
+ line-height: 31px;
224
+ }
225
+ .tab:hover .tab-close,
226
+ .tab:focus-within .tab-close,
227
+ .tab.active .tab-close {
228
+ opacity: 0.85;
229
+ }
230
+ .tab-close:disabled {
231
+ opacity: 0.22;
232
+ cursor: default;
145
233
  }
146
234
  .tab-close:hover,
147
235
  .tab-new:hover {
148
236
  color: var(--ink);
149
- background: #f3f6f4;
150
- }
151
- .title { font-weight: 650; }
152
- .subtitle {
153
- color: var(--muted);
154
- font-size: 12px;
155
- overflow: hidden;
156
- text-overflow: ellipsis;
157
- white-space: nowrap;
237
+ background: #ffffff;
158
238
  }
159
239
  .log {
160
240
  min-height: 0;
161
241
  overflow-y: auto;
162
242
  overflow-x: hidden;
163
- padding: 14px;
243
+ padding: var(--space-6) var(--space-7);
164
244
  display: flex;
165
245
  flex-direction: column;
166
- gap: 10px;
246
+ gap: var(--space-4);
167
247
  background: #fbfcfa;
168
248
  }
169
249
  .entry {
250
+ min-width: 0;
170
251
  border: 1px solid var(--line);
171
- border-radius: 6px;
172
- padding: 10px 11px;
252
+ border-radius: var(--radius-sm);
253
+ padding: var(--space-4) var(--space-5);
173
254
  background: #fff;
255
+ box-shadow: var(--entry-shadow);
174
256
  }
175
257
  .entry.user {
176
- border-color: #e6d790;
177
- background: #fff9d8;
258
+ border-color: #edd17a;
259
+ background: var(--warn-soft);
260
+ color: #5a4714;
178
261
  }
179
262
  .entry.response,
180
263
  .entry.assistant {
181
- border-color: #d9ded9;
264
+ border-color: #d0ddd5;
182
265
  background: #ffffff;
183
266
  }
184
267
  .entry.thinking {
185
- border-color: #b9d9c7;
186
- background: #f5fbf6;
268
+ border-color: #b9dcc5;
269
+ background: #f0fbf3;
270
+ color: #256b43;
271
+ animation: thinking-breathe 4.8s ease-in-out infinite;
187
272
  }
188
273
  .entry.control {
189
- border-color: #c8d2dc;
190
- background: #f7fafd;
274
+ border-color: #b9d8ea;
275
+ background: var(--info-soft);
276
+ color: var(--info);
191
277
  }
192
278
  .entry.tool {
193
- border-color: #d7d1e6;
194
- background: #faf8ff;
195
- color: #4c4261;
279
+ border-color: #d7c8f0;
280
+ background: var(--violet-soft);
281
+ color: var(--violet);
196
282
  font-size: 12px;
197
283
  }
284
+ .entry.error {
285
+ border-color: #efb4ad;
286
+ background: var(--danger-soft);
287
+ color: var(--danger);
288
+ }
198
289
  .text {
199
290
  white-space: pre-wrap;
200
291
  overflow-wrap: anywhere;
@@ -204,7 +295,7 @@
204
295
  overflow-wrap: anywhere;
205
296
  }
206
297
  .markdown p {
207
- margin: 0 0 8px;
298
+ margin: 0 0 var(--space-4);
208
299
  }
209
300
  .markdown p:last-child,
210
301
  .markdown ul:last-child,
@@ -215,17 +306,17 @@
215
306
  }
216
307
  .markdown ul,
217
308
  .markdown ol {
218
- margin: 0 0 8px 18px;
309
+ margin: 0 0 var(--space-4) 18px;
219
310
  padding: 0;
220
311
  }
221
312
  .markdown li {
222
313
  margin: 2px 0;
223
314
  }
224
315
  .markdown pre {
225
- margin: 0 0 8px;
226
- padding: 9px 10px;
316
+ margin: 0 0 var(--space-4);
317
+ padding: var(--space-4) var(--space-5);
227
318
  border: 1px solid var(--line);
228
- border-radius: 6px;
319
+ border-radius: var(--radius-sm);
229
320
  background: var(--code);
230
321
  overflow-x: auto;
231
322
  white-space: pre;
@@ -238,45 +329,56 @@
238
329
  }
239
330
  .markdown table {
240
331
  width: 100%;
241
- margin: 0 0 8px;
332
+ margin: 0 0 var(--space-4);
242
333
  border-collapse: collapse;
243
334
  font-size: 13px;
244
335
  }
245
336
  .markdown th,
246
337
  .markdown td {
247
338
  border: 1px solid var(--line);
248
- padding: 5px 7px;
339
+ padding: var(--space-3) var(--space-4);
249
340
  text-align: left;
250
341
  vertical-align: top;
251
342
  }
252
343
  .markdown th {
253
- background: #f3f6f4;
344
+ background: #eef7f1;
254
345
  font-weight: 650;
255
346
  }
256
347
  .markdown a {
257
- color: #0b6bcb;
348
+ color: #176fa8;
258
349
  text-decoration: none;
259
350
  }
260
351
  .markdown a:hover {
261
352
  text-decoration: underline;
262
353
  }
263
354
  .composer {
355
+ min-width: 0;
264
356
  border-top: 1px solid var(--line);
265
- padding: 12px;
357
+ padding: var(--space-4) var(--space-6) var(--space-5);
266
358
  display: grid;
267
- gap: 8px;
359
+ gap: var(--space-3);
360
+ align-content: end;
268
361
  background: #fff;
269
362
  }
270
363
  textarea {
364
+ min-width: 0;
271
365
  resize: vertical;
272
- min-height: 88px;
366
+ min-height: 40px;
273
367
  max-height: 35vh;
274
368
  width: 100%;
275
369
  border: 1px solid var(--line);
276
- border-radius: 6px;
277
- padding: 10px;
370
+ border-radius: var(--radius-sm);
371
+ padding: var(--space-4) var(--space-5);
372
+ background: #ffffff;
373
+ color: var(--ink);
278
374
  font: inherit;
279
375
  line-height: 1.4;
376
+ overflow-y: auto;
377
+ }
378
+ textarea:focus {
379
+ border-color: #65b782;
380
+ box-shadow: 0 0 0 3px rgba(15, 143, 85, 0.13);
381
+ outline: 0;
280
382
  }
281
383
  .actions {
282
384
  display: flex;
@@ -284,51 +386,107 @@
284
386
  align-items: center;
285
387
  }
286
388
  .spinner {
287
- min-height: 18px;
389
+ position: relative;
390
+ display: inline-flex;
391
+ align-items: center;
392
+ justify-self: start;
393
+ align-self: end;
394
+ max-width: 100%;
395
+ min-height: 20px;
288
396
  color: var(--muted);
289
397
  font-size: 12px;
290
398
  white-space: nowrap;
291
399
  overflow: hidden;
292
400
  text-overflow: ellipsis;
401
+ padding: var(--space-1) var(--space-4) var(--space-1) 18px;
402
+ border-radius: 999px;
403
+ background: rgba(240, 250, 244, 0.96);
404
+ border: 1px solid rgba(172, 216, 188, 0.9);
405
+ }
406
+ .spinner:empty {
407
+ display: none;
408
+ }
409
+ .spinner:not(:empty) {
410
+ margin-bottom: var(--space-1);
411
+ }
412
+ .spinner:not(:empty)::before {
413
+ content: "";
414
+ position: absolute;
415
+ left: 7px;
416
+ top: 50%;
417
+ width: 6px;
418
+ height: 6px;
419
+ margin-top: -3px;
420
+ border-radius: 999px;
421
+ background: var(--accent);
422
+ animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
423
+ }
424
+ @keyframes spinner-pulse {
425
+ 0%, 100% {
426
+ opacity: 0.45;
427
+ transform: scale(0.82);
428
+ }
429
+ 50% {
430
+ opacity: 1;
431
+ transform: scale(1.08);
432
+ }
433
+ }
434
+ @keyframes thinking-breathe {
435
+ 0%, 100% {
436
+ border-color: #b9dcc5;
437
+ box-shadow: var(--entry-shadow);
438
+ }
439
+ 50% {
440
+ border-color: #72d494;
441
+ box-shadow:
442
+ 0 0 0 2px rgba(22, 168, 90, 0.16),
443
+ 0 2px 7px rgba(25, 35, 31, 0.08);
444
+ }
445
+ }
446
+ @media (prefers-reduced-motion: reduce) {
447
+ .entry.thinking,
448
+ .tab.running::before,
449
+ .spinner:not(:empty)::before {
450
+ animation: none;
451
+ }
452
+ .tab.running::before,
453
+ .spinner:not(:empty)::before {
454
+ opacity: 0.8;
455
+ transform: none;
456
+ }
293
457
  }
294
458
  code {
295
459
  background: var(--code);
296
460
  border: 1px solid var(--line);
297
- border-radius: 4px;
461
+ border-radius: var(--radius-sm);
298
462
  padding: 1px 4px;
299
463
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
300
464
  font-size: 12px;
301
465
  }
302
466
  @media (max-width: 900px) {
303
467
  body { overflow: auto; height: auto; }
304
- .workspace { display: block; min-width: 0; height: auto; }
305
- .board { height: 56vh; border-right: 0; border-bottom: 1px solid var(--line); }
306
- .splitter { display: none; }
307
- .chat { min-height: 44vh; }
468
+ .workspace { min-width: 0; }
308
469
  }
309
470
  </style>
310
471
  </head>
311
472
  <body>
312
473
  <div class="workspace">
313
- <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
474
+ <section class="board-pane" aria-label="workspace board">
475
+ <div class="boardbar">Board: <code>__BOARD_LABEL__</code></div>
476
+ <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
477
+ </section>
314
478
  <button id="splitter" class="splitter" type="button" aria-label="Resize chat panel"></button>
315
479
  <section class="chat" aria-label="pycodex chat">
316
480
  <div class="topbar">
317
481
  <div class="tabbar">
318
- <div id="tabs" class="tabs"></div>
482
+ <div id="tabs" class="tabs" role="tablist"></div>
319
483
  <button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">+</button>
320
484
  </div>
321
- <div>
322
- <div class="title">pycodex</div>
323
- <div class="subtitle">Board: <code>__BOARD_LABEL__</code></div>
324
- </div>
325
485
  </div>
326
486
  <div id="log" class="log"></div>
327
487
  <form id="composer" class="composer">
488
+ <div id="spinner" class="spinner" role="status" aria-live="polite"></div>
328
489
  <textarea id="prompt" placeholder="Ask pycodex or type /help..."></textarea>
329
- <div class="actions">
330
- <div id="spinner" class="spinner"></div>
331
- </div>
332
490
  </form>
333
491
  </section>
334
492
  </div>
@@ -346,17 +504,18 @@
346
504
  const newTabButton = document.getElementById("newTab");
347
505
  let pollTimer = null;
348
506
  let boardPollTimer = null;
507
+ let pollInFlight = false;
508
+ let boardPollInFlight = false;
349
509
  let lastBoardSignature = "";
350
510
  let lastRenderedSignature = "";
351
511
  let resizeStart = null;
352
- let spinnerTimer = null;
353
- let spinnerFrameIndex = 0;
354
512
  let spinnerText = "";
355
513
  let activeSessionId = "";
356
514
  let sessions = [];
515
+ let isComposing = false;
357
516
  const sessionState = new Map();
358
517
  let restoreScrollTop = null;
359
- const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
518
+ let suppressScrollSave = false;
360
519
  const markdownRenderer = window.markdownit
361
520
  ? window.markdownit({html: false, linkify: true, breaks: false})
362
521
  : null;
@@ -412,17 +571,36 @@
412
571
  if (stored > 0) setChatWidth(stored);
413
572
  }
414
573
 
574
+ function centerSplit() {
575
+ setChatWidth(Math.max(0, (window.innerWidth - 8) / 2));
576
+ }
577
+
415
578
  function setChatWidth(width) {
416
579
  const viewportWidth = window.innerWidth || 0;
417
- const minWidth = Math.min(340, Math.max(260, viewportWidth - 420));
418
- const maxWidth = Math.max(minWidth, viewportWidth - 420);
419
- const nextWidth = Math.max(minWidth, Math.min(maxWidth, Math.round(width)));
580
+ const splitterWidth = 8;
581
+ const gutterWidth = Math.min(24, Math.max(12, Math.round(viewportWidth * 0.06)));
582
+ const availableWidth = Math.max(0, viewportWidth - splitterWidth);
583
+ const maxChatWidth = Math.max(gutterWidth, availableWidth - gutterWidth);
584
+ const collapseWidth = Math.min(
585
+ 280,
586
+ Math.max(120, Math.round(viewportWidth * 0.25)),
587
+ );
588
+ let nextWidth = Math.max(gutterWidth, Math.min(maxChatWidth, Math.round(width)));
589
+ let collapsed = false;
590
+ if (nextWidth < collapseWidth) {
591
+ nextWidth = gutterWidth;
592
+ collapsed = true;
593
+ } else if (availableWidth - nextWidth < collapseWidth) {
594
+ nextWidth = maxChatWidth;
595
+ collapsed = true;
596
+ }
420
597
  workspace.style.setProperty("--chat-width", `${nextWidth}px`);
598
+ workspace.classList.toggle("collapsed", collapsed);
421
599
  window.localStorage.setItem("pycodex.workspace.chatWidth", String(nextWidth));
422
600
  }
423
601
 
424
602
  function startResize(event) {
425
- if (!workspace || window.innerWidth <= 900) return;
603
+ if (!workspace) return;
426
604
  const chatWidth = document.querySelector(".chat").getBoundingClientRect().width;
427
605
  resizeStart = { pointerId: event.pointerId, startX: event.clientX, startWidth: chatWidth };
428
606
  splitter.setPointerCapture(event.pointerId);
@@ -451,25 +629,7 @@
451
629
 
452
630
  function setSpinner(text) {
453
631
  spinnerText = String(text || "").trim();
454
- spinnerFrameIndex = 0;
455
- renderSpinner();
456
- if (spinnerText && !spinnerTimer) {
457
- spinnerTimer = setInterval(renderSpinner, 120);
458
- }
459
- if (!spinnerText && spinnerTimer) {
460
- clearInterval(spinnerTimer);
461
- spinnerTimer = null;
462
- }
463
- }
464
-
465
- function renderSpinner() {
466
- if (!spinnerText) {
467
- spinner.textContent = "";
468
- return;
469
- }
470
- const frame = spinnerFrames[spinnerFrameIndex % spinnerFrames.length];
471
- spinnerFrameIndex += 1;
472
- spinner.textContent = `${frame} ${spinnerText}`;
632
+ spinner.textContent = spinnerText;
473
633
  }
474
634
 
475
635
  function isNearBottom() {
@@ -480,10 +640,18 @@
480
640
  log.scrollTop = log.scrollHeight;
481
641
  }
482
642
 
643
+ function resizePrompt() {
644
+ prompt.style.height = "auto";
645
+ const minHeight = 40;
646
+ const maxHeight = Math.max(minHeight, Math.round(window.innerHeight * 0.35));
647
+ const nextHeight = Math.max(minHeight, Math.min(prompt.scrollHeight, maxHeight));
648
+ prompt.style.height = `${nextHeight}px`;
649
+ }
650
+
483
651
  function stateForSession(sessionId) {
484
652
  const key = String(sessionId || "");
485
653
  if (!sessionState.has(key)) {
486
- sessionState.set(key, {lastRenderedSignature: "", scrollTop: 0, draft: ""});
654
+ sessionState.set(key, {lastRenderedSignature: "", scrollTop: null, draft: ""});
487
655
  }
488
656
  return sessionState.get(key);
489
657
  }
@@ -491,7 +659,9 @@
491
659
  function saveActiveSessionUiState() {
492
660
  if (!activeSessionId) return;
493
661
  const state = stateForSession(activeSessionId);
494
- state.scrollTop = log.scrollTop;
662
+ if (log.scrollHeight > log.clientHeight) {
663
+ state.scrollTop = log.scrollTop;
664
+ }
495
665
  state.draft = prompt.value;
496
666
  state.lastRenderedSignature = lastRenderedSignature;
497
667
  }
@@ -509,10 +679,14 @@
509
679
  sessions.forEach((session, index) => {
510
680
  const sessionId = session.id || "";
511
681
  const tab = document.createElement("div");
512
- tab.className = "tab" + (sessionId === activeSessionId ? " active" : "");
682
+ tab.className =
683
+ "tab" +
684
+ (sessionId === activeSessionId ? " active" : "") +
685
+ (session.running ? " running" : "");
513
686
  tab.title = session.title || `Session ${index + 1}`;
514
687
  tab.setAttribute("role", "tab");
515
688
  tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
689
+ if (session.running) tab.setAttribute("aria-busy", "true");
516
690
  tab.addEventListener("click", () => switchSession(sessionId));
517
691
 
518
692
  const label = document.createElement("span");
@@ -538,11 +712,8 @@
538
712
 
539
713
  function messageSignature(snapshot) {
540
714
  const turns = (snapshot && snapshot.turns) || [];
541
- return JSON.stringify({
542
- running: !!snapshot.running,
543
- spinner: snapshot.spinner || "",
544
- title: snapshot.title || "",
545
- turns: turns.map((item) => [
715
+ return JSON.stringify(
716
+ turns.map((item) => [
546
717
  item.submission_id || "",
547
718
  item.turn_id || "",
548
719
  item.prompt || "",
@@ -552,17 +723,23 @@
552
723
  item.error || "",
553
724
  item.kind || "",
554
725
  ]),
555
- });
726
+ );
556
727
  }
557
728
 
558
729
  async function pollSession() {
730
+ if (pollInFlight) return;
731
+ pollInFlight = true;
732
+ const requestedSessionId = activeSessionId;
559
733
  try {
560
- const path = activeSessionId
561
- ? `api/session?session_id=${encodeURIComponent(activeSessionId)}`
734
+ const path = requestedSessionId
735
+ ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
562
736
  : "api/session";
563
737
  const response = await fetch(relativeUrl(path));
564
738
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
565
739
  const payload = await response.json();
740
+ if (requestedSessionId && requestedSessionId !== activeSessionId) {
741
+ return;
742
+ }
566
743
  if (payload.session_id && payload.session_id !== activeSessionId) {
567
744
  activeSessionId = payload.session_id;
568
745
  }
@@ -570,6 +747,8 @@
570
747
  renderSnapshot(payload.snapshot);
571
748
  } catch (error) {
572
749
  setSpinner(`poll error: ${error.message}`);
750
+ } finally {
751
+ pollInFlight = false;
573
752
  }
574
753
  }
575
754
 
@@ -586,6 +765,8 @@
586
765
  }
587
766
 
588
767
  async function pollBoard() {
768
+ if (boardPollInFlight) return;
769
+ boardPollInFlight = true;
589
770
  try {
590
771
  const response = await fetch(relativeUrl("api/board"));
591
772
  if (!response.ok) return;
@@ -602,6 +783,8 @@
602
783
  }
603
784
  } catch (_error) {
604
785
  return;
786
+ } finally {
787
+ boardPollInFlight = false;
605
788
  }
606
789
  }
607
790
 
@@ -623,25 +806,30 @@
623
806
  lastRenderedSignature = signature;
624
807
  const shouldStick = isNearBottom();
625
808
  const previousScrollTop = log.scrollTop;
626
- log.textContent = "";
627
- const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
628
- if (turns.length) {
629
- turns.forEach((turn) => renderTurn(turn, shouldStick));
630
- }
631
- setSpinner(snapshot.spinner || "");
632
- if (restoreScrollTop !== null) {
633
- log.scrollTop = Math.min(
634
- restoreScrollTop,
635
- Math.max(0, log.scrollHeight - log.clientHeight),
636
- );
637
- restoreScrollTop = null;
638
- } else if (shouldStick) {
639
- scrollToBottom();
640
- } else {
641
- log.scrollTop = Math.min(
642
- previousScrollTop,
643
- Math.max(0, log.scrollHeight - log.clientHeight),
644
- );
809
+ suppressScrollSave = true;
810
+ try {
811
+ log.textContent = "";
812
+ const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
813
+ if (turns.length) {
814
+ turns.forEach((turn) => renderTurn(turn, shouldStick));
815
+ }
816
+ setSpinner(snapshot.spinner || "");
817
+ if (restoreScrollTop !== null) {
818
+ log.scrollTop = Math.min(
819
+ restoreScrollTop,
820
+ Math.max(0, log.scrollHeight - log.clientHeight),
821
+ );
822
+ restoreScrollTop = null;
823
+ } else if (shouldStick) {
824
+ scrollToBottom();
825
+ } else {
826
+ log.scrollTop = Math.min(
827
+ previousScrollTop,
828
+ Math.max(0, log.scrollHeight - log.clientHeight),
829
+ );
830
+ }
831
+ } finally {
832
+ suppressScrollSave = false;
645
833
  }
646
834
  state.scrollTop = log.scrollTop;
647
835
  }
@@ -665,7 +853,7 @@
665
853
  );
666
854
  }
667
855
  if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
668
- if (turn.error) addEntry("error", turn.error || "", "tool", shouldStick);
856
+ if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
669
857
  }
670
858
 
671
859
  async function submitPrompt(text) {
@@ -676,7 +864,7 @@
676
864
  });
677
865
  const payload = await response.json();
678
866
  if (!response.ok || !payload.ok) {
679
- addEntry("error", payload.error || `HTTP ${response.status}`, "tool");
867
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
680
868
  setSpinner("");
681
869
  return;
682
870
  }
@@ -697,9 +885,12 @@
697
885
  state.lastRenderedSignature = "";
698
886
  lastRenderedSignature = "";
699
887
  prompt.value = state.draft || "";
888
+ resizePrompt();
889
+ suppressScrollSave = true;
700
890
  log.textContent = "";
891
+ suppressScrollSave = false;
701
892
  setSpinner("");
702
- restoreScrollTop = state.scrollTop || 0;
893
+ restoreScrollTop = state.scrollTop;
703
894
  await pollSession();
704
895
  renderTabs();
705
896
  }
@@ -709,17 +900,18 @@
709
900
  const response = await fetch(relativeUrl("api/sessions"), {method: "POST"});
710
901
  const payload = await response.json();
711
902
  if (!response.ok || !payload.ok) {
712
- addEntry("error", payload.error || `HTTP ${response.status}`, "tool");
903
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
713
904
  return;
714
905
  }
715
906
  activeSessionId = payload.session_id || "";
716
907
  const state = stateForSession(activeSessionId);
717
908
  state.draft = "";
718
- state.scrollTop = 0;
909
+ state.scrollTop = null;
719
910
  state.lastRenderedSignature = "";
720
911
  lastRenderedSignature = "";
721
912
  prompt.value = "";
722
- restoreScrollTop = 0;
913
+ resizePrompt();
914
+ restoreScrollTop = null;
723
915
  updateSessions(payload.sessions || []);
724
916
  renderSnapshot(payload.snapshot);
725
917
  }
@@ -731,7 +923,7 @@
731
923
  });
732
924
  const payload = await response.json();
733
925
  if (!response.ok || !payload.ok) {
734
- addEntry("error", payload.error || `HTTP ${response.status}`, "tool");
926
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
735
927
  return;
736
928
  }
737
929
  sessionState.delete(sessionId);
@@ -740,7 +932,10 @@
740
932
  activeSessionId = sessions.length ? sessions[0].id || "" : "";
741
933
  lastRenderedSignature = "";
742
934
  prompt.value = "";
935
+ resizePrompt();
936
+ suppressScrollSave = true;
743
937
  log.textContent = "";
938
+ suppressScrollSave = false;
744
939
  await pollSession();
745
940
  } else {
746
941
  renderTabs();
@@ -752,20 +947,32 @@
752
947
  const text = prompt.value.trim();
753
948
  if (!text) return;
754
949
  prompt.value = "";
950
+ resizePrompt();
755
951
  stateForSession(activeSessionId).draft = "";
756
952
  submitPrompt(text);
757
953
  });
758
954
 
759
955
  prompt.addEventListener("input", () => {
760
956
  stateForSession(activeSessionId).draft = prompt.value;
957
+ resizePrompt();
958
+ });
959
+
960
+ prompt.addEventListener("compositionstart", () => {
961
+ isComposing = true;
962
+ });
963
+
964
+ prompt.addEventListener("compositionend", () => {
965
+ isComposing = false;
761
966
  });
762
967
 
763
968
  log.addEventListener("scroll", () => {
969
+ if (suppressScrollSave) return;
764
970
  if (!activeSessionId) return;
765
971
  stateForSession(activeSessionId).scrollTop = log.scrollTop;
766
972
  });
767
973
 
768
974
  prompt.addEventListener("keydown", (event) => {
975
+ if (event.isComposing || isComposing || event.keyCode === 229) return;
769
976
  if (event.key === "Enter" && !event.shiftKey) {
770
977
  event.preventDefault();
771
978
  form.requestSubmit();
@@ -776,13 +983,16 @@
776
983
  splitter.addEventListener("pointermove", updateResize);
777
984
  splitter.addEventListener("pointerup", endResize);
778
985
  splitter.addEventListener("pointercancel", endResize);
986
+ splitter.addEventListener("dblclick", centerSplit);
779
987
  newTabButton.addEventListener("click", createSession);
780
988
  window.addEventListener("resize", () => {
781
989
  const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
782
990
  if (current > 0) setChatWidth(current);
991
+ resizePrompt();
783
992
  });
784
993
 
785
994
  loadChatWidth();
995
+ resizePrompt();
786
996
  startPolling();
787
997
  startBoardPolling();
788
998
  </script>