python-codex 0.2.1__py3-none-any.whl → 0.2.2__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.
- pycodex/interactive_session.py +5 -1
- {python_codex-0.2.1.dist-info → python_codex-0.2.2.dist-info}/METADATA +1 -1
- {python_codex-0.2.1.dist-info → python_codex-0.2.2.dist-info}/RECORD +8 -8
- workspace_server/app.py +51 -2
- workspace_server/workspace.html +324 -29
- {python_codex-0.2.1.dist-info → python_codex-0.2.2.dist-info}/WHEEL +0 -0
- {python_codex-0.2.1.dist-info → python_codex-0.2.2.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.1.dist-info → python_codex-0.2.2.dist-info}/licenses/LICENSE +0 -0
pycodex/interactive_session.py
CHANGED
|
@@ -18,6 +18,7 @@ import typing
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
21
|
+
HELP_COMMAND = "/help"
|
|
21
22
|
HISTORY_COMMAND = "/history"
|
|
22
23
|
TITLE_COMMAND = "/title"
|
|
23
24
|
MODEL_COMMAND = "/model"
|
|
@@ -27,7 +28,7 @@ COMPACT_COMMAND = "/compact"
|
|
|
27
28
|
LINK_COMMAND = "/link"
|
|
28
29
|
UNLINK_COMMAND = "/unlink"
|
|
29
30
|
EXTRA_COMMANDS_LINE = (
|
|
30
|
-
"Extra commands: /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
|
+
"Extra commands: /help, /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
@@ -232,6 +233,9 @@ async def run_interactive_session(
|
|
|
232
233
|
continue
|
|
233
234
|
if prompt_text in EXIT_COMMANDS:
|
|
234
235
|
break
|
|
236
|
+
if prompt_text == HELP_COMMAND:
|
|
237
|
+
view.write_line(EXTRA_COMMANDS_LINE)
|
|
238
|
+
continue
|
|
235
239
|
if prompt_text == HISTORY_COMMAND:
|
|
236
240
|
view.show_history()
|
|
237
241
|
continue
|
|
@@ -7,7 +7,7 @@ pycodex/context.py,sha256=Hwh3vU-qSvaleUDdOWuWiGyNmbULVAZmboxiu9iiHt8,26549
|
|
|
7
7
|
pycodex/doctor.py,sha256=De3M4hRBJq8ZeqsUJgHz0vitqrH18YugrEnz7oHhTdQ,10572
|
|
8
8
|
pycodex/feishu_card.py,sha256=De6pM--3MfhgGo-WcWfhm-fop5UtzjwKrZ4gI2Lls3w,26198
|
|
9
9
|
pycodex/feishu_link.py,sha256=XGV93CIorcc03pNgBnXay9SkivnDL4i2GkLR5NjBjq0,15588
|
|
10
|
-
pycodex/interactive_session.py,sha256=
|
|
10
|
+
pycodex/interactive_session.py,sha256=pcIC_GvQuqQSsjzcTTFB_crrS-XVufAGplJZxnHgy7I,15769
|
|
11
11
|
pycodex/model.py,sha256=O5Gf35qCMAGrqvr_5d1ZQNk3vHmNRD5Uv-8k7-zbpXg,36679
|
|
12
12
|
pycodex/portable.py,sha256=gxl2E2h5uZJbasMEPPs-nyALFPIvX79T2ZYsu6vXZrg,15656
|
|
13
13
|
pycodex/portable_server.py,sha256=6I3pQkWj3e_SFlDXY2mGdCPns1w_3PSxByBV9wv5epI,7331
|
|
@@ -79,10 +79,10 @@ responses_server/tools/custom_adapter.py,sha256=LxO7ldydvR-GWachDz8GKC0Q8KGGFoFP
|
|
|
79
79
|
responses_server/tools/web_search.py,sha256=pm4ZUiHUfxc0bGY1kEvt-BCzDrZIyP24xzPUcga2ul0,8908
|
|
80
80
|
workspace_server/__init__.py,sha256=XbSU6aAeYBMsBTVi9Ug6ZW9sxcwefqPS7T_7wpt_VMo,462
|
|
81
81
|
workspace_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
|
|
82
|
-
workspace_server/app.py,sha256=
|
|
83
|
-
workspace_server/workspace.html,sha256=
|
|
84
|
-
python_codex-0.2.
|
|
85
|
-
python_codex-0.2.
|
|
86
|
-
python_codex-0.2.
|
|
87
|
-
python_codex-0.2.
|
|
88
|
-
python_codex-0.2.
|
|
82
|
+
workspace_server/app.py,sha256=yORhk3BcZZiO1x-QTh0wFi4BXvrS5fbYMgVfEgGBgq8,51270
|
|
83
|
+
workspace_server/workspace.html,sha256=5cHWRXGFknkDDqrOtmiBeOI1HRQHbRmkrNFm8wZ4_JU,39202
|
|
84
|
+
python_codex-0.2.2.dist-info/METADATA,sha256=GbrTS_Leh6VMxhYAImSrdaFzDblJbwW4Xj5FdCRfdfI,16812
|
|
85
|
+
python_codex-0.2.2.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
86
|
+
python_codex-0.2.2.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
|
|
87
|
+
python_codex-0.2.2.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
|
|
88
|
+
python_codex-0.2.2.dist-info/RECORD,,
|
workspace_server/app.py
CHANGED
|
@@ -25,7 +25,12 @@ from pycodex.utils.session_persist import (
|
|
|
25
25
|
load_resumed_session_path,
|
|
26
26
|
)
|
|
27
27
|
from pycodex.utils import uuid7_string
|
|
28
|
-
from pycodex.utils.visualize import
|
|
28
|
+
from pycodex.utils.visualize import (
|
|
29
|
+
IDLE_LISTENING_STATUS,
|
|
30
|
+
percent_of_context_window_remaining,
|
|
31
|
+
shorten_title,
|
|
32
|
+
tool_summary,
|
|
33
|
+
)
|
|
29
34
|
import typing
|
|
30
35
|
|
|
31
36
|
|
|
@@ -180,6 +185,8 @@ class WebSessionView:
|
|
|
180
185
|
self._title = ""
|
|
181
186
|
self._spinner_status = ""
|
|
182
187
|
self._stream_buffer = ""
|
|
188
|
+
self._context_window_tokens: "typing.Union[int, None]" = None
|
|
189
|
+
self._context_remaining_percent: "typing.Union[int, None]" = None
|
|
183
190
|
self._closed = False
|
|
184
191
|
self._server_loop: "typing.Union[asyncio.AbstractEventLoop, None]" = None
|
|
185
192
|
self._worker_loop: "typing.Union[asyncio.AbstractEventLoop, None]" = None
|
|
@@ -347,7 +354,11 @@ class WebSessionView:
|
|
|
347
354
|
self,
|
|
348
355
|
context_window_tokens: "typing.Union[int, None]",
|
|
349
356
|
) -> None:
|
|
350
|
-
|
|
357
|
+
with self._lock:
|
|
358
|
+
self._context_window_tokens = context_window_tokens
|
|
359
|
+
self._context_remaining_percent = (
|
|
360
|
+
100 if context_window_tokens is not None else None
|
|
361
|
+
)
|
|
351
362
|
|
|
352
363
|
def subscribe(self) -> "asyncio.Queue":
|
|
353
364
|
queue: "asyncio.Queue" = asyncio.Queue()
|
|
@@ -386,6 +397,7 @@ class WebSessionView:
|
|
|
386
397
|
"spinner": self._spinner_status,
|
|
387
398
|
"model": "pycodex",
|
|
388
399
|
"title": self._title,
|
|
400
|
+
"context_remaining_percent": self._context_remaining_percent,
|
|
389
401
|
"turns": [_public_turn(turn) for turn in self._turns[-80:]],
|
|
390
402
|
}
|
|
391
403
|
|
|
@@ -396,6 +408,8 @@ class WebSessionView:
|
|
|
396
408
|
"spinner": self._spinner_status,
|
|
397
409
|
"title": self._title,
|
|
398
410
|
"turn_count": len(self._turns),
|
|
411
|
+
"last_assistant": _last_assistant_text(self._turns),
|
|
412
|
+
"context_remaining_percent": self._context_remaining_percent,
|
|
399
413
|
}
|
|
400
414
|
|
|
401
415
|
def _apply_runtime_event(self, event: "AgentEvent") -> None:
|
|
@@ -403,6 +417,9 @@ class WebSessionView:
|
|
|
403
417
|
payload = getattr(event, "payload", {})
|
|
404
418
|
if not isinstance(payload, dict):
|
|
405
419
|
payload = {}
|
|
420
|
+
if kind == "token_count":
|
|
421
|
+
self._update_context_window(payload.get("usage"))
|
|
422
|
+
return
|
|
406
423
|
turn_id = str(payload.get("turn_id") or getattr(event, "turn_id", "") or "")
|
|
407
424
|
submission_id = str(payload.get("submission_id") or turn_id or "")
|
|
408
425
|
turn = self._turns_by_submission_id.get(submission_id)
|
|
@@ -475,6 +492,22 @@ class WebSessionView:
|
|
|
475
492
|
turn["status"] = "interrupted"
|
|
476
493
|
self._stream_buffer = ""
|
|
477
494
|
|
|
495
|
+
def _update_context_window(self, usage: "object") -> None:
|
|
496
|
+
if self._context_window_tokens is None:
|
|
497
|
+
return
|
|
498
|
+
if not isinstance(usage, dict):
|
|
499
|
+
self._context_remaining_percent = None
|
|
500
|
+
return
|
|
501
|
+
try:
|
|
502
|
+
total_tokens = int(usage["total_tokens"])
|
|
503
|
+
except (KeyError, TypeError, ValueError):
|
|
504
|
+
self._context_remaining_percent = None
|
|
505
|
+
return
|
|
506
|
+
self._context_remaining_percent = percent_of_context_window_remaining(
|
|
507
|
+
total_tokens,
|
|
508
|
+
self._context_window_tokens,
|
|
509
|
+
)
|
|
510
|
+
|
|
478
511
|
def _apply_spinner_event(
|
|
479
512
|
self,
|
|
480
513
|
kind: str,
|
|
@@ -974,6 +1007,10 @@ class WorkspaceSessionManager:
|
|
|
974
1007
|
"running": bool(summary.get("running")),
|
|
975
1008
|
"spinner": summary.get("spinner") or "",
|
|
976
1009
|
"turn_count": summary.get("turn_count") or 0,
|
|
1010
|
+
"last_assistant": summary.get("last_assistant") or "",
|
|
1011
|
+
"context_remaining_percent": summary.get(
|
|
1012
|
+
"context_remaining_percent"
|
|
1013
|
+
),
|
|
977
1014
|
}
|
|
978
1015
|
)
|
|
979
1016
|
return result
|
|
@@ -1178,9 +1215,21 @@ def _session_summary(session) -> "typing.Dict[str, object]":
|
|
|
1178
1215
|
"running": bool(snapshot.get("running")),
|
|
1179
1216
|
"spinner": snapshot.get("spinner") or "",
|
|
1180
1217
|
"turn_count": len(snapshot.get("turns") or []),
|
|
1218
|
+
"last_assistant": _last_assistant_text(snapshot.get("turns") or []),
|
|
1219
|
+
"context_remaining_percent": snapshot.get("context_remaining_percent"),
|
|
1181
1220
|
}
|
|
1182
1221
|
|
|
1183
1222
|
|
|
1223
|
+
def _last_assistant_text(turns: "typing.Iterable[typing.Dict[str, object]]") -> str:
|
|
1224
|
+
for turn in reversed(list(turns)):
|
|
1225
|
+
if str(turn.get("kind") or "assistant") == "control":
|
|
1226
|
+
continue
|
|
1227
|
+
response = str(turn.get("response") or "").strip()
|
|
1228
|
+
if response:
|
|
1229
|
+
return response
|
|
1230
|
+
return ""
|
|
1231
|
+
|
|
1232
|
+
|
|
1184
1233
|
def _public_turn(turn: "typing.Dict[str, object]") -> "typing.Dict[str, object]":
|
|
1185
1234
|
return typing.cast(
|
|
1186
1235
|
"typing.Dict[str, object]",
|
workspace_server/workspace.html
CHANGED
|
@@ -136,6 +136,36 @@
|
|
|
136
136
|
gap: var(--space-2);
|
|
137
137
|
min-width: 0;
|
|
138
138
|
}
|
|
139
|
+
.context-meter {
|
|
140
|
+
--context-remaining: 0%;
|
|
141
|
+
--context-meter-size: 22px;
|
|
142
|
+
position: relative;
|
|
143
|
+
display: inline-grid;
|
|
144
|
+
place-items: center;
|
|
145
|
+
width: var(--context-meter-size);
|
|
146
|
+
height: var(--context-meter-size);
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
background: conic-gradient(#4ca56f 0 var(--context-remaining), #cfd8d2 0 100%);
|
|
149
|
+
color: #335243;
|
|
150
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
151
|
+
font-size: 9px;
|
|
152
|
+
line-height: 1;
|
|
153
|
+
flex: 0 0 auto;
|
|
154
|
+
}
|
|
155
|
+
.context-meter.hidden {
|
|
156
|
+
display: none;
|
|
157
|
+
}
|
|
158
|
+
.context-meter::before {
|
|
159
|
+
content: "";
|
|
160
|
+
position: absolute;
|
|
161
|
+
inset: 4px;
|
|
162
|
+
border-radius: 999px;
|
|
163
|
+
background: #fff;
|
|
164
|
+
}
|
|
165
|
+
.context-meter-value {
|
|
166
|
+
position: relative;
|
|
167
|
+
z-index: 1;
|
|
168
|
+
}
|
|
139
169
|
.tabs {
|
|
140
170
|
display: flex;
|
|
141
171
|
align-items: center;
|
|
@@ -171,21 +201,27 @@
|
|
|
171
201
|
background: #eef7f1;
|
|
172
202
|
box-shadow: inset 0 -2px 0 var(--accent);
|
|
173
203
|
}
|
|
174
|
-
.tab
|
|
204
|
+
.tab-status {
|
|
205
|
+
position: relative;
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
width: 15px;
|
|
210
|
+
height: 15px;
|
|
211
|
+
border: 1px solid transparent;
|
|
212
|
+
border-radius: 999px;
|
|
213
|
+
background: transparent;
|
|
214
|
+
padding: 0;
|
|
215
|
+
flex: 0 0 auto;
|
|
216
|
+
}
|
|
217
|
+
.tab-status::before {
|
|
175
218
|
content: "";
|
|
176
|
-
position: absolute;
|
|
177
|
-
left: 6px;
|
|
178
|
-
top: 50%;
|
|
179
219
|
width: 5px;
|
|
180
220
|
height: 5px;
|
|
181
|
-
margin-top: -2.5px;
|
|
182
221
|
border-radius: 999px;
|
|
183
222
|
background: var(--accent);
|
|
184
223
|
animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
185
224
|
}
|
|
186
|
-
.tab.running .tab-label {
|
|
187
|
-
padding-left: var(--space-4);
|
|
188
|
-
}
|
|
189
225
|
.tab-label {
|
|
190
226
|
min-width: 0;
|
|
191
227
|
overflow: hidden;
|
|
@@ -352,9 +388,9 @@
|
|
|
352
388
|
text-decoration: underline;
|
|
353
389
|
}
|
|
354
390
|
.composer {
|
|
391
|
+
position: relative;
|
|
355
392
|
min-width: 0;
|
|
356
|
-
|
|
357
|
-
padding: var(--space-4) var(--space-6) var(--space-5);
|
|
393
|
+
padding: var(--space-4);
|
|
358
394
|
display: grid;
|
|
359
395
|
gap: var(--space-3);
|
|
360
396
|
align-content: end;
|
|
@@ -363,7 +399,6 @@
|
|
|
363
399
|
textarea {
|
|
364
400
|
min-width: 0;
|
|
365
401
|
resize: vertical;
|
|
366
|
-
min-height: 40px;
|
|
367
402
|
max-height: 35vh;
|
|
368
403
|
width: 100%;
|
|
369
404
|
border: 1px solid var(--line);
|
|
@@ -385,14 +420,27 @@
|
|
|
385
420
|
justify-content: flex-end;
|
|
386
421
|
align-items: center;
|
|
387
422
|
}
|
|
388
|
-
.spinner {
|
|
423
|
+
.spinner-wrap {
|
|
389
424
|
position: relative;
|
|
390
425
|
display: inline-flex;
|
|
391
426
|
align-items: center;
|
|
392
427
|
justify-self: start;
|
|
393
428
|
align-self: end;
|
|
394
429
|
max-width: 100%;
|
|
395
|
-
|
|
430
|
+
}
|
|
431
|
+
.prompt-row {
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
gap: var(--space-2);
|
|
435
|
+
min-width: 0;
|
|
436
|
+
}
|
|
437
|
+
.spinner {
|
|
438
|
+
--spinner-pill-height: 22px;
|
|
439
|
+
position: relative;
|
|
440
|
+
display: inline-flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
max-width: 100%;
|
|
443
|
+
height: var(--spinner-pill-height);
|
|
396
444
|
color: var(--muted);
|
|
397
445
|
font-size: 12px;
|
|
398
446
|
white-space: nowrap;
|
|
@@ -402,11 +450,22 @@
|
|
|
402
450
|
border-radius: 999px;
|
|
403
451
|
background: rgba(240, 250, 244, 0.96);
|
|
404
452
|
border: 1px solid rgba(172, 216, 188, 0.9);
|
|
453
|
+
font: inherit;
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
cursor: pointer;
|
|
405
456
|
}
|
|
406
457
|
.spinner:empty {
|
|
407
458
|
display: none;
|
|
408
459
|
}
|
|
409
|
-
.spinner:not(:empty) {
|
|
460
|
+
.spinner-wrap:not(:has(.spinner:not(:empty))) {
|
|
461
|
+
display: none;
|
|
462
|
+
}
|
|
463
|
+
.spinner:focus-visible {
|
|
464
|
+
border-color: #65b782;
|
|
465
|
+
box-shadow: 0 0 0 3px rgba(15, 143, 85, 0.13);
|
|
466
|
+
outline: 0;
|
|
467
|
+
}
|
|
468
|
+
.spinner-wrap:has(.spinner:not(:empty)) {
|
|
410
469
|
margin-bottom: var(--space-1);
|
|
411
470
|
}
|
|
412
471
|
.spinner:not(:empty)::before {
|
|
@@ -421,6 +480,38 @@
|
|
|
421
480
|
background: var(--accent);
|
|
422
481
|
animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
423
482
|
}
|
|
483
|
+
.spinner.notify-enabled {
|
|
484
|
+
color: #0b6b43;
|
|
485
|
+
background: #e6f4eb;
|
|
486
|
+
border-color: #4ca56f;
|
|
487
|
+
box-shadow:
|
|
488
|
+
inset 0 1px 2px rgba(9, 93, 55, 0.16),
|
|
489
|
+
inset 0 0 0 1px rgba(15, 143, 85, 0.1);
|
|
490
|
+
}
|
|
491
|
+
.toast {
|
|
492
|
+
position: absolute;
|
|
493
|
+
left: 0;
|
|
494
|
+
bottom: 100%;
|
|
495
|
+
z-index: 5;
|
|
496
|
+
width: max-content;
|
|
497
|
+
max-width: min(320px, calc(100vw - var(--space-6) * 2));
|
|
498
|
+
border: 1px solid #bfd6c8;
|
|
499
|
+
border-radius: var(--radius-sm);
|
|
500
|
+
background: #f6fbf8;
|
|
501
|
+
color: var(--ink);
|
|
502
|
+
box-shadow: 0 8px 20px rgba(25, 35, 31, 0.12);
|
|
503
|
+
padding: var(--space-4) var(--space-6);
|
|
504
|
+
font-size: 12px;
|
|
505
|
+
opacity: 0;
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
transform: translateY(4px);
|
|
508
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
509
|
+
margin-bottom: var(--space-3);
|
|
510
|
+
}
|
|
511
|
+
.toast.visible {
|
|
512
|
+
opacity: 1;
|
|
513
|
+
transform: translateY(0);
|
|
514
|
+
}
|
|
424
515
|
@keyframes spinner-pulse {
|
|
425
516
|
0%, 100% {
|
|
426
517
|
opacity: 0.45;
|
|
@@ -445,11 +536,11 @@
|
|
|
445
536
|
}
|
|
446
537
|
@media (prefers-reduced-motion: reduce) {
|
|
447
538
|
.entry.thinking,
|
|
448
|
-
.tab
|
|
539
|
+
.tab-status::before,
|
|
449
540
|
.spinner:not(:empty)::before {
|
|
450
541
|
animation: none;
|
|
451
542
|
}
|
|
452
|
-
.tab
|
|
543
|
+
.tab-status::before,
|
|
453
544
|
.spinner:not(:empty)::before {
|
|
454
545
|
opacity: 0.8;
|
|
455
546
|
transform: none;
|
|
@@ -485,8 +576,16 @@
|
|
|
485
576
|
</div>
|
|
486
577
|
<div id="log" class="log"></div>
|
|
487
578
|
<form id="composer" class="composer">
|
|
488
|
-
<div
|
|
489
|
-
|
|
579
|
+
<div class="spinner-wrap">
|
|
580
|
+
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
581
|
+
<button id="spinner" class="spinner" type="button" role="checkbox" aria-checked="false" aria-live="polite"></button>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="prompt-row">
|
|
584
|
+
<div id="contextMeter" class="context-meter hidden" aria-hidden="true">
|
|
585
|
+
<span id="contextMeterValue" class="context-meter-value"></span>
|
|
586
|
+
</div>
|
|
587
|
+
<textarea id="prompt" rows="1" placeholder="Ask pycodex or type /help..."></textarea>
|
|
588
|
+
</div>
|
|
490
589
|
</form>
|
|
491
590
|
</section>
|
|
492
591
|
</div>
|
|
@@ -495,21 +594,25 @@
|
|
|
495
594
|
<script>
|
|
496
595
|
const log = document.getElementById("log");
|
|
497
596
|
const spinner = document.getElementById("spinner");
|
|
597
|
+
const toast = document.getElementById("toast");
|
|
498
598
|
const prompt = document.getElementById("prompt");
|
|
499
599
|
const form = document.getElementById("composer");
|
|
500
600
|
const boardFrame = document.getElementById("boardFrame");
|
|
501
601
|
const workspace = document.querySelector(".workspace");
|
|
502
602
|
const splitter = document.getElementById("splitter");
|
|
503
603
|
const tabsEl = document.getElementById("tabs");
|
|
604
|
+
const contextMeter = document.getElementById("contextMeter");
|
|
605
|
+
const contextMeterValue = document.getElementById("contextMeterValue");
|
|
504
606
|
const newTabButton = document.getElementById("newTab");
|
|
505
607
|
let pollTimer = null;
|
|
506
608
|
let boardPollTimer = null;
|
|
507
609
|
let pollInFlight = false;
|
|
508
610
|
let boardPollInFlight = false;
|
|
509
|
-
let lastBoardSignature =
|
|
611
|
+
let lastBoardSignature = null;
|
|
510
612
|
let lastRenderedSignature = "";
|
|
511
613
|
let resizeStart = null;
|
|
512
614
|
let spinnerText = "";
|
|
615
|
+
let toastTimer = null;
|
|
513
616
|
let activeSessionId = "";
|
|
514
617
|
let sessions = [];
|
|
515
618
|
let isComposing = false;
|
|
@@ -627,9 +730,128 @@
|
|
|
627
730
|
event.preventDefault();
|
|
628
731
|
}
|
|
629
732
|
|
|
630
|
-
function setSpinner(text) {
|
|
631
|
-
|
|
733
|
+
function setSpinner(text, options = {}) {
|
|
734
|
+
const previousText = spinnerText;
|
|
735
|
+
const nextText = String(text || "").trim();
|
|
736
|
+
const wasRunning = Boolean(previousText);
|
|
737
|
+
const isRunning = Boolean(nextText);
|
|
738
|
+
spinnerText = nextText;
|
|
632
739
|
spinner.textContent = spinnerText;
|
|
740
|
+
if (wasRunning && !isRunning && !options.suppressDoneNotification) {
|
|
741
|
+
const state = stateForSession(activeSessionId);
|
|
742
|
+
if (state.wasRunning) notifySessionDone(activeSessionId);
|
|
743
|
+
}
|
|
744
|
+
updateSpinnerNotifyUi();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function showToast(text) {
|
|
748
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
749
|
+
toast.textContent = text;
|
|
750
|
+
toast.classList.add("visible");
|
|
751
|
+
toastTimer = setTimeout(() => {
|
|
752
|
+
toastTimer = null;
|
|
753
|
+
toast.classList.remove("visible");
|
|
754
|
+
}, 1400);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function updateSpinnerNotifyUi() {
|
|
758
|
+
const state = stateForSession(activeSessionId);
|
|
759
|
+
const enabled = Boolean(spinnerText && state.notifyWhenDone);
|
|
760
|
+
spinner.classList.toggle("notify-enabled", enabled);
|
|
761
|
+
spinner.setAttribute("aria-checked", enabled ? "true" : "false");
|
|
762
|
+
if (!spinnerText) {
|
|
763
|
+
spinner.title = "Spinner is idle";
|
|
764
|
+
} else if (enabled) {
|
|
765
|
+
spinner.title = "Notify when done is on";
|
|
766
|
+
} else {
|
|
767
|
+
spinner.title = "Notify when done is off";
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function notificationAvailable() {
|
|
772
|
+
return "Notification" in window;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function requestNotificationPermission() {
|
|
776
|
+
try {
|
|
777
|
+
const requestResult = Notification.requestPermission();
|
|
778
|
+
if (requestResult && typeof requestResult.then === "function") {
|
|
779
|
+
return requestResult;
|
|
780
|
+
}
|
|
781
|
+
return Promise.resolve(Notification.permission);
|
|
782
|
+
} catch (_error) {
|
|
783
|
+
return Promise.resolve(Notification.permission);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function notifySessionDone(sessionId, sessionSummary = null) {
|
|
788
|
+
if (!sessionId) return;
|
|
789
|
+
const state = stateForSession(sessionId);
|
|
790
|
+
if (!state.notifyWhenDone) return;
|
|
791
|
+
state.notifyWhenDone = false;
|
|
792
|
+
if (!notificationAvailable() || Notification.permission !== "granted") return;
|
|
793
|
+
const session = sessionSummary || sessionById(sessionId);
|
|
794
|
+
const title = (session && session.title) || "pycodex";
|
|
795
|
+
const lastAssistant = (session && session.last_assistant) || "Done.";
|
|
796
|
+
try {
|
|
797
|
+
new Notification(`session: ${title}`, {
|
|
798
|
+
body: lastAssistant,
|
|
799
|
+
tag: `pycodex-workspace-done-${sessionId}`,
|
|
800
|
+
});
|
|
801
|
+
} catch (_error) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function sessionById(sessionId) {
|
|
807
|
+
return sessions.find((session) => (session.id || "") === sessionId) || null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function isSessionRunning(sessionId) {
|
|
811
|
+
if (sessionId === activeSessionId && spinnerText) return true;
|
|
812
|
+
const session = sessionById(sessionId);
|
|
813
|
+
if (session) return Boolean(session.running);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function toggleSpinnerNotification() {
|
|
818
|
+
const sessionId = activeSessionId;
|
|
819
|
+
if (!sessionId || !isSessionRunning(sessionId)) {
|
|
820
|
+
showToast("notify-on-done unavailable");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const state = stateForSession(sessionId);
|
|
824
|
+
if (state.notifyWhenDone) {
|
|
825
|
+
state.notifyWhenDone = false;
|
|
826
|
+
showToast("notify-on-done canceled");
|
|
827
|
+
updateSpinnerNotifyUi();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (!notificationAvailable()) {
|
|
831
|
+
showToast("browser notifications unavailable");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (Notification.permission === "denied") {
|
|
835
|
+
showToast("browser notifications blocked");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
state.notifyWhenDone = true;
|
|
839
|
+
showToast("notify-on-done enabled");
|
|
840
|
+
updateSpinnerNotifyUi();
|
|
841
|
+
if (Notification.permission !== "granted") {
|
|
842
|
+
const permission = await requestNotificationPermission();
|
|
843
|
+
if (permission !== "granted") {
|
|
844
|
+
state.notifyWhenDone = false;
|
|
845
|
+
showToast("notify-on-done canceled");
|
|
846
|
+
updateSpinnerNotifyUi();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!isSessionRunning(sessionId)) {
|
|
851
|
+
state.notifyWhenDone = false;
|
|
852
|
+
showToast("notify-on-done canceled");
|
|
853
|
+
}
|
|
854
|
+
updateSpinnerNotifyUi();
|
|
633
855
|
}
|
|
634
856
|
|
|
635
857
|
function isNearBottom() {
|
|
@@ -642,16 +864,22 @@
|
|
|
642
864
|
|
|
643
865
|
function resizePrompt() {
|
|
644
866
|
prompt.style.height = "auto";
|
|
645
|
-
const
|
|
646
|
-
const maxHeight = Math.
|
|
647
|
-
const nextHeight = Math.
|
|
867
|
+
const borderHeight = prompt.offsetHeight - prompt.clientHeight;
|
|
868
|
+
const maxHeight = Math.round(window.innerHeight * 0.35);
|
|
869
|
+
const nextHeight = Math.min(prompt.scrollHeight + borderHeight, maxHeight);
|
|
648
870
|
prompt.style.height = `${nextHeight}px`;
|
|
649
871
|
}
|
|
650
872
|
|
|
651
873
|
function stateForSession(sessionId) {
|
|
652
874
|
const key = String(sessionId || "");
|
|
653
875
|
if (!sessionState.has(key)) {
|
|
654
|
-
sessionState.set(key, {
|
|
876
|
+
sessionState.set(key, {
|
|
877
|
+
lastRenderedSignature: "",
|
|
878
|
+
scrollTop: null,
|
|
879
|
+
draft: "",
|
|
880
|
+
notifyWhenDone: false,
|
|
881
|
+
wasRunning: false,
|
|
882
|
+
});
|
|
655
883
|
}
|
|
656
884
|
return sessionState.get(key);
|
|
657
885
|
}
|
|
@@ -667,11 +895,27 @@
|
|
|
667
895
|
}
|
|
668
896
|
|
|
669
897
|
function updateSessions(nextSessions) {
|
|
670
|
-
|
|
898
|
+
const incomingSessions = Array.isArray(nextSessions) ? nextSessions : sessions;
|
|
899
|
+
incomingSessions.forEach((session) => {
|
|
900
|
+
const sessionId = session.id || "";
|
|
901
|
+
if (!sessionId) return;
|
|
902
|
+
const state = stateForSession(sessionId);
|
|
903
|
+
const wasRunning = Boolean(state.wasRunning);
|
|
904
|
+
const isRunning = Boolean(session.running);
|
|
905
|
+
if (!wasRunning && isRunning) {
|
|
906
|
+
state.notifyWhenDone = false;
|
|
907
|
+
} else if (wasRunning && !isRunning) {
|
|
908
|
+
notifySessionDone(sessionId, session);
|
|
909
|
+
}
|
|
910
|
+
state.wasRunning = isRunning;
|
|
911
|
+
});
|
|
912
|
+
sessions = incomingSessions;
|
|
671
913
|
if (!activeSessionId && sessions.length) {
|
|
672
914
|
activeSessionId = sessions[0].id || "";
|
|
673
915
|
}
|
|
674
916
|
renderTabs();
|
|
917
|
+
updateContextMeter();
|
|
918
|
+
updateSpinnerNotifyUi();
|
|
675
919
|
}
|
|
676
920
|
|
|
677
921
|
function renderTabs() {
|
|
@@ -689,6 +933,13 @@
|
|
|
689
933
|
if (session.running) tab.setAttribute("aria-busy", "true");
|
|
690
934
|
tab.addEventListener("click", () => switchSession(sessionId));
|
|
691
935
|
|
|
936
|
+
if (session.running) {
|
|
937
|
+
const status = document.createElement("span");
|
|
938
|
+
status.className = "tab-status";
|
|
939
|
+
status.setAttribute("aria-hidden", "true");
|
|
940
|
+
tab.appendChild(status);
|
|
941
|
+
}
|
|
942
|
+
|
|
692
943
|
const label = document.createElement("span");
|
|
693
944
|
label.className = "tab-label";
|
|
694
945
|
label.textContent = session.title || `Session ${index + 1}`;
|
|
@@ -710,6 +961,38 @@
|
|
|
710
961
|
});
|
|
711
962
|
}
|
|
712
963
|
|
|
964
|
+
function updateContextMeter(snapshot = null) {
|
|
965
|
+
let percent = null;
|
|
966
|
+
if (snapshot && snapshot.context_remaining_percent !== undefined) {
|
|
967
|
+
percent = snapshot.context_remaining_percent;
|
|
968
|
+
} else {
|
|
969
|
+
const session = sessionById(activeSessionId);
|
|
970
|
+
if (session && session.context_remaining_percent !== undefined) {
|
|
971
|
+
percent = session.context_remaining_percent;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (percent === null || percent === undefined || percent === "") {
|
|
975
|
+
contextMeter.classList.add("hidden");
|
|
976
|
+
contextMeterValue.textContent = "";
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const value = Math.max(0, Math.min(100, Math.round(Number(percent))));
|
|
980
|
+
if (!Number.isFinite(value)) {
|
|
981
|
+
contextMeter.classList.add("hidden");
|
|
982
|
+
contextMeterValue.textContent = "";
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (value >= 100) {
|
|
986
|
+
contextMeter.classList.remove("hidden");
|
|
987
|
+
contextMeter.style.setProperty("--context-remaining", `${value}%`);
|
|
988
|
+
contextMeterValue.textContent = "";
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
contextMeter.classList.remove("hidden");
|
|
992
|
+
contextMeter.style.setProperty("--context-remaining", `${value}%`);
|
|
993
|
+
contextMeterValue.textContent = String(value);
|
|
994
|
+
}
|
|
995
|
+
|
|
713
996
|
function messageSignature(snapshot) {
|
|
714
997
|
const turns = (snapshot && snapshot.turns) || [];
|
|
715
998
|
return JSON.stringify(
|
|
@@ -771,10 +1054,14 @@
|
|
|
771
1054
|
const response = await fetch(relativeUrl("api/board"));
|
|
772
1055
|
if (!response.ok) return;
|
|
773
1056
|
const payload = await response.json();
|
|
774
|
-
if (!payload.exists)
|
|
1057
|
+
if (!payload.exists) {
|
|
1058
|
+
lastBoardSignature = null;
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
775
1061
|
const signature = `${payload.mtime_ns || ""}:${payload.size || ""}`;
|
|
776
|
-
if (
|
|
1062
|
+
if (lastBoardSignature === null) {
|
|
777
1063
|
lastBoardSignature = signature;
|
|
1064
|
+
boardFrame.src = `board?t=${Date.now()}`;
|
|
778
1065
|
return;
|
|
779
1066
|
}
|
|
780
1067
|
if (signature !== lastBoardSignature) {
|
|
@@ -800,6 +1087,7 @@
|
|
|
800
1087
|
const signature = messageSignature(snapshot);
|
|
801
1088
|
if (signature === state.lastRenderedSignature) {
|
|
802
1089
|
setSpinner(snapshot.spinner || "");
|
|
1090
|
+
updateContextMeter(snapshot);
|
|
803
1091
|
return;
|
|
804
1092
|
}
|
|
805
1093
|
state.lastRenderedSignature = signature;
|
|
@@ -814,6 +1102,7 @@
|
|
|
814
1102
|
turns.forEach((turn) => renderTurn(turn, shouldStick));
|
|
815
1103
|
}
|
|
816
1104
|
setSpinner(snapshot.spinner || "");
|
|
1105
|
+
updateContextMeter(snapshot);
|
|
817
1106
|
if (restoreScrollTop !== null) {
|
|
818
1107
|
log.scrollTop = Math.min(
|
|
819
1108
|
restoreScrollTop,
|
|
@@ -889,7 +1178,7 @@
|
|
|
889
1178
|
suppressScrollSave = true;
|
|
890
1179
|
log.textContent = "";
|
|
891
1180
|
suppressScrollSave = false;
|
|
892
|
-
setSpinner("");
|
|
1181
|
+
setSpinner("", {suppressDoneNotification: true});
|
|
893
1182
|
restoreScrollTop = state.scrollTop;
|
|
894
1183
|
await pollSession();
|
|
895
1184
|
renderTabs();
|
|
@@ -984,12 +1273,18 @@
|
|
|
984
1273
|
splitter.addEventListener("pointerup", endResize);
|
|
985
1274
|
splitter.addEventListener("pointercancel", endResize);
|
|
986
1275
|
splitter.addEventListener("dblclick", centerSplit);
|
|
1276
|
+
spinner.addEventListener("click", toggleSpinnerNotification);
|
|
987
1277
|
newTabButton.addEventListener("click", createSession);
|
|
988
1278
|
window.addEventListener("resize", () => {
|
|
989
1279
|
const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
|
|
990
1280
|
if (current > 0) setChatWidth(current);
|
|
991
1281
|
resizePrompt();
|
|
992
1282
|
});
|
|
1283
|
+
document.addEventListener("visibilitychange", () => {
|
|
1284
|
+
if (document.visibilityState !== "visible") return;
|
|
1285
|
+
pollSession();
|
|
1286
|
+
pollBoard();
|
|
1287
|
+
});
|
|
993
1288
|
|
|
994
1289
|
loadChatWidth();
|
|
995
1290
|
resizePrompt();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|