zettcode-app 0.4.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.
- zettcode/app/__init__.py +63 -0
- zettcode/app/agent/__init__.py +47 -0
- zettcode/app/agent/agent/__init__.py +84 -0
- zettcode/app/agent/agent/commands.py +269 -0
- zettcode/app/agent/agent/settings.py +132 -0
- zettcode/app/agent/conversation/__init__.py +7 -0
- zettcode/app/agent/conversation/changes.py +64 -0
- zettcode/app/agent/conversation/effort_gauge.py +126 -0
- zettcode/app/agent/conversation/projection.py +265 -0
- zettcode/app/agent/conversation/rendering.py +837 -0
- zettcode/app/agent/conversation/replay.py +198 -0
- zettcode/app/agent/conversation/shimmer.py +256 -0
- zettcode/app/agent/conversation/stops.py +57 -0
- zettcode/app/agent/conversation/tips.py +86 -0
- zettcode/app/agent/conversation/transcript/__init__.py +80 -0
- zettcode/app/agent/conversation/transcript/fold.py +57 -0
- zettcode/app/agent/conversation/transcript/log.py +182 -0
- zettcode/app/agent/conversation/transcript/oneoff.py +55 -0
- zettcode/app/agent/conversation/transcript/stream.py +187 -0
- zettcode/app/agent/conversation/transcript/turn.py +323 -0
- zettcode/app/commandline/__init__.py +47 -0
- zettcode/app/commandline/arguments.py +169 -0
- zettcode/app/commandline/completion.py +262 -0
- zettcode/app/commandline/history.py +305 -0
- zettcode/app/commandline/runner.py +365 -0
- zettcode/app/errors.py +12 -0
- zettcode/app/plugins/__init__.py +110 -0
- zettcode/app/plugins/builtins.py +33 -0
- zettcode/app/plugins/groups.py +503 -0
- zettcode/app/plugins/loader.py +268 -0
- zettcode/app/py.typed +0 -0
- zettcode/app/ui/__init__.py +63 -0
- zettcode/app/ui/app.py +1507 -0
- zettcode/app/ui/bars.py +290 -0
- zettcode/app/ui/boot.py +131 -0
- zettcode/app/ui/clipboard.py +340 -0
- zettcode/app/ui/commands.py +323 -0
- zettcode/app/ui/config_form.py +343 -0
- zettcode/app/ui/demo.py +180 -0
- zettcode/app/ui/followups.py +430 -0
- zettcode/app/ui/gc_policy.py +147 -0
- zettcode/app/ui/keys.py +462 -0
- zettcode/app/ui/messaging.py +260 -0
- zettcode/app/ui/notices.py +195 -0
- zettcode/app/ui/providers.py +626 -0
- zettcode/app/ui/sessions.py +422 -0
- zettcode/app/ui/settings.py +161 -0
- zettcode/app/ui/shell.py +361 -0
- zettcode/app/ui/turns.py +115 -0
- zettcode/app/ui/updating.py +204 -0
- zettcode/app/ui/widgets/__init__.py +161 -0
- zettcode/app/ui/widgets/approval_page.py +284 -0
- zettcode/app/ui/widgets/ask_page.py +386 -0
- zettcode/app/ui/widgets/bash_page.py +90 -0
- zettcode/app/ui/widgets/completer.py +100 -0
- zettcode/app/ui/widgets/composer.py +529 -0
- zettcode/app/ui/widgets/document_page.py +159 -0
- zettcode/app/ui/widgets/effort_page.py +294 -0
- zettcode/app/ui/widgets/follow_up.py +168 -0
- zettcode/app/ui/widgets/form_page.py +380 -0
- zettcode/app/ui/widgets/input_page.py +175 -0
- zettcode/app/ui/widgets/model_page.py +63 -0
- zettcode/app/ui/widgets/model_settings_page.py +653 -0
- zettcode/app/ui/widgets/pet.py +247 -0
- zettcode/app/ui/widgets/pet_page.py +347 -0
- zettcode/app/ui/widgets/plugins_page.py +442 -0
- zettcode/app/ui/widgets/root.py +68 -0
- zettcode/app/ui/widgets/self_compact_page.py +363 -0
- zettcode/app/ui/widgets/send_kind_page.py +193 -0
- zettcode/app/ui/widgets/sessions_page.py +511 -0
- zettcode/app/ui/widgets/statusline_page.py +427 -0
- zettcode/app/ui/widgets/steering.py +126 -0
- zettcode/app/ui/widgets/transcript.py +287 -0
- zettcode/app/ui/widgets/update_page.py +65 -0
- zettcode/app/ui/widgets/welcome.py +24 -0
- zettcode/collector.py +54 -0
- zettcode/update.py +519 -0
- zettcode_app-0.4.2.dist-info/METADATA +26 -0
- zettcode_app-0.4.2.dist-info/RECORD +81 -0
- zettcode_app-0.4.2.dist-info/WHEEL +4 -0
- zettcode_app-0.4.2.dist-info/entry_points.txt +2 -0
zettcode/update.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""Notice a newer ZettCode on PyPI, and install it when the reader asks.
|
|
2
|
+
|
|
3
|
+
The check itself is never on the startup path: a background task asks PyPI once a day and leaves what it found in
|
|
4
|
+
``~/.zettcode/update.json``. The next start reads that file — no network, nothing to wait for — and offers the version
|
|
5
|
+
it names, unless the reader has already skipped that one. The upgrade that follows a "yes" does block, on purpose:
|
|
6
|
+
the reader asked for it, so it asks PyPI what is newest at that moment, installs from PyPI's own index, and the window
|
|
7
|
+
takes no input until it is done.
|
|
8
|
+
|
|
9
|
+
An upgrade is only believed once it has been checked. A package manager that exits 0 has
|
|
10
|
+
not necessarily installed anything (``uv tool upgrade`` answers "Nothing to upgrade" with 0, and so
|
|
11
|
+
does a pip whose index has not listed the release yet), so after it runs the installed version is
|
|
12
|
+
read back from the interpreter that was upgraded and written to the same file. What the file says
|
|
13
|
+
is what the next start trusts: an upgrade that did not take is reported as that, not as done, and
|
|
14
|
+
one that did is not offered again.
|
|
15
|
+
|
|
16
|
+
Every failure means "no news". A machine with no network, a proxy that eats the
|
|
17
|
+
request, an index that answers with something unexpected: none of them may
|
|
18
|
+
become the reason a session fails to open, so the check swallows what it cannot
|
|
19
|
+
use and leaves the stored state alone.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import ssl
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
import threading
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.request
|
|
34
|
+
from collections.abc import Callable, Sequence
|
|
35
|
+
from dataclasses import dataclass, replace
|
|
36
|
+
from datetime import datetime, timedelta, timezone
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from shutil import which
|
|
39
|
+
from tempfile import NamedTemporaryFile
|
|
40
|
+
from typing import Any, TypeVar
|
|
41
|
+
|
|
42
|
+
from zettcode.paths import DEFAULT_UPDATE_FILE
|
|
43
|
+
from zettcode.version import __version__
|
|
44
|
+
|
|
45
|
+
Result = TypeVar("Result")
|
|
46
|
+
|
|
47
|
+
#: PyPI's JSON API for this project; ``info.version`` is the newest release. Named for the API it is, beside the
|
|
48
|
+
#: simple index below, so the two cannot be mistaken for one another.
|
|
49
|
+
JSON_API_URL = "https://pypi.org/pypi/zettcode/json"
|
|
50
|
+
|
|
51
|
+
#: PyPI's package index, which the upgrade is pointed at explicitly. The metadata above comes from PyPI, so the
|
|
52
|
+
#: download has to as well: a mirror configured on this machine can lag behind it by hours, and the package manager
|
|
53
|
+
#: then reports "already satisfied" for a release that has existed for a while. A reader who wants the mirror can
|
|
54
|
+
#: still install by hand.
|
|
55
|
+
SIMPLE_INDEX = "https://pypi.org/simple"
|
|
56
|
+
|
|
57
|
+
#: Seconds the request may take before it is abandoned. Generous on purpose: a
|
|
58
|
+
#: corporate proxy is routine here, and the check runs in the background, so a
|
|
59
|
+
#: slow answer costs a thread rather than a frame. A start never notices either way.
|
|
60
|
+
TIMEOUT = 10.0
|
|
61
|
+
|
|
62
|
+
#: How long one answer is trusted: a start inside the window reads the file only.
|
|
63
|
+
CHECK_INTERVAL = timedelta(hours=24)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def version_key(value: str) -> tuple[int, ...]:
|
|
67
|
+
"""Return a comparable key for a version such as ``0.1.2``.
|
|
68
|
+
|
|
69
|
+
Only the leading digits of each dot-separated part are read, so a release
|
|
70
|
+
candidate (``0.2.0rc1``) keys the same as the release it leads to and is
|
|
71
|
+
never offered over it.
|
|
72
|
+
"""
|
|
73
|
+
parts: list[int] = []
|
|
74
|
+
for piece in value.strip().lstrip("vV").split("."):
|
|
75
|
+
leading = ""
|
|
76
|
+
for character in piece:
|
|
77
|
+
if not character.isdigit():
|
|
78
|
+
break
|
|
79
|
+
leading += character
|
|
80
|
+
parts.append(int(leading) if leading else 0)
|
|
81
|
+
return tuple(parts)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_newer(candidate: str, current: str) -> bool:
|
|
85
|
+
"""Return whether ``candidate`` is a later release than ``current``."""
|
|
86
|
+
return version_key(candidate) > version_key(current)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _text(value: object) -> str | None:
|
|
90
|
+
"""Return a stored string, or ``None`` for anything else."""
|
|
91
|
+
if not isinstance(value, str):
|
|
92
|
+
return None
|
|
93
|
+
return value.strip() or None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _moment(value: object) -> datetime | None:
|
|
97
|
+
"""Return a stored timestamp as an aware datetime, or ``None``."""
|
|
98
|
+
if not isinstance(value, str):
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
moment = datetime.fromisoformat(value)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None
|
|
104
|
+
return moment if moment.tzinfo is not None else moment.replace(tzinfo=timezone.utc)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class UpdateState:
|
|
109
|
+
"""What the last check found, what the reader has dismissed, and what an upgrade installed.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
checked_at: When the index was last asked, aware and in UTC; ``None``
|
|
113
|
+
before the first successful check.
|
|
114
|
+
latest: Newest release the index named, if any.
|
|
115
|
+
skipped: Version the reader said not to ask about again.
|
|
116
|
+
installed: Version an upgrade was *confirmed* to have installed, read back from the
|
|
117
|
+
environment afterwards; ``None`` until one has been.
|
|
118
|
+
installed_by: The interpreter that upgrade ran in. A record is only about that interpreter.
|
|
119
|
+
noted: Version whose mismatch with the running program has already been explained, so the
|
|
120
|
+
explanation is given once and not at every start.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
checked_at: datetime | None = None
|
|
124
|
+
latest: str | None = None
|
|
125
|
+
skipped: str | None = None
|
|
126
|
+
installed: str | None = None
|
|
127
|
+
installed_by: str | None = None
|
|
128
|
+
noted: str | None = None
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def stale(self) -> bool:
|
|
132
|
+
"""Return whether the stored answer is old enough to ask the index again."""
|
|
133
|
+
if self.checked_at is None:
|
|
134
|
+
return True
|
|
135
|
+
return datetime.now(timezone.utc) - self.checked_at >= CHECK_INTERVAL
|
|
136
|
+
|
|
137
|
+
def shadowed_by(self, current: str = __version__, executable: str | None = None) -> str | None:
|
|
138
|
+
"""Return the version an upgrade installed here that this process is not running, if any.
|
|
139
|
+
|
|
140
|
+
A confirmed install newer than the running program, in the interpreter that is running it,
|
|
141
|
+
means the upgrade worked and something else is supplying the code — an editable checkout, a
|
|
142
|
+
``PYTHONPATH`` — so offering the same upgrade again cannot change what runs. A record made
|
|
143
|
+
by another interpreter says nothing about this one: that copy really is old.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
current: Version of the running program.
|
|
147
|
+
executable: Interpreter running it; defaults to this process's.
|
|
148
|
+
"""
|
|
149
|
+
if self.installed is None or self.installed_by != (executable or sys.executable):
|
|
150
|
+
return None
|
|
151
|
+
return self.installed if is_newer(self.installed, current) else None
|
|
152
|
+
|
|
153
|
+
def offer(self, current: str = __version__, executable: str | None = None) -> str | None:
|
|
154
|
+
"""Return the version worth offering, or ``None`` when there is nothing to say."""
|
|
155
|
+
if self.latest is None or self.skipped == self.latest:
|
|
156
|
+
return None
|
|
157
|
+
if self.shadowed_by(current, executable) is not None:
|
|
158
|
+
return None
|
|
159
|
+
return self.latest if is_newer(self.latest, current) else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def in_background(work: Callable[[], Result]) -> Result:
|
|
163
|
+
"""Run one blocking call on a daemon thread and return what it produced.
|
|
164
|
+
|
|
165
|
+
Not :func:`asyncio.to_thread`: that uses the loop's own executor, which is
|
|
166
|
+
joined when the process exits, so a request still waiting on a slow proxy
|
|
167
|
+
would hold the terminal open for as long as its timeout. This thread is left
|
|
168
|
+
behind instead, because a check nobody is waiting for cannot matter once the
|
|
169
|
+
session is gone.
|
|
170
|
+
"""
|
|
171
|
+
loop = asyncio.get_running_loop()
|
|
172
|
+
future: asyncio.Future[Result] = loop.create_future()
|
|
173
|
+
|
|
174
|
+
def deliver(value: Result) -> None:
|
|
175
|
+
"""Resolve the awaiting coroutine, unless it already went away."""
|
|
176
|
+
if not future.done():
|
|
177
|
+
future.set_result(value)
|
|
178
|
+
|
|
179
|
+
def fail(error: BaseException) -> None:
|
|
180
|
+
"""Report why the call produced nothing, for the same reason."""
|
|
181
|
+
if not future.done():
|
|
182
|
+
future.set_exception(error)
|
|
183
|
+
|
|
184
|
+
def hand_over(callback: Callable[[Any], None], argument: Any) -> None:
|
|
185
|
+
"""Pass the outcome to the loop, unless the loop is gone.
|
|
186
|
+
|
|
187
|
+
The thread is left running on purpose when the session ends, so its answer can
|
|
188
|
+
arrive after the loop has been closed — and ``call_soon_threadsafe`` then raises.
|
|
189
|
+
A daemon thread that raises prints a traceback, and one that does so while the
|
|
190
|
+
interpreter is shutting down can take the whole process down with it: the
|
|
191
|
+
standard error stream's lock cannot be taken, and Python aborts. Nobody is
|
|
192
|
+
waiting for this answer any more, so dropping it is the whole of the handling.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
loop.call_soon_threadsafe(callback, argument)
|
|
196
|
+
except RuntimeError:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
def run() -> None:
|
|
200
|
+
try:
|
|
201
|
+
value = work()
|
|
202
|
+
except BaseException as error: # noqa: BLE001 - handed to the awaiting caller
|
|
203
|
+
hand_over(fail, error)
|
|
204
|
+
else:
|
|
205
|
+
hand_over(deliver, value)
|
|
206
|
+
|
|
207
|
+
threading.Thread(target=run, name="zettcode-background", daemon=True).start()
|
|
208
|
+
return await future
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def read_state(path: Path) -> UpdateState:
|
|
212
|
+
"""Return what the last check left, or an empty state when nothing is readable."""
|
|
213
|
+
try:
|
|
214
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
215
|
+
except (OSError, ValueError):
|
|
216
|
+
return UpdateState()
|
|
217
|
+
if not isinstance(raw, dict):
|
|
218
|
+
return UpdateState()
|
|
219
|
+
return UpdateState(
|
|
220
|
+
checked_at=_moment(raw.get("checked_at")),
|
|
221
|
+
latest=_text(raw.get("latest")),
|
|
222
|
+
skipped=_text(raw.get("skipped")),
|
|
223
|
+
installed=_text(raw.get("installed")),
|
|
224
|
+
installed_by=_text(raw.get("installed_by")),
|
|
225
|
+
noted=_text(raw.get("noted")),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def write_state(state: UpdateState, path: Path) -> None:
|
|
230
|
+
"""Write the state in one replace, so a crash cannot leave half a file."""
|
|
231
|
+
payload = {
|
|
232
|
+
"checked_at": state.checked_at.isoformat() if state.checked_at is not None else None,
|
|
233
|
+
"latest": state.latest,
|
|
234
|
+
"skipped": state.skipped,
|
|
235
|
+
"installed": state.installed,
|
|
236
|
+
"installed_by": state.installed_by,
|
|
237
|
+
"noted": state.noted,
|
|
238
|
+
}
|
|
239
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
240
|
+
with NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
241
|
+
json.dump(payload, handle, indent=2)
|
|
242
|
+
handle.write("\n")
|
|
243
|
+
temporary = Path(handle.name)
|
|
244
|
+
os.replace(temporary, path)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def fetch_latest(url: str = JSON_API_URL, *, timeout: float = TIMEOUT) -> str:
|
|
248
|
+
"""Return the newest release the index reports, raising when it cannot."""
|
|
249
|
+
request = urllib.request.Request(url, headers={"User-Agent": f"zettcode/{__version__}"})
|
|
250
|
+
try:
|
|
251
|
+
with urllib.request.urlopen( # noqa: S310 - the URL is ours
|
|
252
|
+
request, timeout=timeout, context=ssl_context()
|
|
253
|
+
) as response:
|
|
254
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
255
|
+
except (urllib.error.URLError, OSError, ValueError) as error:
|
|
256
|
+
raise RuntimeError(f"could not read {url}: {error}") from error
|
|
257
|
+
latest = payload.get("info", {}).get("version") if isinstance(payload, dict) else None
|
|
258
|
+
if not isinstance(latest, str) or not latest.strip():
|
|
259
|
+
raise RuntimeError(f"{url} answered without a version")
|
|
260
|
+
return latest.strip()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def ssl_context() -> ssl.SSLContext:
|
|
264
|
+
"""Return the trust store to verify the index against.
|
|
265
|
+
|
|
266
|
+
The platform's own, through ``truststore``, rather than the roots bundled
|
|
267
|
+
with Python: a machine behind a corporate proxy — which is what an internal
|
|
268
|
+
gateway usually means — cannot verify anything against the bundled set, and
|
|
269
|
+
the system store is what every other program on it already trusts. The
|
|
270
|
+
import is local because the check runs in a worker thread, and a client that
|
|
271
|
+
never asks about updates should not pay for it at import time.
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
import truststore
|
|
275
|
+
except ImportError: # pragma: no cover - truststore ships with the runtime deps
|
|
276
|
+
return ssl.create_default_context()
|
|
277
|
+
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def check_for_update(
|
|
281
|
+
path: Path,
|
|
282
|
+
*,
|
|
283
|
+
fetch: Callable[[], str] = fetch_latest,
|
|
284
|
+
now: Callable[[], datetime] | None = None,
|
|
285
|
+
) -> UpdateState | None:
|
|
286
|
+
"""Ask the index in a thread and store what it said.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
path: File the answer is written to.
|
|
290
|
+
fetch: Supplies the newest version; injectable so tests stay offline.
|
|
291
|
+
now: Clock for the check timestamp; defaults to UTC now.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The state that was written, or ``None`` when the stored answer was still
|
|
295
|
+
fresh, or the index could not be read.
|
|
296
|
+
"""
|
|
297
|
+
state = read_state(path)
|
|
298
|
+
if not state.stale:
|
|
299
|
+
return None
|
|
300
|
+
try:
|
|
301
|
+
latest = await in_background(fetch)
|
|
302
|
+
except Exception:
|
|
303
|
+
# Offline, a proxy in the way, an index that answered oddly: the next
|
|
304
|
+
# start tries again, and this one says nothing about it.
|
|
305
|
+
return None
|
|
306
|
+
moment = (now or (lambda: datetime.now(timezone.utc)))()
|
|
307
|
+
written = replace(state, checked_at=moment, latest=latest)
|
|
308
|
+
write_state(written, path)
|
|
309
|
+
return written
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def installed_by_uv_tool() -> bool:
|
|
313
|
+
"""Return whether this interpreter lives in a ``uv tool`` environment.
|
|
314
|
+
|
|
315
|
+
``uv`` leaves a ``uv-receipt.toml`` at the root of every tool environment, wherever
|
|
316
|
+
``UV_TOOL_DIR`` put it, which is the one thing that is not a guess. The path is the fallback,
|
|
317
|
+
and it is read as given: a tool environment's ``python`` is a link to the interpreter it was
|
|
318
|
+
built from, and resolving it names that interpreter's directory instead, which has no ``tools``
|
|
319
|
+
in it. That is how a real ``uv tool`` install used to be taken for a pip one, and handed a
|
|
320
|
+
``pip install --upgrade`` in an environment that has no pip.
|
|
321
|
+
"""
|
|
322
|
+
if (Path(sys.prefix) / "uv-receipt.toml").is_file():
|
|
323
|
+
return True
|
|
324
|
+
parts = Path(sys.executable).parts
|
|
325
|
+
return "uv" in parts and "tools" in parts
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def upgrade_command() -> tuple[str, ...]:
|
|
329
|
+
"""Return the command that replaces this installation with the newest release.
|
|
330
|
+
|
|
331
|
+
``uv tool upgrade`` when uv installed ZettCode that way, ``pip`` for the
|
|
332
|
+
interpreter running this process otherwise. Both are pointed at PyPI's own index: the newest version was read
|
|
333
|
+
from PyPI, and installing through whatever mirror the machine has configured would ask for a release that index
|
|
334
|
+
may not list yet. The panel shows the command before it runs, so a wrong guess is visible rather than
|
|
335
|
+
surprising.
|
|
336
|
+
"""
|
|
337
|
+
if installed_by_uv_tool() and which("uv") is not None:
|
|
338
|
+
# `--index-url` rather than the newer `--default-index`: current uv still takes the old spelling, and an
|
|
339
|
+
# older one only has that, so an upgrade that worked before this flag keeps working.
|
|
340
|
+
return ("uv", "tool", "upgrade", "zettcode", "--index-url", SIMPLE_INDEX)
|
|
341
|
+
return (sys.executable, "-m", "pip", "install", "--upgrade", "--index-url", SIMPLE_INDEX, "zettcode")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def run_upgrade(command: Sequence[str] | None = None, *, timeout: float = 600.0) -> tuple[bool, str]:
|
|
345
|
+
"""Run the upgrade in a thread and return whether it worked and what it said.
|
|
346
|
+
|
|
347
|
+
The output is captured rather than painted: this runs while the terminal
|
|
348
|
+
belongs to the frame, and a package manager's progress bars would corrupt it.
|
|
349
|
+
"""
|
|
350
|
+
argv = tuple(command) if command is not None else upgrade_command()
|
|
351
|
+
try:
|
|
352
|
+
result = await in_background(
|
|
353
|
+
lambda: subprocess.run(
|
|
354
|
+
argv,
|
|
355
|
+
capture_output=True,
|
|
356
|
+
text=True,
|
|
357
|
+
stdin=subprocess.DEVNULL,
|
|
358
|
+
timeout=timeout,
|
|
359
|
+
check=False,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
363
|
+
return False, str(error)
|
|
364
|
+
output = f"{result.stdout or ''}{result.stderr or ''}".strip()
|
|
365
|
+
return result.returncode == 0, output
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
#: What a fresh interpreter is asked to print: the version of the installed distribution, which is
|
|
369
|
+
#: what the next start will run, whatever this process was started with.
|
|
370
|
+
INSTALLED_PROBE = "from importlib.metadata import version; print(version('zettcode'))"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def installed_version(executable: str | None = None, *, timeout: float = 60.0) -> str | None:
|
|
374
|
+
"""Return the version of ZettCode installed for an interpreter, read from a fresh process.
|
|
375
|
+
|
|
376
|
+
The running process cannot answer: it imported the code it started with, which is the version
|
|
377
|
+
that is being replaced. A new process reads what is on disk now, which is what the next start
|
|
378
|
+
will run. It is started from a neutral directory, because a start is not made from the workspace
|
|
379
|
+
and a stray ``zettcode`` there must not answer for the install.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
executable: The interpreter the upgrade ran in; defaults to this process's.
|
|
383
|
+
timeout: Seconds before giving up.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
The version, or ``None`` when it could not be read — which is "unconfirmed", never "unchanged".
|
|
387
|
+
"""
|
|
388
|
+
argv = (executable or sys.executable, "-c", INSTALLED_PROBE)
|
|
389
|
+
try:
|
|
390
|
+
result = await in_background(
|
|
391
|
+
lambda: subprocess.run(
|
|
392
|
+
argv,
|
|
393
|
+
capture_output=True,
|
|
394
|
+
text=True,
|
|
395
|
+
stdin=subprocess.DEVNULL,
|
|
396
|
+
timeout=timeout,
|
|
397
|
+
check=False,
|
|
398
|
+
cwd=tempfile.gettempdir(),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
except (OSError, subprocess.SubprocessError):
|
|
402
|
+
return None
|
|
403
|
+
if result.returncode != 0:
|
|
404
|
+
return None
|
|
405
|
+
lines = [line.strip() for line in (result.stdout or "").splitlines() if line.strip()]
|
|
406
|
+
return lines[-1] if lines else None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def record_install(path: Path, version: str, executable: str | None = None) -> None:
|
|
410
|
+
"""Write down that an upgrade installed ``version``, keeping everything else the file says.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
path: The state file.
|
|
414
|
+
version: What :func:`installed_version` confirmed.
|
|
415
|
+
executable: The interpreter it was confirmed for; defaults to this process's.
|
|
416
|
+
"""
|
|
417
|
+
write_state(
|
|
418
|
+
replace(read_state(path), installed=version, installed_by=executable or sys.executable, noted=None), path
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def running_from() -> str:
|
|
423
|
+
"""Return the directory the running program's code was loaded from."""
|
|
424
|
+
return str(Path(__file__).resolve().parent)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def display_command(command: Sequence[str]) -> str:
|
|
428
|
+
"""Return the command the way a reader would type it.
|
|
429
|
+
|
|
430
|
+
The pip fallback has to name an interpreter to be unambiguous
|
|
431
|
+
(``/path/to/python -m pip ...``), and that path is both long enough to be
|
|
432
|
+
clipped in a panel row and noise beside the ``pip install`` it stands for.
|
|
433
|
+
Everything else is shown as it runs, including the index: the panel promises the command can be read before it
|
|
434
|
+
runs, and which index it installs from is exactly what a reader deciding to accept it would want to see.
|
|
435
|
+
"""
|
|
436
|
+
argv = list(command)
|
|
437
|
+
if len(argv) > 2 and argv[1] == "-m" and Path(argv[0]).name.lower().startswith(("python", "pypy")):
|
|
438
|
+
return " ".join(argv[2:])
|
|
439
|
+
return " ".join(argv)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _run_in_terminal(command: Sequence[str]) -> int:
|
|
443
|
+
"""Run the package manager with the reader's own terminal, so its progress is what they see."""
|
|
444
|
+
try:
|
|
445
|
+
return subprocess.run(list(command), check=False).returncode
|
|
446
|
+
except OSError as error:
|
|
447
|
+
print(f"could not run {display_command(command)}: {error}", file=sys.stderr)
|
|
448
|
+
return 127
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _installed_now() -> str | None:
|
|
452
|
+
"""Read the installed version back from a fresh interpreter (see :func:`installed_version`)."""
|
|
453
|
+
return asyncio.run(installed_version())
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def update_program(
|
|
457
|
+
*,
|
|
458
|
+
current: str = __version__,
|
|
459
|
+
path: Path | None = None,
|
|
460
|
+
fetch: Callable[[], str] = fetch_latest,
|
|
461
|
+
run: Callable[[Sequence[str]], int] = _run_in_terminal,
|
|
462
|
+
read_installed: Callable[[], str | None] = _installed_now,
|
|
463
|
+
say: Callable[[str], None] = print,
|
|
464
|
+
) -> int:
|
|
465
|
+
"""Install the newest release now, from the command line: what ``zettcode update`` does.
|
|
466
|
+
|
|
467
|
+
The panel inside the program offers the same upgrade; this is for the reader who would rather ask for it
|
|
468
|
+
than wait to be offered. It asks the index first, so a program that is already the newest says so and
|
|
469
|
+
runs nothing; if the index cannot be reached it goes on regardless, because the package manager knows
|
|
470
|
+
better than a failed request whether there is anything to install. The package manager's own output is
|
|
471
|
+
shown, and its exit status is not believed on its own (see the module notes): the version is read back
|
|
472
|
+
from a fresh interpreter, and only that is reported and recorded.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
current: The version running now.
|
|
476
|
+
path: The state file the confirmed install is written to; defaults to ``~/.zettcode/update.json``.
|
|
477
|
+
fetch: Supplies the newest version; injectable so tests stay offline.
|
|
478
|
+
run: Runs the upgrade command and returns its exit status.
|
|
479
|
+
read_installed: Reads the version installed now, from outside this process.
|
|
480
|
+
say: Where the report goes.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
The exit status: ``0`` when ZettCode is the newest release or was updated (or the update ran and the
|
|
484
|
+
result could not be read back), ``1`` when the upgrade failed or left the old version.
|
|
485
|
+
"""
|
|
486
|
+
say(f"ZettCode {current}")
|
|
487
|
+
latest: str | None
|
|
488
|
+
try:
|
|
489
|
+
latest = fetch()
|
|
490
|
+
except Exception: # noqa: BLE001 - offline, a proxy, an odd answer: let the package manager decide
|
|
491
|
+
latest = None
|
|
492
|
+
if latest is not None and not is_newer(latest, current):
|
|
493
|
+
say(f"already the newest release ({current})")
|
|
494
|
+
return 0
|
|
495
|
+
command = upgrade_command()
|
|
496
|
+
say(f"updating{f' to {latest}' if latest else ''}: {display_command(command)}")
|
|
497
|
+
status = run(command)
|
|
498
|
+
if status != 0:
|
|
499
|
+
say(f"the update failed (exit status {status}); run it yourself: {display_command(command)}")
|
|
500
|
+
return 1
|
|
501
|
+
installed = read_installed()
|
|
502
|
+
if installed is None:
|
|
503
|
+
say("the update ran, but the installed version could not be read back — check with `zettcode --version`")
|
|
504
|
+
return 0
|
|
505
|
+
if not is_newer(installed, current):
|
|
506
|
+
if latest is not None:
|
|
507
|
+
say(
|
|
508
|
+
f"ZettCode is still {installed}, not {latest}: the release may be yanked, or the installer may have "
|
|
509
|
+
f"been pinned to {installed}; try `uv tool install zettcode@latest`, or {display_command(command)}"
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
say(f"ZettCode is {installed}: there was nothing newer to install")
|
|
513
|
+
return 1 if latest is not None else 0
|
|
514
|
+
try:
|
|
515
|
+
record_install(path if path is not None else DEFAULT_UPDATE_FILE, installed)
|
|
516
|
+
except OSError:
|
|
517
|
+
pass # the install happened; failing to write it down must not make it look as if it had not
|
|
518
|
+
say(f"updated {current} → {installed}; restart ZettCode to use it")
|
|
519
|
+
return 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zettcode-app
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: The terminal application of ZettCode: the conversation model, the shell and its views
|
|
5
|
+
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
|
+
Author: Chang-LeHung
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: agent,terminal,tui,zettcode
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: truststore<1,>=0.10
|
|
19
|
+
Requires-Dist: zett-agent<0.3,>=0.2.10
|
|
20
|
+
Requires-Dist: zettcode-broker
|
|
21
|
+
Requires-Dist: zettcode-engine
|
|
22
|
+
Requires-Dist: zettcode-messaging
|
|
23
|
+
Requires-Dist: zettcode-plugin-api
|
|
24
|
+
Requires-Dist: zettcode-presenters
|
|
25
|
+
Requires-Dist: zettcode-sdk
|
|
26
|
+
Requires-Dist: zettcode-tui
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
zettcode/collector.py,sha256=bYx20re5LX4VrJ8G7X-0oSrutqg6Bb9D2wlHXK_lwTM,2026
|
|
2
|
+
zettcode/update.py,sha256=HSAI219P5T_JphU79axKoUT7gSDHLT7t-EssV8ElQW4,22902
|
|
3
|
+
zettcode/app/__init__.py,sha256=VkkjVsS5BFrHELyB202476EoB217pZEo3IQkQpYG52I,2021
|
|
4
|
+
zettcode/app/errors.py,sha256=n9DzRnItv96c_8pWtYnGsJ4snxD5b_MVr-ZW4krZspU,581
|
|
5
|
+
zettcode/app/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
zettcode/app/agent/__init__.py,sha256=oud8tPXFmVpDcsMd0klnSaq0ViNENaxJiWy61oIQlNw,1642
|
|
7
|
+
zettcode/app/agent/agent/__init__.py,sha256=8McGcz87u0-KPD9HpcOXWHYtoLn_JRfDXQqSmQ_f5Tk,3681
|
|
8
|
+
zettcode/app/agent/agent/commands.py,sha256=s81xUuEg1e_Wtpjp0bF0it_n9DFs6DiBUVGaYTJBBfc,12780
|
|
9
|
+
zettcode/app/agent/agent/settings.py,sha256=BLrb7WKQJFpbZDncnEOWc0-r7aq7q8Jmf9_m1rYt_yM,6076
|
|
10
|
+
zettcode/app/agent/conversation/__init__.py,sha256=NMuYfXmNpVlyd7yvX0d8RtNCPSar5pXVn0PygEg_SUU,431
|
|
11
|
+
zettcode/app/agent/conversation/changes.py,sha256=pBy80toI-CfcRF6UAUZrapUCi27lA5Jgt4z66CTG4_Y,2834
|
|
12
|
+
zettcode/app/agent/conversation/effort_gauge.py,sha256=FJIQhaA5hOWYyU5Dq4UbydYK0Ha8yVkkDAuWy578Uhc,5550
|
|
13
|
+
zettcode/app/agent/conversation/projection.py,sha256=s2ZCUk7f9wyepN1BIKX0vbYwqKUOhSXxvOsEvJktl_U,13523
|
|
14
|
+
zettcode/app/agent/conversation/rendering.py,sha256=vSAhA2R5B7wElcNTN5eYCIiV3NNfgWxNbwkDq7SPU8M,36075
|
|
15
|
+
zettcode/app/agent/conversation/replay.py,sha256=YblcKg3nE_nWLW1j1vygZwL03jpQpYB5guY37ySp6Tk,9301
|
|
16
|
+
zettcode/app/agent/conversation/shimmer.py,sha256=H13nyLU2il01zdjOyo8x6kESBx4gkVTkRuRhRBYWtzo,12582
|
|
17
|
+
zettcode/app/agent/conversation/stops.py,sha256=GsYwBHy0kJfr2uWHUbltDs4-fXPBkDmB55vYXyw8R9M,3027
|
|
18
|
+
zettcode/app/agent/conversation/tips.py,sha256=MWRHxFUDbZxPitCu-fXLFipsCnWoccG8j1W988_JuAg,5418
|
|
19
|
+
zettcode/app/agent/conversation/transcript/__init__.py,sha256=GuX7BAfB3v2qVIeoEa5-x-u6cppDd3HMs2tfMfb_Vos,3761
|
|
20
|
+
zettcode/app/agent/conversation/transcript/fold.py,sha256=7EmpZXmlBCYA3Nxi7NbP82RbhfVx_Mq8LJF9YYSzmCc,2011
|
|
21
|
+
zettcode/app/agent/conversation/transcript/log.py,sha256=p2Ap75ICPVpz4wrlHp7BscAmnZlqjD1kyL-Xm60O9UQ,8241
|
|
22
|
+
zettcode/app/agent/conversation/transcript/oneoff.py,sha256=RMYevSfmSk5KwjWB6jGl2MktU--mDzeyChf7-GaK0QE,2506
|
|
23
|
+
zettcode/app/agent/conversation/transcript/stream.py,sha256=-vIBurmykFXRVzAS6YbcewTNxbuLVnmhMQ19LPhIXIw,8431
|
|
24
|
+
zettcode/app/agent/conversation/transcript/turn.py,sha256=lFF7Kuv3DRG95yxM3YeYag8R2TmTuPBQfm5EknsXsuc,15455
|
|
25
|
+
zettcode/app/commandline/__init__.py,sha256=1LfVb4_I3bZf_-AkmPyviimM1wUxTMTyG-8PNNNhEjU,1016
|
|
26
|
+
zettcode/app/commandline/arguments.py,sha256=DUtpNMTYH1dY6U1uKHTs_FEy7qCXen3uP7xQFBRXmes,6501
|
|
27
|
+
zettcode/app/commandline/completion.py,sha256=I1nerxI3gyHvvjfUktuOnCUn0dlv47WLe5HqSJR5L08,12788
|
|
28
|
+
zettcode/app/commandline/history.py,sha256=BoSBmFmwy0jXarvH3rBR2WQp0o5CtrTzmPJ1scmevOY,12156
|
|
29
|
+
zettcode/app/commandline/runner.py,sha256=mPpCX6JNgu3thj3HXPdZtqVo-54ht47A-D2a_2lK9sY,15481
|
|
30
|
+
zettcode/app/plugins/__init__.py,sha256=9fLhiGQHNhH2SuEOegxyWAdNKM0ISp19o_rXIIE_yNw,3531
|
|
31
|
+
zettcode/app/plugins/builtins.py,sha256=1AvNNi4U5mbSTBhhGEv5dlu3KKIdFwl0r8aD1w3-hCA,1419
|
|
32
|
+
zettcode/app/plugins/groups.py,sha256=v6CdaEtti9FRvEWRYGLCIN-MGTvaY_X-XdK4HLvtgdQ,23049
|
|
33
|
+
zettcode/app/plugins/loader.py,sha256=bXvN2vuwUQHFvJ_c5ltNDGTCeAdEsPkgXatgJzBS5NA,13143
|
|
34
|
+
zettcode/app/ui/__init__.py,sha256=X4psJrk-0LbVawDHHwdKIRFSnYZXyX2L8YK1vCjhBgo,1720
|
|
35
|
+
zettcode/app/ui/app.py,sha256=iq8XcMrhR5Oc7IOLLAndeqzp9j9jtX_5uwpVMiihhIo,74748
|
|
36
|
+
zettcode/app/ui/bars.py,sha256=HXcA7JFiuKpzh2jlQaB-Zpy0t5NAWQCBcyLOUX7evCg,14148
|
|
37
|
+
zettcode/app/ui/boot.py,sha256=v_6-ts3lQX8ZgWuj77QNl06138OsWRj6eMv_KutHJgQ,6135
|
|
38
|
+
zettcode/app/ui/clipboard.py,sha256=jjENYb9HgdS99IxMZgwM-4ZfSkNspBbSjrtoH_biY2Q,14670
|
|
39
|
+
zettcode/app/ui/commands.py,sha256=Z5Ug0Es6bYA5JkDtzbIf_xGPLo07UB2lbveJUQrCFrk,16529
|
|
40
|
+
zettcode/app/ui/config_form.py,sha256=xKHCcGhcDx0oQS-5lMETPhfIfRk-qcRpm_qf8tS19xY,13733
|
|
41
|
+
zettcode/app/ui/demo.py,sha256=ykt5oJtNgwgulLSJLmInRKOZh7fwNskE_yhKBpY5fvY,6306
|
|
42
|
+
zettcode/app/ui/followups.py,sha256=0t4Ff0mKMho6Iub0kvvHGJveKqUreY-8gKx5cU8HTq4,20368
|
|
43
|
+
zettcode/app/ui/gc_policy.py,sha256=vHNYaikNcsOCiGlwRdEgmybapW6zS1_-DKHeHzGNBk8,6341
|
|
44
|
+
zettcode/app/ui/keys.py,sha256=2wKeBqGzDtUN18AMOIBT2bn07894FR-WidMMtdKNTMw,24216
|
|
45
|
+
zettcode/app/ui/messaging.py,sha256=rZYLW45QVPVQhYhYAqnmIJK3akQmaypi2KqOlHU4h8g,13332
|
|
46
|
+
zettcode/app/ui/notices.py,sha256=vx9Mgaa8A21M6zDxDzOE_4Gc6kyMB_C74yPJAXqoTeU,8608
|
|
47
|
+
zettcode/app/ui/providers.py,sha256=8GobiWrbOjxBQM_jOcsKR8q2xD9SzjLGWRi69fqGT4k,28397
|
|
48
|
+
zettcode/app/ui/sessions.py,sha256=EraEvh3-2cgpClnTn67l4yfsHoLmN8aM9qdWNqsi-3c,20719
|
|
49
|
+
zettcode/app/ui/settings.py,sha256=s5SCRg22m3H0aa-q97W_WoR1s_vxBQscrEAZF4m93HA,6734
|
|
50
|
+
zettcode/app/ui/shell.py,sha256=CatRQgMttX995RGDsXsYn1eSFcfW0nITaJhqTECavZs,16710
|
|
51
|
+
zettcode/app/ui/turns.py,sha256=wCNepTbDLu1htFJgm9deZvlUwx5uURy7i8K0VTjSRVE,4960
|
|
52
|
+
zettcode/app/ui/updating.py,sha256=LtB3vGq6OUo2XlnLXav1PuIz0WMdAn9ZLHZzr_YfFrI,9754
|
|
53
|
+
zettcode/app/ui/widgets/__init__.py,sha256=4ifL1V3x42BY89ni-_8x7TISgJY-miLA63R1mD0-7lA,5176
|
|
54
|
+
zettcode/app/ui/widgets/approval_page.py,sha256=zT9oR-YNC67azEXFxjIF2nsoms4zJQMzyLB2VgLO6ck,12155
|
|
55
|
+
zettcode/app/ui/widgets/ask_page.py,sha256=7lW8mQau6kHAUnRG0W5sXA5fJdozH6m3YErLI8cGsf8,16498
|
|
56
|
+
zettcode/app/ui/widgets/bash_page.py,sha256=-0sVK89HIRrhOz6n2YDenRFMXXo1GIenRWy68WoccLE,3667
|
|
57
|
+
zettcode/app/ui/widgets/completer.py,sha256=1o1wWdwXxyjfkGHWC7wGXl3g8HNpcaJmn-5gu6gCoVE,4761
|
|
58
|
+
zettcode/app/ui/widgets/composer.py,sha256=urxL_Bnpkg4P8C6-C3LdfZduA_ae9EtHsmGZd01ef_k,23674
|
|
59
|
+
zettcode/app/ui/widgets/document_page.py,sha256=xEmWjiWjT7AJraFLu33S7rsVORr8YJWKVvogzY3gXuk,6038
|
|
60
|
+
zettcode/app/ui/widgets/effort_page.py,sha256=Gs1UpOpev9sruL61iA7TSP928JYAc0_20Cmx4GL4w08,13632
|
|
61
|
+
zettcode/app/ui/widgets/follow_up.py,sha256=kpX4YfetJERWGsu8K5vNccmMcWnXtgKcmnAs8hQV-TI,8310
|
|
62
|
+
zettcode/app/ui/widgets/form_page.py,sha256=INiEDQkrLliJ1Ot6uwMZypu_TwhAFxvTgg8WOO5PNTQ,16303
|
|
63
|
+
zettcode/app/ui/widgets/input_page.py,sha256=PRVFHzizVhbp7YxcmIASIpyTEcAqdYVI5_qlVbuk19s,6543
|
|
64
|
+
zettcode/app/ui/widgets/model_page.py,sha256=9S9k1NbyffsIiLAwltRbKwMq8lD8W9PPNV-AqGEHF5M,2745
|
|
65
|
+
zettcode/app/ui/widgets/model_settings_page.py,sha256=4rIi53PEoSYZqA3HjHE3cWygkXpIeSZgffVvC8vcHuY,26382
|
|
66
|
+
zettcode/app/ui/widgets/pet.py,sha256=dsVLO_BvverF-0Iec8l6KBu1716Qi56mwnAizrcohHc,12108
|
|
67
|
+
zettcode/app/ui/widgets/pet_page.py,sha256=aXEmgPF8U3l9Q_X7dTL0ldN7A8hl1WrptIKevmjU8yo,15840
|
|
68
|
+
zettcode/app/ui/widgets/plugins_page.py,sha256=gHpkdCU_8JKdkRlZSHj-CWmc_jVpzUl90x-ttszezHI,20095
|
|
69
|
+
zettcode/app/ui/widgets/root.py,sha256=qVgAhWwZ9dK6CQJ5dgI7k36qFam12q5oU3ltXJuFj1M,2714
|
|
70
|
+
zettcode/app/ui/widgets/self_compact_page.py,sha256=0g0Ozu_ru6MwbtYFDEg_XR2TYYSYRqe-T6VGUQ1gkAk,16999
|
|
71
|
+
zettcode/app/ui/widgets/send_kind_page.py,sha256=_0knUlYNl9_9esaQ6WfSP5RH38PYSboacDodW_y7mGM,7437
|
|
72
|
+
zettcode/app/ui/widgets/sessions_page.py,sha256=wsoH4fjXg4K-Xg9WsqyMFiMZk4XE0sRVo74UxMqKfdk,24825
|
|
73
|
+
zettcode/app/ui/widgets/statusline_page.py,sha256=Jc535A3Yt1ahcP0Hx7uEA0_NcBHQjmijcTDTw9_4Yi4,19821
|
|
74
|
+
zettcode/app/ui/widgets/steering.py,sha256=go1zaSYYjXCSJJnaXrGsbYiZNp2jaASnP3DVm_1eEDk,5216
|
|
75
|
+
zettcode/app/ui/widgets/transcript.py,sha256=0_WQhR3YUZ8zxZftLcQemgehoC4a4jKZ2I8oLQ2v-fU,12155
|
|
76
|
+
zettcode/app/ui/widgets/update_page.py,sha256=FBgXU9P1A7FAq0_apa-XKc7IcKian9O2hwx0RxwBpIo,2588
|
|
77
|
+
zettcode/app/ui/widgets/welcome.py,sha256=9jbcTUzvWQxWjLeG4ZQCLm64H12QKXn6Vc_auSR2fhA,919
|
|
78
|
+
zettcode_app-0.4.2.dist-info/METADATA,sha256=ZkIYj685g6jSmsYWccCQWbQvtmStSSKaRTUdMlP_VJc,1010
|
|
79
|
+
zettcode_app-0.4.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
80
|
+
zettcode_app-0.4.2.dist-info/entry_points.txt,sha256=r2wjH9DbirtzmkUoBErNV1u2sOco10jBRGBCyEEQYDE,71
|
|
81
|
+
zettcode_app-0.4.2.dist-info/RECORD,,
|