python-codex 0.2.0__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.0.dist-info → python_codex-0.2.2.dist-info}/METADATA +1 -1
- {python_codex-0.2.0.dist-info → python_codex-0.2.2.dist-info}/RECORD +8 -8
- workspace_server/app.py +51 -2
- workspace_server/workspace.html +527 -98
- {python_codex-0.2.0.dist-info → python_codex-0.2.2.dist-info}/WHEEL +0 -0
- {python_codex-0.2.0.dist-info → python_codex-0.2.2.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.0.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
|
@@ -7,14 +7,31 @@
|
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
9
|
color-scheme: light;
|
|
10
|
-
--bg: #
|
|
10
|
+
--bg: #f2f6f3;
|
|
11
11
|
--panel: #ffffff;
|
|
12
|
-
--ink: #
|
|
13
|
-
--muted: #
|
|
14
|
-
--line: #
|
|
15
|
-
--accent: #
|
|
12
|
+
--ink: #1e2529;
|
|
13
|
+
--muted: #5f6f75;
|
|
14
|
+
--line: #d2ddd5;
|
|
15
|
+
--accent: #0f8f55;
|
|
16
16
|
--accent-ink: #ffffff;
|
|
17
|
-
--
|
|
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);
|
|
19
36
|
--splitter-width: 8px;
|
|
20
37
|
}
|
|
@@ -34,7 +51,7 @@
|
|
|
34
51
|
min-width: 0;
|
|
35
52
|
}
|
|
36
53
|
.workspace.collapsed .splitter::after {
|
|
37
|
-
background: #
|
|
54
|
+
background: #4f6f60;
|
|
38
55
|
}
|
|
39
56
|
.board-pane {
|
|
40
57
|
min-width: 0;
|
|
@@ -46,7 +63,7 @@
|
|
|
46
63
|
.boardbar {
|
|
47
64
|
min-width: 0;
|
|
48
65
|
border-bottom: 1px solid var(--line);
|
|
49
|
-
padding:
|
|
66
|
+
padding: var(--space-4) var(--space-6);
|
|
50
67
|
color: var(--muted);
|
|
51
68
|
font-size: 12px;
|
|
52
69
|
white-space: nowrap;
|
|
@@ -61,24 +78,36 @@
|
|
|
61
78
|
background: #fff;
|
|
62
79
|
}
|
|
63
80
|
.splitter {
|
|
81
|
+
position: relative;
|
|
64
82
|
width: 8px;
|
|
65
83
|
height: 100dvh;
|
|
66
84
|
border: 0;
|
|
67
|
-
border-left: 1px solid
|
|
68
|
-
border-right: 1px solid
|
|
69
|
-
background: #
|
|
85
|
+
border-left: 1px solid #c1cec6;
|
|
86
|
+
border-right: 1px solid #c1cec6;
|
|
87
|
+
background: #dfe8e2;
|
|
70
88
|
cursor: col-resize;
|
|
71
89
|
padding: 0;
|
|
72
90
|
touch-action: none;
|
|
73
91
|
}
|
|
74
92
|
.splitter::after {
|
|
75
93
|
content: "";
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
position: absolute;
|
|
95
|
+
left: 50%;
|
|
96
|
+
top: 50%;
|
|
97
|
+
width: 3px;
|
|
78
98
|
height: 42px;
|
|
79
|
-
margin: calc(50dvh - 21px) auto 0;
|
|
80
99
|
border-radius: 999px;
|
|
81
|
-
background: #
|
|
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;
|
|
82
111
|
}
|
|
83
112
|
.workspace.resizing,
|
|
84
113
|
.workspace.resizing * {
|
|
@@ -96,37 +125,68 @@
|
|
|
96
125
|
}
|
|
97
126
|
.topbar {
|
|
98
127
|
min-width: 0;
|
|
99
|
-
padding:
|
|
128
|
+
padding: var(--space-4) var(--space-7);
|
|
100
129
|
border-bottom: 1px solid var(--line);
|
|
101
130
|
display: grid;
|
|
102
|
-
gap:
|
|
131
|
+
gap: var(--space-4);
|
|
103
132
|
}
|
|
104
133
|
.tabbar {
|
|
105
134
|
display: flex;
|
|
106
135
|
align-items: center;
|
|
107
|
-
gap:
|
|
136
|
+
gap: var(--space-2);
|
|
108
137
|
min-width: 0;
|
|
109
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
|
+
}
|
|
110
169
|
.tabs {
|
|
111
170
|
display: flex;
|
|
112
171
|
align-items: center;
|
|
113
|
-
gap:
|
|
172
|
+
gap: var(--space-1);
|
|
114
173
|
min-width: 0;
|
|
115
174
|
overflow-x: auto;
|
|
116
175
|
scrollbar-width: thin;
|
|
117
176
|
}
|
|
118
177
|
.tab {
|
|
178
|
+
position: relative;
|
|
119
179
|
display: inline-flex;
|
|
120
180
|
align-items: center;
|
|
121
|
-
gap:
|
|
181
|
+
gap: var(--space-2);
|
|
122
182
|
min-width: 88px;
|
|
123
183
|
max-width: 180px;
|
|
124
|
-
height:
|
|
125
|
-
border: 1px solid
|
|
126
|
-
border-radius:
|
|
127
|
-
background: #
|
|
184
|
+
height: 31px;
|
|
185
|
+
border: 1px solid #d5ddd7;
|
|
186
|
+
border-radius: var(--radius-sm);
|
|
187
|
+
background: #f6f8f7;
|
|
128
188
|
color: var(--ink);
|
|
129
|
-
padding: 0
|
|
189
|
+
padding: 0 var(--space-3) 0 var(--space-4);
|
|
130
190
|
font: inherit;
|
|
131
191
|
cursor: pointer;
|
|
132
192
|
user-select: none;
|
|
@@ -137,8 +197,30 @@
|
|
|
137
197
|
outline-offset: 1px;
|
|
138
198
|
}
|
|
139
199
|
.tab.active {
|
|
140
|
-
border-color: #
|
|
141
|
-
background: #
|
|
200
|
+
border-color: #afd6bd;
|
|
201
|
+
background: #eef7f1;
|
|
202
|
+
box-shadow: inset 0 -2px 0 var(--accent);
|
|
203
|
+
}
|
|
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 {
|
|
218
|
+
content: "";
|
|
219
|
+
width: 5px;
|
|
220
|
+
height: 5px;
|
|
221
|
+
border-radius: 999px;
|
|
222
|
+
background: var(--accent);
|
|
223
|
+
animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
142
224
|
}
|
|
143
225
|
.tab-label {
|
|
144
226
|
min-width: 0;
|
|
@@ -150,11 +232,11 @@
|
|
|
150
232
|
}
|
|
151
233
|
.tab-close,
|
|
152
234
|
.tab-new {
|
|
153
|
-
width:
|
|
154
|
-
height:
|
|
235
|
+
width: 31px;
|
|
236
|
+
height: 31px;
|
|
155
237
|
border: 1px solid var(--line);
|
|
156
|
-
border-radius:
|
|
157
|
-
background: #
|
|
238
|
+
border-radius: var(--radius-md);
|
|
239
|
+
background: #eef7f1;
|
|
158
240
|
color: var(--muted);
|
|
159
241
|
padding: 0;
|
|
160
242
|
line-height: 1;
|
|
@@ -166,56 +248,79 @@
|
|
|
166
248
|
height: 18px;
|
|
167
249
|
border: 0;
|
|
168
250
|
background: transparent;
|
|
251
|
+
opacity: 0.42;
|
|
252
|
+
font-size: 17px;
|
|
253
|
+
font-weight: 500;
|
|
254
|
+
line-height: 18px;
|
|
255
|
+
}
|
|
256
|
+
.tab-new {
|
|
257
|
+
font-size: 22px;
|
|
258
|
+
font-weight: 400;
|
|
259
|
+
line-height: 31px;
|
|
260
|
+
}
|
|
261
|
+
.tab:hover .tab-close,
|
|
262
|
+
.tab:focus-within .tab-close,
|
|
263
|
+
.tab.active .tab-close {
|
|
264
|
+
opacity: 0.85;
|
|
265
|
+
}
|
|
266
|
+
.tab-close:disabled {
|
|
267
|
+
opacity: 0.22;
|
|
268
|
+
cursor: default;
|
|
169
269
|
}
|
|
170
270
|
.tab-close:hover,
|
|
171
271
|
.tab-new:hover {
|
|
172
272
|
color: var(--ink);
|
|
173
|
-
background: #
|
|
273
|
+
background: #ffffff;
|
|
174
274
|
}
|
|
175
275
|
.log {
|
|
176
276
|
min-height: 0;
|
|
177
277
|
overflow-y: auto;
|
|
178
278
|
overflow-x: hidden;
|
|
179
|
-
padding:
|
|
279
|
+
padding: var(--space-6) var(--space-7);
|
|
180
280
|
display: flex;
|
|
181
281
|
flex-direction: column;
|
|
182
|
-
gap:
|
|
282
|
+
gap: var(--space-4);
|
|
183
283
|
background: #fbfcfa;
|
|
184
284
|
}
|
|
185
285
|
.entry {
|
|
186
286
|
min-width: 0;
|
|
187
287
|
border: 1px solid var(--line);
|
|
188
|
-
border-radius:
|
|
189
|
-
padding:
|
|
288
|
+
border-radius: var(--radius-sm);
|
|
289
|
+
padding: var(--space-4) var(--space-5);
|
|
190
290
|
background: #fff;
|
|
291
|
+
box-shadow: var(--entry-shadow);
|
|
191
292
|
}
|
|
192
293
|
.entry.user {
|
|
193
|
-
border-color: #
|
|
194
|
-
background:
|
|
294
|
+
border-color: #edd17a;
|
|
295
|
+
background: var(--warn-soft);
|
|
296
|
+
color: #5a4714;
|
|
195
297
|
}
|
|
196
298
|
.entry.response,
|
|
197
299
|
.entry.assistant {
|
|
198
|
-
border-color: #
|
|
300
|
+
border-color: #d0ddd5;
|
|
199
301
|
background: #ffffff;
|
|
200
302
|
}
|
|
201
303
|
.entry.thinking {
|
|
202
|
-
border-color: #
|
|
203
|
-
background: #
|
|
304
|
+
border-color: #b9dcc5;
|
|
305
|
+
background: #f0fbf3;
|
|
306
|
+
color: #256b43;
|
|
307
|
+
animation: thinking-breathe 4.8s ease-in-out infinite;
|
|
204
308
|
}
|
|
205
309
|
.entry.control {
|
|
206
|
-
border-color: #
|
|
207
|
-
background:
|
|
310
|
+
border-color: #b9d8ea;
|
|
311
|
+
background: var(--info-soft);
|
|
312
|
+
color: var(--info);
|
|
208
313
|
}
|
|
209
314
|
.entry.tool {
|
|
210
|
-
border-color: #
|
|
211
|
-
background:
|
|
212
|
-
color:
|
|
315
|
+
border-color: #d7c8f0;
|
|
316
|
+
background: var(--violet-soft);
|
|
317
|
+
color: var(--violet);
|
|
213
318
|
font-size: 12px;
|
|
214
319
|
}
|
|
215
320
|
.entry.error {
|
|
216
|
-
border-color: #
|
|
217
|
-
background:
|
|
218
|
-
color:
|
|
321
|
+
border-color: #efb4ad;
|
|
322
|
+
background: var(--danger-soft);
|
|
323
|
+
color: var(--danger);
|
|
219
324
|
}
|
|
220
325
|
.text {
|
|
221
326
|
white-space: pre-wrap;
|
|
@@ -226,7 +331,7 @@
|
|
|
226
331
|
overflow-wrap: anywhere;
|
|
227
332
|
}
|
|
228
333
|
.markdown p {
|
|
229
|
-
margin: 0 0
|
|
334
|
+
margin: 0 0 var(--space-4);
|
|
230
335
|
}
|
|
231
336
|
.markdown p:last-child,
|
|
232
337
|
.markdown ul:last-child,
|
|
@@ -237,17 +342,17 @@
|
|
|
237
342
|
}
|
|
238
343
|
.markdown ul,
|
|
239
344
|
.markdown ol {
|
|
240
|
-
margin: 0 0
|
|
345
|
+
margin: 0 0 var(--space-4) 18px;
|
|
241
346
|
padding: 0;
|
|
242
347
|
}
|
|
243
348
|
.markdown li {
|
|
244
349
|
margin: 2px 0;
|
|
245
350
|
}
|
|
246
351
|
.markdown pre {
|
|
247
|
-
margin: 0 0
|
|
248
|
-
padding:
|
|
352
|
+
margin: 0 0 var(--space-4);
|
|
353
|
+
padding: var(--space-4) var(--space-5);
|
|
249
354
|
border: 1px solid var(--line);
|
|
250
|
-
border-radius:
|
|
355
|
+
border-radius: var(--radius-sm);
|
|
251
356
|
background: var(--code);
|
|
252
357
|
overflow-x: auto;
|
|
253
358
|
white-space: pre;
|
|
@@ -260,69 +365,191 @@
|
|
|
260
365
|
}
|
|
261
366
|
.markdown table {
|
|
262
367
|
width: 100%;
|
|
263
|
-
margin: 0 0
|
|
368
|
+
margin: 0 0 var(--space-4);
|
|
264
369
|
border-collapse: collapse;
|
|
265
370
|
font-size: 13px;
|
|
266
371
|
}
|
|
267
372
|
.markdown th,
|
|
268
373
|
.markdown td {
|
|
269
374
|
border: 1px solid var(--line);
|
|
270
|
-
padding:
|
|
375
|
+
padding: var(--space-3) var(--space-4);
|
|
271
376
|
text-align: left;
|
|
272
377
|
vertical-align: top;
|
|
273
378
|
}
|
|
274
379
|
.markdown th {
|
|
275
|
-
background: #
|
|
380
|
+
background: #eef7f1;
|
|
276
381
|
font-weight: 650;
|
|
277
382
|
}
|
|
278
383
|
.markdown a {
|
|
279
|
-
color: #
|
|
384
|
+
color: #176fa8;
|
|
280
385
|
text-decoration: none;
|
|
281
386
|
}
|
|
282
387
|
.markdown a:hover {
|
|
283
388
|
text-decoration: underline;
|
|
284
389
|
}
|
|
285
390
|
.composer {
|
|
391
|
+
position: relative;
|
|
286
392
|
min-width: 0;
|
|
287
|
-
|
|
288
|
-
padding: 12px 12px 28px;
|
|
393
|
+
padding: var(--space-4);
|
|
289
394
|
display: grid;
|
|
290
|
-
gap:
|
|
395
|
+
gap: var(--space-3);
|
|
396
|
+
align-content: end;
|
|
291
397
|
background: #fff;
|
|
292
398
|
}
|
|
293
399
|
textarea {
|
|
294
400
|
min-width: 0;
|
|
295
401
|
resize: vertical;
|
|
296
|
-
min-height: 88px;
|
|
297
402
|
max-height: 35vh;
|
|
298
403
|
width: 100%;
|
|
299
404
|
border: 1px solid var(--line);
|
|
300
|
-
border-radius:
|
|
301
|
-
padding:
|
|
405
|
+
border-radius: var(--radius-sm);
|
|
406
|
+
padding: var(--space-4) var(--space-5);
|
|
407
|
+
background: #ffffff;
|
|
408
|
+
color: var(--ink);
|
|
302
409
|
font: inherit;
|
|
303
410
|
line-height: 1.4;
|
|
411
|
+
overflow-y: auto;
|
|
412
|
+
}
|
|
413
|
+
textarea:focus {
|
|
414
|
+
border-color: #65b782;
|
|
415
|
+
box-shadow: 0 0 0 3px rgba(15, 143, 85, 0.13);
|
|
416
|
+
outline: 0;
|
|
304
417
|
}
|
|
305
418
|
.actions {
|
|
306
419
|
display: flex;
|
|
307
420
|
justify-content: flex-end;
|
|
308
421
|
align-items: center;
|
|
309
422
|
}
|
|
423
|
+
.spinner-wrap {
|
|
424
|
+
position: relative;
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
justify-self: start;
|
|
428
|
+
align-self: end;
|
|
429
|
+
max-width: 100%;
|
|
430
|
+
}
|
|
431
|
+
.prompt-row {
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
gap: var(--space-2);
|
|
435
|
+
min-width: 0;
|
|
436
|
+
}
|
|
310
437
|
.spinner {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
444
|
color: var(--muted);
|
|
317
445
|
font-size: 12px;
|
|
318
446
|
white-space: nowrap;
|
|
319
447
|
overflow: hidden;
|
|
320
448
|
text-overflow: ellipsis;
|
|
449
|
+
padding: var(--space-1) var(--space-4) var(--space-1) 18px;
|
|
450
|
+
border-radius: 999px;
|
|
451
|
+
background: rgba(240, 250, 244, 0.96);
|
|
452
|
+
border: 1px solid rgba(172, 216, 188, 0.9);
|
|
453
|
+
font: inherit;
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
}
|
|
457
|
+
.spinner:empty {
|
|
458
|
+
display: none;
|
|
459
|
+
}
|
|
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)) {
|
|
469
|
+
margin-bottom: var(--space-1);
|
|
470
|
+
}
|
|
471
|
+
.spinner:not(:empty)::before {
|
|
472
|
+
content: "";
|
|
473
|
+
position: absolute;
|
|
474
|
+
left: 7px;
|
|
475
|
+
top: 50%;
|
|
476
|
+
width: 6px;
|
|
477
|
+
height: 6px;
|
|
478
|
+
margin-top: -3px;
|
|
479
|
+
border-radius: 999px;
|
|
480
|
+
background: var(--accent);
|
|
481
|
+
animation: spinner-pulse 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
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
|
+
}
|
|
515
|
+
@keyframes spinner-pulse {
|
|
516
|
+
0%, 100% {
|
|
517
|
+
opacity: 0.45;
|
|
518
|
+
transform: scale(0.82);
|
|
519
|
+
}
|
|
520
|
+
50% {
|
|
521
|
+
opacity: 1;
|
|
522
|
+
transform: scale(1.08);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
@keyframes thinking-breathe {
|
|
526
|
+
0%, 100% {
|
|
527
|
+
border-color: #b9dcc5;
|
|
528
|
+
box-shadow: var(--entry-shadow);
|
|
529
|
+
}
|
|
530
|
+
50% {
|
|
531
|
+
border-color: #72d494;
|
|
532
|
+
box-shadow:
|
|
533
|
+
0 0 0 2px rgba(22, 168, 90, 0.16),
|
|
534
|
+
0 2px 7px rgba(25, 35, 31, 0.08);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
@media (prefers-reduced-motion: reduce) {
|
|
538
|
+
.entry.thinking,
|
|
539
|
+
.tab-status::before,
|
|
540
|
+
.spinner:not(:empty)::before {
|
|
541
|
+
animation: none;
|
|
542
|
+
}
|
|
543
|
+
.tab-status::before,
|
|
544
|
+
.spinner:not(:empty)::before {
|
|
545
|
+
opacity: 0.8;
|
|
546
|
+
transform: none;
|
|
547
|
+
}
|
|
321
548
|
}
|
|
322
549
|
code {
|
|
323
550
|
background: var(--code);
|
|
324
551
|
border: 1px solid var(--line);
|
|
325
|
-
border-radius:
|
|
552
|
+
border-radius: var(--radius-sm);
|
|
326
553
|
padding: 1px 4px;
|
|
327
554
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
328
555
|
font-size: 12px;
|
|
@@ -343,15 +570,23 @@
|
|
|
343
570
|
<section class="chat" aria-label="pycodex chat">
|
|
344
571
|
<div class="topbar">
|
|
345
572
|
<div class="tabbar">
|
|
346
|
-
<div id="tabs" class="tabs"></div>
|
|
573
|
+
<div id="tabs" class="tabs" role="tablist"></div>
|
|
347
574
|
<button id="newTab" class="tab-new" type="button" title="New session" aria-label="New session">+</button>
|
|
348
575
|
</div>
|
|
349
576
|
</div>
|
|
350
577
|
<div id="log" class="log"></div>
|
|
351
578
|
<form id="composer" class="composer">
|
|
352
|
-
<
|
|
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>
|
|
353
589
|
</form>
|
|
354
|
-
<div id="spinner" class="spinner"></div>
|
|
355
590
|
</section>
|
|
356
591
|
</div>
|
|
357
592
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
|
@@ -359,30 +594,31 @@
|
|
|
359
594
|
<script>
|
|
360
595
|
const log = document.getElementById("log");
|
|
361
596
|
const spinner = document.getElementById("spinner");
|
|
597
|
+
const toast = document.getElementById("toast");
|
|
362
598
|
const prompt = document.getElementById("prompt");
|
|
363
599
|
const form = document.getElementById("composer");
|
|
364
600
|
const boardFrame = document.getElementById("boardFrame");
|
|
365
601
|
const workspace = document.querySelector(".workspace");
|
|
366
602
|
const splitter = document.getElementById("splitter");
|
|
367
603
|
const tabsEl = document.getElementById("tabs");
|
|
604
|
+
const contextMeter = document.getElementById("contextMeter");
|
|
605
|
+
const contextMeterValue = document.getElementById("contextMeterValue");
|
|
368
606
|
const newTabButton = document.getElementById("newTab");
|
|
369
607
|
let pollTimer = null;
|
|
370
608
|
let boardPollTimer = null;
|
|
371
609
|
let pollInFlight = false;
|
|
372
610
|
let boardPollInFlight = false;
|
|
373
|
-
let lastBoardSignature =
|
|
611
|
+
let lastBoardSignature = null;
|
|
374
612
|
let lastRenderedSignature = "";
|
|
375
613
|
let resizeStart = null;
|
|
376
|
-
let spinnerTimer = null;
|
|
377
|
-
let spinnerFrameIndex = 0;
|
|
378
614
|
let spinnerText = "";
|
|
615
|
+
let toastTimer = null;
|
|
379
616
|
let activeSessionId = "";
|
|
380
617
|
let sessions = [];
|
|
381
618
|
let isComposing = false;
|
|
382
619
|
const sessionState = new Map();
|
|
383
620
|
let restoreScrollTop = null;
|
|
384
621
|
let suppressScrollSave = false;
|
|
385
|
-
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
386
622
|
const markdownRenderer = window.markdownit
|
|
387
623
|
? window.markdownit({html: false, linkify: true, breaks: false})
|
|
388
624
|
: null;
|
|
@@ -494,27 +730,128 @@
|
|
|
494
730
|
event.preventDefault();
|
|
495
731
|
}
|
|
496
732
|
|
|
497
|
-
function setSpinner(text) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (!
|
|
505
|
-
|
|
506
|
-
|
|
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;
|
|
739
|
+
spinner.textContent = spinnerText;
|
|
740
|
+
if (wasRunning && !isRunning && !options.suppressDoneNotification) {
|
|
741
|
+
const state = stateForSession(activeSessionId);
|
|
742
|
+
if (state.wasRunning) notifySessionDone(activeSessionId);
|
|
507
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);
|
|
508
755
|
}
|
|
509
756
|
|
|
510
|
-
function
|
|
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");
|
|
511
762
|
if (!spinnerText) {
|
|
512
|
-
spinner.
|
|
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) {
|
|
513
802
|
return;
|
|
514
803
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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();
|
|
518
855
|
}
|
|
519
856
|
|
|
520
857
|
function isNearBottom() {
|
|
@@ -525,10 +862,24 @@
|
|
|
525
862
|
log.scrollTop = log.scrollHeight;
|
|
526
863
|
}
|
|
527
864
|
|
|
865
|
+
function resizePrompt() {
|
|
866
|
+
prompt.style.height = "auto";
|
|
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);
|
|
870
|
+
prompt.style.height = `${nextHeight}px`;
|
|
871
|
+
}
|
|
872
|
+
|
|
528
873
|
function stateForSession(sessionId) {
|
|
529
874
|
const key = String(sessionId || "");
|
|
530
875
|
if (!sessionState.has(key)) {
|
|
531
|
-
sessionState.set(key, {
|
|
876
|
+
sessionState.set(key, {
|
|
877
|
+
lastRenderedSignature: "",
|
|
878
|
+
scrollTop: null,
|
|
879
|
+
draft: "",
|
|
880
|
+
notifyWhenDone: false,
|
|
881
|
+
wasRunning: false,
|
|
882
|
+
});
|
|
532
883
|
}
|
|
533
884
|
return sessionState.get(key);
|
|
534
885
|
}
|
|
@@ -544,11 +895,27 @@
|
|
|
544
895
|
}
|
|
545
896
|
|
|
546
897
|
function updateSessions(nextSessions) {
|
|
547
|
-
|
|
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;
|
|
548
913
|
if (!activeSessionId && sessions.length) {
|
|
549
914
|
activeSessionId = sessions[0].id || "";
|
|
550
915
|
}
|
|
551
916
|
renderTabs();
|
|
917
|
+
updateContextMeter();
|
|
918
|
+
updateSpinnerNotifyUi();
|
|
552
919
|
}
|
|
553
920
|
|
|
554
921
|
function renderTabs() {
|
|
@@ -556,12 +923,23 @@
|
|
|
556
923
|
sessions.forEach((session, index) => {
|
|
557
924
|
const sessionId = session.id || "";
|
|
558
925
|
const tab = document.createElement("div");
|
|
559
|
-
tab.className =
|
|
926
|
+
tab.className =
|
|
927
|
+
"tab" +
|
|
928
|
+
(sessionId === activeSessionId ? " active" : "") +
|
|
929
|
+
(session.running ? " running" : "");
|
|
560
930
|
tab.title = session.title || `Session ${index + 1}`;
|
|
561
931
|
tab.setAttribute("role", "tab");
|
|
562
932
|
tab.setAttribute("aria-selected", sessionId === activeSessionId ? "true" : "false");
|
|
933
|
+
if (session.running) tab.setAttribute("aria-busy", "true");
|
|
563
934
|
tab.addEventListener("click", () => switchSession(sessionId));
|
|
564
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
|
+
|
|
565
943
|
const label = document.createElement("span");
|
|
566
944
|
label.className = "tab-label";
|
|
567
945
|
label.textContent = session.title || `Session ${index + 1}`;
|
|
@@ -583,6 +961,38 @@
|
|
|
583
961
|
});
|
|
584
962
|
}
|
|
585
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
|
+
|
|
586
996
|
function messageSignature(snapshot) {
|
|
587
997
|
const turns = (snapshot && snapshot.turns) || [];
|
|
588
998
|
return JSON.stringify(
|
|
@@ -644,10 +1054,14 @@
|
|
|
644
1054
|
const response = await fetch(relativeUrl("api/board"));
|
|
645
1055
|
if (!response.ok) return;
|
|
646
1056
|
const payload = await response.json();
|
|
647
|
-
if (!payload.exists)
|
|
1057
|
+
if (!payload.exists) {
|
|
1058
|
+
lastBoardSignature = null;
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
648
1061
|
const signature = `${payload.mtime_ns || ""}:${payload.size || ""}`;
|
|
649
|
-
if (
|
|
1062
|
+
if (lastBoardSignature === null) {
|
|
650
1063
|
lastBoardSignature = signature;
|
|
1064
|
+
boardFrame.src = `board?t=${Date.now()}`;
|
|
651
1065
|
return;
|
|
652
1066
|
}
|
|
653
1067
|
if (signature !== lastBoardSignature) {
|
|
@@ -673,6 +1087,7 @@
|
|
|
673
1087
|
const signature = messageSignature(snapshot);
|
|
674
1088
|
if (signature === state.lastRenderedSignature) {
|
|
675
1089
|
setSpinner(snapshot.spinner || "");
|
|
1090
|
+
updateContextMeter(snapshot);
|
|
676
1091
|
return;
|
|
677
1092
|
}
|
|
678
1093
|
state.lastRenderedSignature = signature;
|
|
@@ -687,6 +1102,7 @@
|
|
|
687
1102
|
turns.forEach((turn) => renderTurn(turn, shouldStick));
|
|
688
1103
|
}
|
|
689
1104
|
setSpinner(snapshot.spinner || "");
|
|
1105
|
+
updateContextMeter(snapshot);
|
|
690
1106
|
if (restoreScrollTop !== null) {
|
|
691
1107
|
log.scrollTop = Math.min(
|
|
692
1108
|
restoreScrollTop,
|
|
@@ -758,10 +1174,11 @@
|
|
|
758
1174
|
state.lastRenderedSignature = "";
|
|
759
1175
|
lastRenderedSignature = "";
|
|
760
1176
|
prompt.value = state.draft || "";
|
|
1177
|
+
resizePrompt();
|
|
761
1178
|
suppressScrollSave = true;
|
|
762
1179
|
log.textContent = "";
|
|
763
1180
|
suppressScrollSave = false;
|
|
764
|
-
setSpinner("");
|
|
1181
|
+
setSpinner("", {suppressDoneNotification: true});
|
|
765
1182
|
restoreScrollTop = state.scrollTop;
|
|
766
1183
|
await pollSession();
|
|
767
1184
|
renderTabs();
|
|
@@ -782,6 +1199,7 @@
|
|
|
782
1199
|
state.lastRenderedSignature = "";
|
|
783
1200
|
lastRenderedSignature = "";
|
|
784
1201
|
prompt.value = "";
|
|
1202
|
+
resizePrompt();
|
|
785
1203
|
restoreScrollTop = null;
|
|
786
1204
|
updateSessions(payload.sessions || []);
|
|
787
1205
|
renderSnapshot(payload.snapshot);
|
|
@@ -803,6 +1221,7 @@
|
|
|
803
1221
|
activeSessionId = sessions.length ? sessions[0].id || "" : "";
|
|
804
1222
|
lastRenderedSignature = "";
|
|
805
1223
|
prompt.value = "";
|
|
1224
|
+
resizePrompt();
|
|
806
1225
|
suppressScrollSave = true;
|
|
807
1226
|
log.textContent = "";
|
|
808
1227
|
suppressScrollSave = false;
|
|
@@ -817,12 +1236,14 @@
|
|
|
817
1236
|
const text = prompt.value.trim();
|
|
818
1237
|
if (!text) return;
|
|
819
1238
|
prompt.value = "";
|
|
1239
|
+
resizePrompt();
|
|
820
1240
|
stateForSession(activeSessionId).draft = "";
|
|
821
1241
|
submitPrompt(text);
|
|
822
1242
|
});
|
|
823
1243
|
|
|
824
1244
|
prompt.addEventListener("input", () => {
|
|
825
1245
|
stateForSession(activeSessionId).draft = prompt.value;
|
|
1246
|
+
resizePrompt();
|
|
826
1247
|
});
|
|
827
1248
|
|
|
828
1249
|
prompt.addEventListener("compositionstart", () => {
|
|
@@ -852,13 +1273,21 @@
|
|
|
852
1273
|
splitter.addEventListener("pointerup", endResize);
|
|
853
1274
|
splitter.addEventListener("pointercancel", endResize);
|
|
854
1275
|
splitter.addEventListener("dblclick", centerSplit);
|
|
1276
|
+
spinner.addEventListener("click", toggleSpinnerNotification);
|
|
855
1277
|
newTabButton.addEventListener("click", createSession);
|
|
856
1278
|
window.addEventListener("resize", () => {
|
|
857
1279
|
const current = Number(window.localStorage.getItem("pycodex.workspace.chatWidth") || 0);
|
|
858
1280
|
if (current > 0) setChatWidth(current);
|
|
1281
|
+
resizePrompt();
|
|
1282
|
+
});
|
|
1283
|
+
document.addEventListener("visibilitychange", () => {
|
|
1284
|
+
if (document.visibilityState !== "visible") return;
|
|
1285
|
+
pollSession();
|
|
1286
|
+
pollBoard();
|
|
859
1287
|
});
|
|
860
1288
|
|
|
861
1289
|
loadChatWidth();
|
|
1290
|
+
resizePrompt();
|
|
862
1291
|
startPolling();
|
|
863
1292
|
startBoardPolling();
|
|
864
1293
|
</script>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|