python-codex 0.2.1__py3-none-any.whl → 0.2.3__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.
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>pycodex workspace</title>
6
+ <title>__WORKSPACE_TITLE__</title>
7
7
  <style>
8
8
  :root {
9
9
  color-scheme: light;
@@ -63,13 +63,57 @@
63
63
  .boardbar {
64
64
  min-width: 0;
65
65
  border-bottom: 1px solid var(--line);
66
- padding: var(--space-4) var(--space-6);
66
+ padding: 0;
67
67
  color: var(--muted);
68
68
  font-size: 12px;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: var(--space-5);
69
72
  white-space: nowrap;
70
73
  overflow: hidden;
74
+ }
75
+ .boardbar-paths {
76
+ min-width: 0;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: var(--space-4);
80
+ overflow: hidden;
81
+ line-height: 20px;
82
+ }
83
+ .boardbar-paths span {
84
+ min-width: 0;
85
+ overflow: hidden;
71
86
  text-overflow: ellipsis;
72
87
  }
88
+ .workspace-back {
89
+ flex: 0 0 auto;
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ width: 28px;
94
+ height: 20px;
95
+ border: 0;
96
+ border-radius: var(--radius-sm);
97
+ background: transparent;
98
+ color: var(--ink);
99
+ padding: 0;
100
+ text-decoration: none;
101
+ }
102
+ .workspace-back:hover {
103
+ background: #eef7f1;
104
+ }
105
+ .workspace-back:focus {
106
+ outline: 2px solid #9bb7a6;
107
+ outline-offset: 1px;
108
+ }
109
+ .workspace-back::before {
110
+ content: "";
111
+ width: 7px;
112
+ height: 7px;
113
+ border-left: 2px solid currentColor;
114
+ border-bottom: 2px solid currentColor;
115
+ transform: rotate(45deg);
116
+ }
73
117
  .board {
74
118
  border: 0;
75
119
  min-width: 0;
@@ -136,6 +180,36 @@
136
180
  gap: var(--space-2);
137
181
  min-width: 0;
138
182
  }
183
+ .context-meter {
184
+ --context-remaining: 0%;
185
+ --context-meter-size: 22px;
186
+ position: relative;
187
+ display: inline-grid;
188
+ place-items: center;
189
+ width: var(--context-meter-size);
190
+ height: var(--context-meter-size);
191
+ border-radius: 999px;
192
+ background: conic-gradient(#4ca56f 0 var(--context-remaining), #cfd8d2 0 100%);
193
+ color: #335243;
194
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
195
+ font-size: 9px;
196
+ line-height: 1;
197
+ flex: 0 0 auto;
198
+ }
199
+ .context-meter.hidden {
200
+ display: none;
201
+ }
202
+ .context-meter::before {
203
+ content: "";
204
+ position: absolute;
205
+ inset: 4px;
206
+ border-radius: 999px;
207
+ background: #fff;
208
+ }
209
+ .context-meter-value {
210
+ position: relative;
211
+ z-index: 1;
212
+ }
139
213
  .tabs {
140
214
  display: flex;
141
215
  align-items: center;
@@ -171,21 +245,27 @@
171
245
  background: #eef7f1;
172
246
  box-shadow: inset 0 -2px 0 var(--accent);
173
247
  }
174
- .tab.running::before {
248
+ .tab-status {
249
+ position: relative;
250
+ display: inline-flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ width: 15px;
254
+ height: 15px;
255
+ border: 1px solid transparent;
256
+ border-radius: 999px;
257
+ background: transparent;
258
+ padding: 0;
259
+ flex: 0 0 auto;
260
+ }
261
+ .tab-status::before {
175
262
  content: "";
176
- position: absolute;
177
- left: 6px;
178
- top: 50%;
179
263
  width: 5px;
180
264
  height: 5px;
181
- margin-top: -2.5px;
182
265
  border-radius: 999px;
183
266
  background: var(--accent);
184
267
  animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
185
268
  }
186
- .tab.running .tab-label {
187
- padding-left: var(--space-4);
188
- }
189
269
  .tab-label {
190
270
  min-width: 0;
191
271
  overflow: hidden;
@@ -352,9 +432,9 @@
352
432
  text-decoration: underline;
353
433
  }
354
434
  .composer {
435
+ position: relative;
355
436
  min-width: 0;
356
- border-top: 1px solid var(--line);
357
- padding: var(--space-4) var(--space-6) var(--space-5);
437
+ padding: var(--space-4);
358
438
  display: grid;
359
439
  gap: var(--space-3);
360
440
  align-content: end;
@@ -363,7 +443,6 @@
363
443
  textarea {
364
444
  min-width: 0;
365
445
  resize: vertical;
366
- min-height: 40px;
367
446
  max-height: 35vh;
368
447
  width: 100%;
369
448
  border: 1px solid var(--line);
@@ -385,14 +464,27 @@
385
464
  justify-content: flex-end;
386
465
  align-items: center;
387
466
  }
388
- .spinner {
467
+ .spinner-wrap {
389
468
  position: relative;
390
469
  display: inline-flex;
391
470
  align-items: center;
392
471
  justify-self: start;
393
472
  align-self: end;
394
473
  max-width: 100%;
395
- min-height: 20px;
474
+ }
475
+ .prompt-row {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: var(--space-2);
479
+ min-width: 0;
480
+ }
481
+ .spinner {
482
+ --spinner-pill-height: 22px;
483
+ position: relative;
484
+ display: inline-flex;
485
+ align-items: center;
486
+ max-width: 100%;
487
+ height: var(--spinner-pill-height);
396
488
  color: var(--muted);
397
489
  font-size: 12px;
398
490
  white-space: nowrap;
@@ -402,11 +494,22 @@
402
494
  border-radius: 999px;
403
495
  background: rgba(240, 250, 244, 0.96);
404
496
  border: 1px solid rgba(172, 216, 188, 0.9);
497
+ font: inherit;
498
+ font-size: 12px;
499
+ cursor: pointer;
405
500
  }
406
501
  .spinner:empty {
407
502
  display: none;
408
503
  }
409
- .spinner:not(:empty) {
504
+ .spinner-wrap:not(:has(.spinner:not(:empty))) {
505
+ display: none;
506
+ }
507
+ .spinner:focus-visible {
508
+ border-color: #65b782;
509
+ box-shadow: 0 0 0 3px rgba(15, 143, 85, 0.13);
510
+ outline: 0;
511
+ }
512
+ .spinner-wrap:has(.spinner:not(:empty)) {
410
513
  margin-bottom: var(--space-1);
411
514
  }
412
515
  .spinner:not(:empty)::before {
@@ -421,6 +524,38 @@
421
524
  background: var(--accent);
422
525
  animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
423
526
  }
527
+ .spinner.notify-enabled {
528
+ color: #0b6b43;
529
+ background: #e6f4eb;
530
+ border-color: #4ca56f;
531
+ box-shadow:
532
+ inset 0 1px 2px rgba(9, 93, 55, 0.16),
533
+ inset 0 0 0 1px rgba(15, 143, 85, 0.1);
534
+ }
535
+ .toast {
536
+ position: absolute;
537
+ left: 0;
538
+ bottom: 100%;
539
+ z-index: 5;
540
+ width: max-content;
541
+ max-width: min(320px, calc(100vw - var(--space-6) * 2));
542
+ border: 1px solid #bfd6c8;
543
+ border-radius: var(--radius-sm);
544
+ background: #f6fbf8;
545
+ color: var(--ink);
546
+ box-shadow: 0 8px 20px rgba(25, 35, 31, 0.12);
547
+ padding: var(--space-4) var(--space-6);
548
+ font-size: 12px;
549
+ opacity: 0;
550
+ pointer-events: none;
551
+ transform: translateY(4px);
552
+ transition: opacity 120ms ease, transform 120ms ease;
553
+ margin-bottom: var(--space-3);
554
+ }
555
+ .toast.visible {
556
+ opacity: 1;
557
+ transform: translateY(0);
558
+ }
424
559
  @keyframes spinner-pulse {
425
560
  0%, 100% {
426
561
  opacity: 0.45;
@@ -445,11 +580,11 @@
445
580
  }
446
581
  @media (prefers-reduced-motion: reduce) {
447
582
  .entry.thinking,
448
- .tab.running::before,
583
+ .tab-status::before,
449
584
  .spinner:not(:empty)::before {
450
585
  animation: none;
451
586
  }
452
- .tab.running::before,
587
+ .tab-status::before,
453
588
  .spinner:not(:empty)::before {
454
589
  opacity: 0.8;
455
590
  transform: none;
@@ -472,7 +607,13 @@
472
607
  <body>
473
608
  <div class="workspace">
474
609
  <section class="board-pane" aria-label="workspace board">
475
- <div class="boardbar">Board: <code>__BOARD_LABEL__</code></div>
610
+ <div class="boardbar">
611
+ <a class="workspace-back" href="/" title="Back to workspaces" aria-label="Back to workspaces"></a>
612
+ <div class="boardbar-paths">
613
+ <span>@<code>__CWD_LABEL__</code></span>
614
+ <span>Board: <code>__BOARD_LABEL__</code></span>
615
+ </div>
616
+ </div>
476
617
  <iframe id="boardFrame" class="board" src="board" title="board"></iframe>
477
618
  </section>
478
619
  <button id="splitter" class="splitter" type="button" aria-label="Resize chat panel"></button>
@@ -485,8 +626,16 @@
485
626
  </div>
486
627
  <div id="log" class="log"></div>
487
628
  <form id="composer" class="composer">
488
- <div id="spinner" class="spinner" role="status" aria-live="polite"></div>
489
- <textarea id="prompt" placeholder="Ask pycodex or type /help..."></textarea>
629
+ <div class="spinner-wrap">
630
+ <div id="toast" class="toast" role="status" aria-live="polite"></div>
631
+ <button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
632
+ </div>
633
+ <div class="prompt-row">
634
+ <div id="contextMeter" class="context-meter hidden" aria-hidden="true">
635
+ <span id="contextMeterValue" class="context-meter-value"></span>
636
+ </div>
637
+ <textarea id="prompt" rows="1" placeholder="Ask pycodex or type /help..."></textarea>
638
+ </div>
490
639
  </form>
491
640
  </section>
492
641
  </div>
@@ -495,21 +644,25 @@
495
644
  <script>
496
645
  const log = document.getElementById("log");
497
646
  const spinner = document.getElementById("spinner");
647
+ const toast = document.getElementById("toast");
498
648
  const prompt = document.getElementById("prompt");
499
649
  const form = document.getElementById("composer");
500
650
  const boardFrame = document.getElementById("boardFrame");
501
651
  const workspace = document.querySelector(".workspace");
502
652
  const splitter = document.getElementById("splitter");
503
653
  const tabsEl = document.getElementById("tabs");
654
+ const contextMeter = document.getElementById("contextMeter");
655
+ const contextMeterValue = document.getElementById("contextMeterValue");
504
656
  const newTabButton = document.getElementById("newTab");
505
657
  let pollTimer = null;
506
658
  let boardPollTimer = null;
507
659
  let pollInFlight = false;
508
660
  let boardPollInFlight = false;
509
- let lastBoardSignature = "";
661
+ let lastBoardSignature = null;
510
662
  let lastRenderedSignature = "";
511
663
  let resizeStart = null;
512
664
  let spinnerText = "";
665
+ let toastTimer = null;
513
666
  let activeSessionId = "";
514
667
  let sessions = [];
515
668
  let isComposing = false;
@@ -627,9 +780,128 @@
627
780
  event.preventDefault();
628
781
  }
629
782
 
630
- function setSpinner(text) {
631
- spinnerText = String(text || "").trim();
783
+ function setSpinner(text, options = {}) {
784
+ const previousText = spinnerText;
785
+ const nextText = String(text || "").trim();
786
+ const wasRunning = Boolean(previousText);
787
+ const isRunning = Boolean(nextText);
788
+ spinnerText = nextText;
632
789
  spinner.textContent = spinnerText;
790
+ if (wasRunning && !isRunning && !options.suppressDoneNotification) {
791
+ const state = stateForSession(activeSessionId);
792
+ if (state.wasRunning) notifySessionDone(activeSessionId);
793
+ }
794
+ updateSpinnerNotifyUi();
795
+ }
796
+
797
+ function showToast(text) {
798
+ if (toastTimer) clearTimeout(toastTimer);
799
+ toast.textContent = text;
800
+ toast.classList.add("visible");
801
+ toastTimer = setTimeout(() => {
802
+ toastTimer = null;
803
+ toast.classList.remove("visible");
804
+ }, 1400);
805
+ }
806
+
807
+ function updateSpinnerNotifyUi() {
808
+ const state = stateForSession(activeSessionId);
809
+ const enabled = Boolean(spinnerText && state.notifyWhenDone);
810
+ spinner.classList.toggle("notify-enabled", enabled);
811
+ spinner.setAttribute("aria-checked", enabled ? "true" : "false");
812
+ if (!spinnerText) {
813
+ spinner.title = "Spinner is idle";
814
+ } else if (enabled) {
815
+ spinner.title = "Notify when done is on";
816
+ } else {
817
+ spinner.title = "Notify when done is off";
818
+ }
819
+ }
820
+
821
+ function notificationAvailable() {
822
+ return "Notification" in window;
823
+ }
824
+
825
+ function requestNotificationPermission() {
826
+ try {
827
+ const requestResult = Notification.requestPermission();
828
+ if (requestResult && typeof requestResult.then === "function") {
829
+ return requestResult;
830
+ }
831
+ return Promise.resolve(Notification.permission);
832
+ } catch (_error) {
833
+ return Promise.resolve(Notification.permission);
834
+ }
835
+ }
836
+
837
+ function notifySessionDone(sessionId, sessionSummary = null) {
838
+ if (!sessionId) return;
839
+ const state = stateForSession(sessionId);
840
+ if (!state.notifyWhenDone) return;
841
+ state.notifyWhenDone = false;
842
+ if (!notificationAvailable() || Notification.permission !== "granted") return;
843
+ const session = sessionSummary || sessionById(sessionId);
844
+ const title = (session && session.title) || "pycodex";
845
+ const lastAssistant = (session && session.last_assistant) || "Done.";
846
+ try {
847
+ new Notification(`session: ${title}`, {
848
+ body: lastAssistant,
849
+ tag: `pycodex-workspace-done-${sessionId}`,
850
+ });
851
+ } catch (_error) {
852
+ return;
853
+ }
854
+ }
855
+
856
+ function sessionById(sessionId) {
857
+ return sessions.find((session) => (session.id || "") === sessionId) || null;
858
+ }
859
+
860
+ function isSessionRunning(sessionId) {
861
+ if (sessionId === activeSessionId && spinnerText) return true;
862
+ const session = sessionById(sessionId);
863
+ if (session) return Boolean(session.running);
864
+ return false;
865
+ }
866
+
867
+ async function toggleSpinnerNotification() {
868
+ const sessionId = activeSessionId;
869
+ if (!sessionId || !isSessionRunning(sessionId)) {
870
+ showToast("notify-on-done unavailable");
871
+ return;
872
+ }
873
+ const state = stateForSession(sessionId);
874
+ if (state.notifyWhenDone) {
875
+ state.notifyWhenDone = false;
876
+ showToast("notify-on-done canceled");
877
+ updateSpinnerNotifyUi();
878
+ return;
879
+ }
880
+ if (!notificationAvailable()) {
881
+ showToast("browser notifications unavailable");
882
+ return;
883
+ }
884
+ if (Notification.permission === "denied") {
885
+ showToast("browser notifications blocked");
886
+ return;
887
+ }
888
+ state.notifyWhenDone = true;
889
+ showToast("notify-on-done enabled");
890
+ updateSpinnerNotifyUi();
891
+ if (Notification.permission !== "granted") {
892
+ const permission = await requestNotificationPermission();
893
+ if (permission !== "granted") {
894
+ state.notifyWhenDone = false;
895
+ showToast("notify-on-done canceled");
896
+ updateSpinnerNotifyUi();
897
+ return;
898
+ }
899
+ }
900
+ if (!isSessionRunning(sessionId)) {
901
+ state.notifyWhenDone = false;
902
+ showToast("notify-on-done canceled");
903
+ }
904
+ updateSpinnerNotifyUi();
633
905
  }
634
906
 
635
907
  function isNearBottom() {
@@ -642,16 +914,22 @@
642
914
 
643
915
  function resizePrompt() {
644
916
  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));
917
+ const borderHeight = prompt.offsetHeight - prompt.clientHeight;
918
+ const maxHeight = Math.round(window.innerHeight * 0.35);
919
+ const nextHeight = Math.min(prompt.scrollHeight + borderHeight, maxHeight);
648
920
  prompt.style.height = `${nextHeight}px`;
649
921
  }
650
922
 
651
923
  function stateForSession(sessionId) {
652
924
  const key = String(sessionId || "");
653
925
  if (!sessionState.has(key)) {
654
- sessionState.set(key, {lastRenderedSignature: "", scrollTop: null, draft: ""});
926
+ sessionState.set(key, {
927
+ lastRenderedSignature: "",
928
+ scrollTop: null,
929
+ draft: "",
930
+ notifyWhenDone: false,
931
+ wasRunning: false,
932
+ });
655
933
  }
656
934
  return sessionState.get(key);
657
935
  }
@@ -667,11 +945,27 @@
667
945
  }
668
946
 
669
947
  function updateSessions(nextSessions) {
670
- sessions = Array.isArray(nextSessions) ? nextSessions : sessions;
948
+ const incomingSessions = Array.isArray(nextSessions) ? nextSessions : sessions;
949
+ incomingSessions.forEach((session) => {
950
+ const sessionId = session.id || "";
951
+ if (!sessionId) return;
952
+ const state = stateForSession(sessionId);
953
+ const wasRunning = Boolean(state.wasRunning);
954
+ const isRunning = Boolean(session.running);
955
+ if (!wasRunning && isRunning) {
956
+ state.notifyWhenDone = false;
957
+ } else if (wasRunning && !isRunning) {
958
+ notifySessionDone(sessionId, session);
959
+ }
960
+ state.wasRunning = isRunning;
961
+ });
962
+ sessions = incomingSessions;
671
963
  if (!activeSessionId && sessions.length) {
672
964
  activeSessionId = sessions[0].id || "";
673
965
  }
674
966
  renderTabs();
967
+ updateContextMeter();
968
+ updateSpinnerNotifyUi();
675
969
  }
676
970
 
677
971
  function renderTabs() {
@@ -689,6 +983,13 @@
689
983
  if (session.running) tab.setAttribute("aria-busy", "true");
690
984
  tab.addEventListener("click", () => switchSession(sessionId));
691
985
 
986
+ if (session.running) {
987
+ const status = document.createElement("span");
988
+ status.className = "tab-status";
989
+ status.setAttribute("aria-hidden", "true");
990
+ tab.appendChild(status);
991
+ }
992
+
692
993
  const label = document.createElement("span");
693
994
  label.className = "tab-label";
694
995
  label.textContent = session.title || `Session ${index + 1}`;
@@ -710,6 +1011,38 @@
710
1011
  });
711
1012
  }
712
1013
 
1014
+ function updateContextMeter(snapshot = null) {
1015
+ let percent = null;
1016
+ if (snapshot && snapshot.context_remaining_percent !== undefined) {
1017
+ percent = snapshot.context_remaining_percent;
1018
+ } else {
1019
+ const session = sessionById(activeSessionId);
1020
+ if (session && session.context_remaining_percent !== undefined) {
1021
+ percent = session.context_remaining_percent;
1022
+ }
1023
+ }
1024
+ if (percent === null || percent === undefined || percent === "") {
1025
+ contextMeter.classList.add("hidden");
1026
+ contextMeterValue.textContent = "";
1027
+ return;
1028
+ }
1029
+ const value = Math.max(0, Math.min(100, Math.round(Number(percent))));
1030
+ if (!Number.isFinite(value)) {
1031
+ contextMeter.classList.add("hidden");
1032
+ contextMeterValue.textContent = "";
1033
+ return;
1034
+ }
1035
+ if (value >= 100) {
1036
+ contextMeter.classList.remove("hidden");
1037
+ contextMeter.style.setProperty("--context-remaining", `${value}%`);
1038
+ contextMeterValue.textContent = "";
1039
+ return;
1040
+ }
1041
+ contextMeter.classList.remove("hidden");
1042
+ contextMeter.style.setProperty("--context-remaining", `${value}%`);
1043
+ contextMeterValue.textContent = String(value);
1044
+ }
1045
+
713
1046
  function messageSignature(snapshot) {
714
1047
  const turns = (snapshot && snapshot.turns) || [];
715
1048
  return JSON.stringify(
@@ -771,10 +1104,14 @@
771
1104
  const response = await fetch(relativeUrl("api/board"));
772
1105
  if (!response.ok) return;
773
1106
  const payload = await response.json();
774
- if (!payload.exists) return;
1107
+ if (!payload.exists) {
1108
+ lastBoardSignature = null;
1109
+ return;
1110
+ }
775
1111
  const signature = `${payload.mtime_ns || ""}:${payload.size || ""}`;
776
- if (!lastBoardSignature) {
1112
+ if (lastBoardSignature === null) {
777
1113
  lastBoardSignature = signature;
1114
+ boardFrame.src = `board?t=${Date.now()}`;
778
1115
  return;
779
1116
  }
780
1117
  if (signature !== lastBoardSignature) {
@@ -800,6 +1137,7 @@
800
1137
  const signature = messageSignature(snapshot);
801
1138
  if (signature === state.lastRenderedSignature) {
802
1139
  setSpinner(snapshot.spinner || "");
1140
+ updateContextMeter(snapshot);
803
1141
  return;
804
1142
  }
805
1143
  state.lastRenderedSignature = signature;
@@ -814,6 +1152,7 @@
814
1152
  turns.forEach((turn) => renderTurn(turn, shouldStick));
815
1153
  }
816
1154
  setSpinner(snapshot.spinner || "");
1155
+ updateContextMeter(snapshot);
817
1156
  if (restoreScrollTop !== null) {
818
1157
  log.scrollTop = Math.min(
819
1158
  restoreScrollTop,
@@ -889,7 +1228,7 @@
889
1228
  suppressScrollSave = true;
890
1229
  log.textContent = "";
891
1230
  suppressScrollSave = false;
892
- setSpinner("");
1231
+ setSpinner("", {suppressDoneNotification: true});
893
1232
  restoreScrollTop = state.scrollTop;
894
1233
  await pollSession();
895
1234
  renderTabs();
@@ -984,12 +1323,18 @@
984
1323
  splitter.addEventListener("pointerup", endResize);
985
1324
  splitter.addEventListener("pointercancel", endResize);
986
1325
  splitter.addEventListener("dblclick", centerSplit);
1326
+ spinner.addEventListener("click", toggleSpinnerNotification);
987
1327
  newTabButton.addEventListener("click", createSession);
988
1328
  window.addEventListener("resize", () => {
989
1329
  const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
990
1330
  if (current > 0) setChatWidth(current);
991
1331
  resizePrompt();
992
1332
  });
1333
+ document.addEventListener("visibilitychange", () => {
1334
+ if (document.visibilityState !== "visible") return;
1335
+ pollSession();
1336
+ pollBoard();
1337
+ });
993
1338
 
994
1339
  loadChatWidth();
995
1340
  resizePrompt();