python-codex 0.1.13__py3-none-any.whl → 0.2.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 (50) hide show
  1. pycodex/agent.py +71 -11
  2. pycodex/cli.py +16 -356
  3. pycodex/context.py +12 -0
  4. pycodex/feishu_card.py +76 -30
  5. pycodex/feishu_link.py +131 -11
  6. pycodex/interactive_session.py +397 -0
  7. pycodex/model.py +11 -22
  8. pycodex/protocol.py +0 -5
  9. pycodex/runtime.py +23 -0
  10. pycodex/runtime_services.py +2 -2
  11. pycodex/tools/agent_tool_schemas.py +1 -1
  12. pycodex/tools/apply_patch_tool.py +1 -1
  13. pycodex/tools/base_tool.py +1 -27
  14. pycodex/tools/close_agent_tool.py +11 -4
  15. pycodex/tools/code_mode_manager.py +1 -1
  16. pycodex/tools/exec_command_tool.py +40 -16
  17. pycodex/tools/exec_tool.py +18 -2
  18. pycodex/tools/grep_files_tool.py +19 -6
  19. pycodex/tools/ipython_tool.py +3 -2
  20. pycodex/tools/list_dir_tool.py +19 -6
  21. pycodex/tools/read_file_tool.py +39 -9
  22. pycodex/tools/request_permissions_tool.py +12 -1
  23. pycodex/tools/request_user_input_tool.py +28 -1
  24. pycodex/tools/send_input_tool.py +4 -2
  25. pycodex/tools/shell_command_tool.py +23 -6
  26. pycodex/tools/shell_tool.py +13 -4
  27. pycodex/tools/spawn_agent_tool.py +31 -8
  28. pycodex/tools/unified_exec_manager.py +49 -93
  29. pycodex/tools/update_plan_tool.py +14 -6
  30. pycodex/tools/view_image_tool.py +17 -16
  31. pycodex/tools/wait_agent_tool.py +15 -3
  32. pycodex/tools/wait_tool.py +18 -4
  33. pycodex/tools/web_search_tool.py +2 -1
  34. pycodex/tools/write_stdin_tool.py +42 -10
  35. pycodex/utils/compactor.py +7 -1
  36. pycodex/utils/session_persist.py +42 -1
  37. pycodex/utils/truncation.py +206 -0
  38. pycodex/utils/visualize.py +34 -15
  39. {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/METADATA +4 -1
  40. python_codex-0.2.0.dist-info/RECORD +88 -0
  41. {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/entry_points.txt +1 -0
  42. workspace_server/__init__.py +23 -0
  43. workspace_server/__main__.py +5 -0
  44. workspace_server/app.py +1347 -0
  45. workspace_server/workspace.html +866 -0
  46. pycodex/prompts/exec_tools.json +0 -411
  47. pycodex/prompts/subagent_tools.json +0 -163
  48. python_codex-0.1.13.dist-info/RECORD +0 -84
  49. {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/WHEEL +0 -0
  50. {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,866 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>pycodex workspace</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --bg: #f5f5f2;
11
+ --panel: #ffffff;
12
+ --ink: #1f2528;
13
+ --muted: #667178;
14
+ --line: #d8ddd8;
15
+ --accent: #126b43;
16
+ --accent-ink: #ffffff;
17
+ --code: #f6f8fa;
18
+ --chat-width: min(42vw, 760px);
19
+ --splitter-width: 8px;
20
+ }
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0;
24
+ height: 100dvh;
25
+ overflow: hidden;
26
+ background: var(--bg);
27
+ color: var(--ink);
28
+ font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
29
+ }
30
+ .workspace {
31
+ display: grid;
32
+ grid-template-columns: minmax(0, 1fr) var(--splitter-width) var(--chat-width);
33
+ height: 100dvh;
34
+ min-width: 0;
35
+ }
36
+ .workspace.collapsed .splitter::after {
37
+ background: #7e9187;
38
+ }
39
+ .board-pane {
40
+ min-width: 0;
41
+ height: 100dvh;
42
+ display: grid;
43
+ grid-template-rows: auto minmax(0, 1fr);
44
+ background: #fff;
45
+ }
46
+ .boardbar {
47
+ min-width: 0;
48
+ border-bottom: 1px solid var(--line);
49
+ padding: 8px 12px;
50
+ color: var(--muted);
51
+ font-size: 12px;
52
+ white-space: nowrap;
53
+ overflow: hidden;
54
+ text-overflow: ellipsis;
55
+ }
56
+ .board {
57
+ border: 0;
58
+ min-width: 0;
59
+ width: 100%;
60
+ height: 100%;
61
+ background: #fff;
62
+ }
63
+ .splitter {
64
+ width: 8px;
65
+ height: 100dvh;
66
+ border: 0;
67
+ border-left: 1px solid var(--line);
68
+ border-right: 1px solid var(--line);
69
+ background: #eef1ed;
70
+ cursor: col-resize;
71
+ padding: 0;
72
+ touch-action: none;
73
+ }
74
+ .splitter::after {
75
+ content: "";
76
+ display: block;
77
+ width: 2px;
78
+ height: 42px;
79
+ margin: calc(50dvh - 21px) auto 0;
80
+ border-radius: 999px;
81
+ background: #b9c1ba;
82
+ }
83
+ .workspace.resizing,
84
+ .workspace.resizing * {
85
+ cursor: col-resize;
86
+ user-select: none;
87
+ }
88
+ .chat {
89
+ position: relative;
90
+ min-width: 0;
91
+ height: 100dvh;
92
+ overflow: hidden;
93
+ display: grid;
94
+ grid-template-rows: auto minmax(0, 1fr) auto;
95
+ background: var(--panel);
96
+ }
97
+ .topbar {
98
+ min-width: 0;
99
+ padding: 12px 14px;
100
+ border-bottom: 1px solid var(--line);
101
+ display: grid;
102
+ gap: 10px;
103
+ }
104
+ .tabbar {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 6px;
108
+ min-width: 0;
109
+ }
110
+ .tabs {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 6px;
114
+ min-width: 0;
115
+ overflow-x: auto;
116
+ scrollbar-width: thin;
117
+ }
118
+ .tab {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ gap: 6px;
122
+ min-width: 88px;
123
+ max-width: 180px;
124
+ height: 28px;
125
+ border: 1px solid var(--line);
126
+ border-radius: 6px;
127
+ background: #f8faf8;
128
+ color: var(--ink);
129
+ padding: 0 7px;
130
+ font: inherit;
131
+ cursor: pointer;
132
+ user-select: none;
133
+ }
134
+ .tab:focus-within,
135
+ .tab-new:focus {
136
+ outline: 2px solid #9bb7a6;
137
+ outline-offset: 1px;
138
+ }
139
+ .tab.active {
140
+ border-color: #9bb7a6;
141
+ background: #edf6f0;
142
+ }
143
+ .tab-label {
144
+ min-width: 0;
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ white-space: nowrap;
148
+ flex: 1;
149
+ text-align: left;
150
+ }
151
+ .tab-close,
152
+ .tab-new {
153
+ width: 24px;
154
+ height: 24px;
155
+ border: 1px solid var(--line);
156
+ border-radius: 6px;
157
+ background: #fff;
158
+ color: var(--muted);
159
+ padding: 0;
160
+ line-height: 1;
161
+ cursor: pointer;
162
+ flex: 0 0 auto;
163
+ }
164
+ .tab-close {
165
+ width: 18px;
166
+ height: 18px;
167
+ border: 0;
168
+ background: transparent;
169
+ }
170
+ .tab-close:hover,
171
+ .tab-new:hover {
172
+ color: var(--ink);
173
+ background: #f3f6f4;
174
+ }
175
+ .log {
176
+ min-height: 0;
177
+ overflow-y: auto;
178
+ overflow-x: hidden;
179
+ padding: 14px;
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 10px;
183
+ background: #fbfcfa;
184
+ }
185
+ .entry {
186
+ min-width: 0;
187
+ border: 1px solid var(--line);
188
+ border-radius: 6px;
189
+ padding: 10px 11px;
190
+ background: #fff;
191
+ }
192
+ .entry.user {
193
+ border-color: #e6d790;
194
+ background: #fff9d8;
195
+ }
196
+ .entry.response,
197
+ .entry.assistant {
198
+ border-color: #d9ded9;
199
+ background: #ffffff;
200
+ }
201
+ .entry.thinking {
202
+ border-color: #b9d9c7;
203
+ background: #f5fbf6;
204
+ }
205
+ .entry.control {
206
+ border-color: #c8d2dc;
207
+ background: #f7fafd;
208
+ }
209
+ .entry.tool {
210
+ border-color: #d7d1e6;
211
+ background: #faf8ff;
212
+ color: #4c4261;
213
+ font-size: 12px;
214
+ }
215
+ .entry.error {
216
+ border-color: #e4b6b6;
217
+ background: #fff7f7;
218
+ color: #7a2c2c;
219
+ }
220
+ .text {
221
+ white-space: pre-wrap;
222
+ overflow-wrap: anywhere;
223
+ }
224
+ .markdown {
225
+ white-space: normal;
226
+ overflow-wrap: anywhere;
227
+ }
228
+ .markdown p {
229
+ margin: 0 0 8px;
230
+ }
231
+ .markdown p:last-child,
232
+ .markdown ul:last-child,
233
+ .markdown ol:last-child,
234
+ .markdown pre:last-child,
235
+ .markdown table:last-child {
236
+ margin-bottom: 0;
237
+ }
238
+ .markdown ul,
239
+ .markdown ol {
240
+ margin: 0 0 8px 18px;
241
+ padding: 0;
242
+ }
243
+ .markdown li {
244
+ margin: 2px 0;
245
+ }
246
+ .markdown pre {
247
+ margin: 0 0 8px;
248
+ padding: 9px 10px;
249
+ border: 1px solid var(--line);
250
+ border-radius: 6px;
251
+ background: var(--code);
252
+ overflow-x: auto;
253
+ white-space: pre;
254
+ }
255
+ .markdown pre code {
256
+ padding: 0;
257
+ border: 0;
258
+ background: transparent;
259
+ font-size: 12px;
260
+ }
261
+ .markdown table {
262
+ width: 100%;
263
+ margin: 0 0 8px;
264
+ border-collapse: collapse;
265
+ font-size: 13px;
266
+ }
267
+ .markdown th,
268
+ .markdown td {
269
+ border: 1px solid var(--line);
270
+ padding: 5px 7px;
271
+ text-align: left;
272
+ vertical-align: top;
273
+ }
274
+ .markdown th {
275
+ background: #f3f6f4;
276
+ font-weight: 650;
277
+ }
278
+ .markdown a {
279
+ color: #0b6bcb;
280
+ text-decoration: none;
281
+ }
282
+ .markdown a:hover {
283
+ text-decoration: underline;
284
+ }
285
+ .composer {
286
+ min-width: 0;
287
+ border-top: 1px solid var(--line);
288
+ padding: 12px 12px 28px;
289
+ display: grid;
290
+ gap: 8px;
291
+ background: #fff;
292
+ }
293
+ textarea {
294
+ min-width: 0;
295
+ resize: vertical;
296
+ min-height: 88px;
297
+ max-height: 35vh;
298
+ width: 100%;
299
+ border: 1px solid var(--line);
300
+ border-radius: 6px;
301
+ padding: 10px;
302
+ font: inherit;
303
+ line-height: 1.4;
304
+ }
305
+ .actions {
306
+ display: flex;
307
+ justify-content: flex-end;
308
+ align-items: center;
309
+ }
310
+ .spinner {
311
+ position: absolute;
312
+ left: 12px;
313
+ right: 12px;
314
+ bottom: 6px;
315
+ min-height: 18px;
316
+ color: var(--muted);
317
+ font-size: 12px;
318
+ white-space: nowrap;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ }
322
+ code {
323
+ background: var(--code);
324
+ border: 1px solid var(--line);
325
+ border-radius: 4px;
326
+ padding: 1px 4px;
327
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
328
+ font-size: 12px;
329
+ }
330
+ @media (max-width: 900px) {
331
+ body { overflow: auto; height: auto; }
332
+ .workspace { min-width: 0; }
333
+ }
334
+ </style>
335
+ </head>
336
+ <body>
337
+ <div class="workspace">
338
+ <section class="board-pane" aria-label="workspace board">
339
+ <div class="boardbar">Board: <code>__BOARD_LABEL__</code></div>
340
+ <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
341
+ </section>
342
+ <button id="splitter" class="splitter" type="button" aria-label="Resize chat panel"></button>
343
+ <section class="chat" aria-label="pycodex chat">
344
+ <div class="topbar">
345
+ <div class="tabbar">
346
+ <div id="tabs" class="tabs"></div>
347
+ <button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">+</button>
348
+ </div>
349
+ </div>
350
+ <div id="log" class="log"></div>
351
+ <form id="composer" class="composer">
352
+ <textarea id="prompt" placeholder="Ask pycodex or type /help..."></textarea>
353
+ </form>
354
+ <div id="spinner" class="spinner"></div>
355
+ </section>
356
+ </div>
357
+ <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
358
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
359
+ <script>
360
+ const log = document.getElementById("log");
361
+ const spinner = document.getElementById("spinner");
362
+ const prompt = document.getElementById("prompt");
363
+ const form = document.getElementById("composer");
364
+ const boardFrame = document.getElementById("boardFrame");
365
+ const workspace = document.querySelector(".workspace");
366
+ const splitter = document.getElementById("splitter");
367
+ const tabsEl = document.getElementById("tabs");
368
+ const newTabButton = document.getElementById("newTab");
369
+ let pollTimer = null;
370
+ let boardPollTimer = null;
371
+ let pollInFlight = false;
372
+ let boardPollInFlight = false;
373
+ let lastBoardSignature = "";
374
+ let lastRenderedSignature = "";
375
+ let resizeStart = null;
376
+ let spinnerTimer = null;
377
+ let spinnerFrameIndex = 0;
378
+ let spinnerText = "";
379
+ let activeSessionId = "";
380
+ let sessions = [];
381
+ let isComposing = false;
382
+ const sessionState = new Map();
383
+ let restoreScrollTop = null;
384
+ let suppressScrollSave = false;
385
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
386
+ const markdownRenderer = window.markdownit
387
+ ? window.markdownit({html: false, linkify: true, breaks: false})
388
+ : null;
389
+
390
+ function relativeUrl(path) {
391
+ return new URL(path, window.location.href).toString();
392
+ }
393
+
394
+ function addEntry(_role, text, cls, shouldStick = null) {
395
+ const stickToBottom = shouldStick === null ? isNearBottom() : shouldStick;
396
+ const entry = document.createElement("div");
397
+ entry.className = "entry " + (cls || _role);
398
+ const body = document.createElement("div");
399
+ body.className = "text";
400
+ body.textContent = text || "";
401
+ entry.appendChild(body);
402
+ log.appendChild(entry);
403
+ if (stickToBottom) scrollToBottom();
404
+ return body;
405
+ }
406
+
407
+ function addMarkdownEntry(role, text, cls, shouldStick = null) {
408
+ const stickToBottom = shouldStick === null ? isNearBottom() : shouldStick;
409
+ const entry = document.createElement("div");
410
+ entry.className = "entry " + (cls || role);
411
+ const body = document.createElement("div");
412
+ body.className = "text markdown";
413
+ renderMarkdownInto(body, text || "");
414
+ entry.appendChild(body);
415
+ log.appendChild(entry);
416
+ if (stickToBottom) scrollToBottom();
417
+ return body;
418
+ }
419
+
420
+ function renderMarkdownInto(root, source) {
421
+ if (!markdownRenderer || !window.DOMPurify) {
422
+ root.textContent = source || "";
423
+ return;
424
+ }
425
+ const dirtyHtml = markdownRenderer.render(String(source || ""));
426
+ root.innerHTML = window.DOMPurify.sanitize(dirtyHtml, {
427
+ USE_PROFILES: {html: true},
428
+ ADD_ATTR: ["target", "rel"],
429
+ });
430
+ root.querySelectorAll("a").forEach((anchor) => {
431
+ anchor.target = "_blank";
432
+ anchor.rel = "noopener noreferrer";
433
+ });
434
+ }
435
+
436
+ function loadChatWidth() {
437
+ const stored = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
438
+ if (stored > 0) setChatWidth(stored);
439
+ }
440
+
441
+ function centerSplit() {
442
+ setChatWidth(Math.max(0, (window.innerWidth - 8) / 2));
443
+ }
444
+
445
+ function setChatWidth(width) {
446
+ const viewportWidth = window.innerWidth || 0;
447
+ const splitterWidth = 8;
448
+ const gutterWidth = Math.min(24, Math.max(12, Math.round(viewportWidth * 0.06)));
449
+ const availableWidth = Math.max(0, viewportWidth - splitterWidth);
450
+ const maxChatWidth = Math.max(gutterWidth, availableWidth - gutterWidth);
451
+ const collapseWidth = Math.min(
452
+ 280,
453
+ Math.max(120, Math.round(viewportWidth * 0.25)),
454
+ );
455
+ let nextWidth = Math.max(gutterWidth, Math.min(maxChatWidth, Math.round(width)));
456
+ let collapsed = false;
457
+ if (nextWidth < collapseWidth) {
458
+ nextWidth = gutterWidth;
459
+ collapsed = true;
460
+ } else if (availableWidth - nextWidth < collapseWidth) {
461
+ nextWidth = maxChatWidth;
462
+ collapsed = true;
463
+ }
464
+ workspace.style.setProperty("--chat-width", `${nextWidth}px`);
465
+ workspace.classList.toggle("collapsed", collapsed);
466
+ window.localStorage.setItem("pycodex.workspace.chatWidth", String(nextWidth));
467
+ }
468
+
469
+ function startResize(event) {
470
+ if (!workspace) return;
471
+ const chatWidth = document.querySelector(".chat").getBoundingClientRect().width;
472
+ resizeStart = { pointerId: event.pointerId, startX: event.clientX, startWidth: chatWidth };
473
+ splitter.setPointerCapture(event.pointerId);
474
+ workspace.classList.add("resizing");
475
+ event.preventDefault();
476
+ }
477
+
478
+ function updateResize(event) {
479
+ if (!resizeStart) return;
480
+ const delta = resizeStart.startX - event.clientX;
481
+ setChatWidth(resizeStart.startWidth + delta);
482
+ event.preventDefault();
483
+ }
484
+
485
+ function endResize(event) {
486
+ if (!resizeStart) return;
487
+ try {
488
+ splitter.releasePointerCapture(resizeStart.pointerId);
489
+ } catch (_error) {
490
+ // Pointer capture can already be released by the browser.
491
+ }
492
+ resizeStart = null;
493
+ workspace.classList.remove("resizing");
494
+ event.preventDefault();
495
+ }
496
+
497
+ function setSpinner(text) {
498
+ spinnerText = String(text || "").trim();
499
+ spinnerFrameIndex = 0;
500
+ renderSpinner();
501
+ if (spinnerText && !spinnerTimer) {
502
+ spinnerTimer = setInterval(renderSpinner, 120);
503
+ }
504
+ if (!spinnerText && spinnerTimer) {
505
+ clearInterval(spinnerTimer);
506
+ spinnerTimer = null;
507
+ }
508
+ }
509
+
510
+ function renderSpinner() {
511
+ if (!spinnerText) {
512
+ spinner.textContent = "";
513
+ return;
514
+ }
515
+ const frame = spinnerFrames[spinnerFrameIndex % spinnerFrames.length];
516
+ spinnerFrameIndex += 1;
517
+ spinner.textContent = `${frame} ${spinnerText}`;
518
+ }
519
+
520
+ function isNearBottom() {
521
+ return log.scrollHeight - log.scrollTop - log.clientHeight < 80;
522
+ }
523
+
524
+ function scrollToBottom() {
525
+ log.scrollTop = log.scrollHeight;
526
+ }
527
+
528
+ function stateForSession(sessionId) {
529
+ const key = String(sessionId || "");
530
+ if (!sessionState.has(key)) {
531
+ sessionState.set(key, {lastRenderedSignature: "", scrollTop: null, draft: ""});
532
+ }
533
+ return sessionState.get(key);
534
+ }
535
+
536
+ function saveActiveSessionUiState() {
537
+ if (!activeSessionId) return;
538
+ const state = stateForSession(activeSessionId);
539
+ if (log.scrollHeight > log.clientHeight) {
540
+ state.scrollTop = log.scrollTop;
541
+ }
542
+ state.draft = prompt.value;
543
+ state.lastRenderedSignature = lastRenderedSignature;
544
+ }
545
+
546
+ function updateSessions(nextSessions) {
547
+ sessions = Array.isArray(nextSessions) ? nextSessions : sessions;
548
+ if (!activeSessionId && sessions.length) {
549
+ activeSessionId = sessions[0].id || "";
550
+ }
551
+ renderTabs();
552
+ }
553
+
554
+ function renderTabs() {
555
+ tabsEl.textContent = "";
556
+ sessions.forEach((session, index) => {
557
+ const sessionId = session.id || "";
558
+ const tab = document.createElement("div");
559
+ tab.className = "tab" + (sessionId === activeSessionId ? " active" : "");
560
+ tab.title = session.title || `Session ${index + 1}`;
561
+ tab.setAttribute("role", "tab");
562
+ tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
563
+ tab.addEventListener("click", () => switchSession(sessionId));
564
+
565
+ const label = document.createElement("span");
566
+ label.className = "tab-label";
567
+ label.textContent = session.title || `Session ${index + 1}`;
568
+ tab.appendChild(label);
569
+
570
+ const close = document.createElement("button");
571
+ close.type = "button";
572
+ close.className = "tab-close";
573
+ close.title = "Close session";
574
+ close.setAttribute("aria-label", "Close session");
575
+ close.textContent = "×";
576
+ close.disabled = sessions.length <= 1;
577
+ close.addEventListener("click", (event) => {
578
+ event.stopPropagation();
579
+ closeSession(sessionId);
580
+ });
581
+ tab.appendChild(close);
582
+ tabsEl.appendChild(tab);
583
+ });
584
+ }
585
+
586
+ function messageSignature(snapshot) {
587
+ const turns = (snapshot && snapshot.turns) || [];
588
+ return JSON.stringify(
589
+ turns.map((item) => [
590
+ item.submission_id || "",
591
+ item.turn_id || "",
592
+ item.prompt || "",
593
+ item.response || "",
594
+ item.thinking || "",
595
+ item.status || "",
596
+ item.error || "",
597
+ item.kind || "",
598
+ ]),
599
+ );
600
+ }
601
+
602
+ async function pollSession() {
603
+ if (pollInFlight) return;
604
+ pollInFlight = true;
605
+ const requestedSessionId = activeSessionId;
606
+ try {
607
+ const path = requestedSessionId
608
+ ? `api/session?session_id=${encodeURIComponent(requestedSessionId)}`
609
+ : "api/session";
610
+ const response = await fetch(relativeUrl(path));
611
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
612
+ const payload = await response.json();
613
+ if (requestedSessionId && requestedSessionId !== activeSessionId) {
614
+ return;
615
+ }
616
+ if (payload.session_id && payload.session_id !== activeSessionId) {
617
+ activeSessionId = payload.session_id;
618
+ }
619
+ updateSessions(payload.sessions || []);
620
+ renderSnapshot(payload.snapshot);
621
+ } catch (error) {
622
+ setSpinner(`poll error: ${error.message}`);
623
+ } finally {
624
+ pollInFlight = false;
625
+ }
626
+ }
627
+
628
+ function startPolling() {
629
+ if (pollTimer) return;
630
+ pollSession();
631
+ pollTimer = setInterval(pollSession, 2000);
632
+ }
633
+
634
+ function stopPolling() {
635
+ if (!pollTimer) return;
636
+ clearInterval(pollTimer);
637
+ pollTimer = null;
638
+ }
639
+
640
+ async function pollBoard() {
641
+ if (boardPollInFlight) return;
642
+ boardPollInFlight = true;
643
+ try {
644
+ const response = await fetch(relativeUrl("api/board"));
645
+ if (!response.ok) return;
646
+ const payload = await response.json();
647
+ if (!payload.exists) return;
648
+ const signature = `${payload.mtime_ns || ""}:${payload.size || ""}`;
649
+ if (!lastBoardSignature) {
650
+ lastBoardSignature = signature;
651
+ return;
652
+ }
653
+ if (signature !== lastBoardSignature) {
654
+ lastBoardSignature = signature;
655
+ boardFrame.src = `board?t=${Date.now()}`;
656
+ }
657
+ } catch (_error) {
658
+ return;
659
+ } finally {
660
+ boardPollInFlight = false;
661
+ }
662
+ }
663
+
664
+ function startBoardPolling() {
665
+ if (boardPollTimer) return;
666
+ pollBoard();
667
+ boardPollTimer = setInterval(pollBoard, 2000);
668
+ }
669
+
670
+ function renderSnapshot(snapshot) {
671
+ if (!snapshot) return;
672
+ const state = stateForSession(activeSessionId);
673
+ const signature = messageSignature(snapshot);
674
+ if (signature === state.lastRenderedSignature) {
675
+ setSpinner(snapshot.spinner || "");
676
+ return;
677
+ }
678
+ state.lastRenderedSignature = signature;
679
+ lastRenderedSignature = signature;
680
+ const shouldStick = isNearBottom();
681
+ const previousScrollTop = log.scrollTop;
682
+ suppressScrollSave = true;
683
+ try {
684
+ log.textContent = "";
685
+ const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
686
+ if (turns.length) {
687
+ turns.forEach((turn) => renderTurn(turn, shouldStick));
688
+ }
689
+ setSpinner(snapshot.spinner || "");
690
+ if (restoreScrollTop !== null) {
691
+ log.scrollTop = Math.min(
692
+ restoreScrollTop,
693
+ Math.max(0, log.scrollHeight - log.clientHeight),
694
+ );
695
+ restoreScrollTop = null;
696
+ } else if (shouldStick) {
697
+ scrollToBottom();
698
+ } else {
699
+ log.scrollTop = Math.min(
700
+ previousScrollTop,
701
+ Math.max(0, log.scrollHeight - log.clientHeight),
702
+ );
703
+ }
704
+ } finally {
705
+ suppressScrollSave = false;
706
+ }
707
+ state.scrollTop = log.scrollTop;
708
+ }
709
+
710
+ function renderTurn(turn, shouldStick) {
711
+ const isControl = turn.kind === "control";
712
+ if (turn.prompt) {
713
+ addEntry(
714
+ isControl ? "command" : "prompt",
715
+ turn.prompt || "",
716
+ isControl ? "control" : "user",
717
+ shouldStick,
718
+ );
719
+ }
720
+ if (turn.response) {
721
+ addMarkdownEntry(
722
+ isControl ? "result" : "response",
723
+ turn.response || "",
724
+ isControl ? "control" : "response",
725
+ shouldStick,
726
+ );
727
+ }
728
+ if (turn.thinking) addEntry("thinking", turn.thinking || "", "thinking", shouldStick);
729
+ if (turn.error) addEntry("error", turn.error || "", "error", shouldStick);
730
+ }
731
+
732
+ async function submitPrompt(text) {
733
+ const response = await fetch(relativeUrl("api/session/message"), {
734
+ method: "POST",
735
+ headers: {"Content-Type": "application/json"},
736
+ body: JSON.stringify({prompt: text, sender: "web", session_id: activeSessionId}),
737
+ });
738
+ const payload = await response.json();
739
+ if (!response.ok || !payload.ok) {
740
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
741
+ setSpinner("");
742
+ return;
743
+ }
744
+ if (payload.snapshot) {
745
+ if (payload.sessions) updateSessions(payload.sessions);
746
+ renderSnapshot(payload.snapshot);
747
+ } else {
748
+ setSpinner("");
749
+ }
750
+ startPolling();
751
+ }
752
+
753
+ async function switchSession(sessionId) {
754
+ if (!sessionId || sessionId === activeSessionId) return;
755
+ saveActiveSessionUiState();
756
+ activeSessionId = sessionId;
757
+ const state = stateForSession(sessionId);
758
+ state.lastRenderedSignature = "";
759
+ lastRenderedSignature = "";
760
+ prompt.value = state.draft || "";
761
+ suppressScrollSave = true;
762
+ log.textContent = "";
763
+ suppressScrollSave = false;
764
+ setSpinner("");
765
+ restoreScrollTop = state.scrollTop;
766
+ await pollSession();
767
+ renderTabs();
768
+ }
769
+
770
+ async function createSession() {
771
+ saveActiveSessionUiState();
772
+ const response = await fetch(relativeUrl("api/sessions"), {method: "POST"});
773
+ const payload = await response.json();
774
+ if (!response.ok || !payload.ok) {
775
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
776
+ return;
777
+ }
778
+ activeSessionId = payload.session_id || "";
779
+ const state = stateForSession(activeSessionId);
780
+ state.draft = "";
781
+ state.scrollTop = null;
782
+ state.lastRenderedSignature = "";
783
+ lastRenderedSignature = "";
784
+ prompt.value = "";
785
+ restoreScrollTop = null;
786
+ updateSessions(payload.sessions || []);
787
+ renderSnapshot(payload.snapshot);
788
+ }
789
+
790
+ async function closeSession(sessionId) {
791
+ if (!sessionId || sessions.length <= 1) return;
792
+ const response = await fetch(relativeUrl(`api/sessions/${encodeURIComponent(sessionId)}`), {
793
+ method: "DELETE",
794
+ });
795
+ const payload = await response.json();
796
+ if (!response.ok || !payload.ok) {
797
+ addEntry("error", payload.error || `HTTP ${response.status}`, "error");
798
+ return;
799
+ }
800
+ sessionState.delete(sessionId);
801
+ updateSessions(payload.sessions || []);
802
+ if (activeSessionId === sessionId) {
803
+ activeSessionId = sessions.length ? sessions[0].id || "" : "";
804
+ lastRenderedSignature = "";
805
+ prompt.value = "";
806
+ suppressScrollSave = true;
807
+ log.textContent = "";
808
+ suppressScrollSave = false;
809
+ await pollSession();
810
+ } else {
811
+ renderTabs();
812
+ }
813
+ }
814
+
815
+ form.addEventListener("submit", (event) => {
816
+ event.preventDefault();
817
+ const text = prompt.value.trim();
818
+ if (!text) return;
819
+ prompt.value = "";
820
+ stateForSession(activeSessionId).draft = "";
821
+ submitPrompt(text);
822
+ });
823
+
824
+ prompt.addEventListener("input", () => {
825
+ stateForSession(activeSessionId).draft = prompt.value;
826
+ });
827
+
828
+ prompt.addEventListener("compositionstart", () => {
829
+ isComposing = true;
830
+ });
831
+
832
+ prompt.addEventListener("compositionend", () => {
833
+ isComposing = false;
834
+ });
835
+
836
+ log.addEventListener("scroll", () => {
837
+ if (suppressScrollSave) return;
838
+ if (!activeSessionId) return;
839
+ stateForSession(activeSessionId).scrollTop = log.scrollTop;
840
+ });
841
+
842
+ prompt.addEventListener("keydown", (event) => {
843
+ if (event.isComposing || isComposing || event.keyCode === 229) return;
844
+ if (event.key === "Enter" && !event.shiftKey) {
845
+ event.preventDefault();
846
+ form.requestSubmit();
847
+ }
848
+ });
849
+
850
+ splitter.addEventListener("pointerdown", startResize);
851
+ splitter.addEventListener("pointermove", updateResize);
852
+ splitter.addEventListener("pointerup", endResize);
853
+ splitter.addEventListener("pointercancel", endResize);
854
+ splitter.addEventListener("dblclick", centerSplit);
855
+ newTabButton.addEventListener("click", createSession);
856
+ window.addEventListener("resize", () => {
857
+ const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
858
+ if (current > 0) setChatWidth(current);
859
+ });
860
+
861
+ loadChatWidth();
862
+ startPolling();
863
+ startBoardPolling();
864
+ </script>
865
+ </body>
866
+ </html>