zettcode-plugin-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zettcode/plugin_api/__init__.py +330 -0
- zettcode/plugin_api/commands.py +383 -0
- zettcode/plugin_api/entries.py +385 -0
- zettcode/plugin_api/host.py +36 -0
- zettcode/plugin_api/mixins.py +67 -0
- zettcode/plugin_api/panels.py +39 -0
- zettcode/plugin_api/pets.py +224 -0
- zettcode/plugin_api/plugin.py +76 -0
- zettcode/plugin_api/presenters.py +277 -0
- zettcode/plugin_api/py.typed +0 -0
- zettcode/plugin_api/rows.py +91 -0
- zettcode/plugin_api/state.py +251 -0
- zettcode/plugin_api/switch.py +87 -0
- zettcode_plugin_api-0.4.0.dist-info/METADATA +21 -0
- zettcode_plugin_api-0.4.0.dist-info/RECORD +16 -0
- zettcode_plugin_api-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""The shell snapshot and segment types a plugin draws against.
|
|
2
|
+
|
|
3
|
+
A segment builder is called once per paint with a :class:`ShellContext`: a
|
|
4
|
+
snapshot of everything the shell's own rows read, grouped by what it describes
|
|
5
|
+
(``session``, ``model``, ``activity``, ``display``) with the resolved ``config``
|
|
6
|
+
beside them. The same module carries the segment types a plugin registers, so a
|
|
7
|
+
builder and what it registers are described in one place.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING, Literal, TypeAlias
|
|
15
|
+
|
|
16
|
+
from zettcode._compat import StrEnum
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only, so the imports stay lazy
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from zettcode.config import ZettCodeConfig
|
|
22
|
+
from zettcode.sdk.types import ModelInfo, Usage
|
|
23
|
+
from zettcode.tui import TextLine, Theme
|
|
24
|
+
|
|
25
|
+
from .rows import Row
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: Which row a plugin draws into: the header above the transcript, or the
|
|
29
|
+
#: status line under the composer.
|
|
30
|
+
UiRegion: TypeAlias = Literal["header", "status"]
|
|
31
|
+
|
|
32
|
+
#: Which side of a row a segment gathers on. A plugin never chooses it: the
|
|
33
|
+
#: registration method it calls, or the builtin segment it replaces, does.
|
|
34
|
+
UiSide: TypeAlias = Literal["left", "right"]
|
|
35
|
+
|
|
36
|
+
#: What one segment paints: a styled line, a plain string, a :class:`~zettcode.statusline.Row` of
|
|
37
|
+
#: named parts the reader may switch off, or nothing. The alias quotes its members because
|
|
38
|
+
#: :class:`TextLine` is imported only for the checker; an alias evaluates eagerly, so the names must stay strings.
|
|
39
|
+
UiSegmentValue: TypeAlias = "TextLine | Row | str | None"
|
|
40
|
+
|
|
41
|
+
#: What a builder returns: a value, or ``(value, override)`` when the segment
|
|
42
|
+
#: wants its side to itself. With ``override`` the segments registered before it
|
|
43
|
+
#: are dropped for that paint, so the side shows this segment (and anything
|
|
44
|
+
#: registered after it) instead of what the builtins put there.
|
|
45
|
+
UiBuilderResult: TypeAlias = "UiSegmentValue | tuple[UiSegmentValue, bool]"
|
|
46
|
+
|
|
47
|
+
#: One plugin-drawn segment: the callable that paints it.
|
|
48
|
+
UiBuilder: TypeAlias = Callable[["ShellContext"], UiBuilderResult]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class SessionState:
|
|
53
|
+
"""Which conversation the shell is showing.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
id: Active session identity.
|
|
57
|
+
title: Stored session title, or ``None`` before the first reply.
|
|
58
|
+
name: Title to show, falling back to ``"New session"``.
|
|
59
|
+
workspace: Workspace directory the agent runs in.
|
|
60
|
+
handle: The name other sessions address this one by with ``@``, or ``""`` when messaging is off or
|
|
61
|
+
the session is not registered yet.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
id: str
|
|
65
|
+
title: str | None
|
|
66
|
+
name: str
|
|
67
|
+
workspace: Path
|
|
68
|
+
handle: str = ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ModelState:
|
|
73
|
+
"""What the next request will ask for.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
config: The active model, with its id, display name, context window, and
|
|
77
|
+
multimodal flag; ``None`` on a machine with no model configured yet,
|
|
78
|
+
where ``name`` is empty, ``efforts`` is empty, and anything that needs
|
|
79
|
+
a window has none to measure against.
|
|
80
|
+
name: Display name, the short form of ``config.name``.
|
|
81
|
+
effort: Reasoning effort as its string value.
|
|
82
|
+
efforts: Every reasoning level the runtime accepts, cheapest first.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
config: ModelInfo | None
|
|
86
|
+
name: str
|
|
87
|
+
effort: str
|
|
88
|
+
efforts: tuple[str, ...]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Activity(StrEnum):
|
|
92
|
+
"""What the shell is doing between requests, as the status line words it."""
|
|
93
|
+
|
|
94
|
+
READY = "ready"
|
|
95
|
+
RUNNING = "running"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, slots=True)
|
|
99
|
+
class ActivityState:
|
|
100
|
+
"""What the agent is doing right now, and what it has spent.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
busy: Whether a turn or a command is in flight.
|
|
104
|
+
status: The shell's own state next to the activity glyph.
|
|
105
|
+
note: One-off text that takes the status word's place, such as the
|
|
106
|
+
running command's name or ``"copied 12 characters"``; ``None``
|
|
107
|
+
shows the status word itself.
|
|
108
|
+
auto_shell: Whether shell commands are approved for the rest of the run.
|
|
109
|
+
``True`` exactly when ``permission`` is ``"full"``; kept for plugins
|
|
110
|
+
written before the other levels existed.
|
|
111
|
+
usage: Cumulative token counters for the active session.
|
|
112
|
+
tasks: The session's plan as ``(status, content)`` pairs, empty when the
|
|
113
|
+
model has not published one.
|
|
114
|
+
frame: Animation frame counter; it advances only while something is
|
|
115
|
+
busy, which is what a plugin animates against.
|
|
116
|
+
permission: How much of the shell may run unasked: ``"deny"``, ``"ask"``,
|
|
117
|
+
``"safe"``, or ``"full"``.
|
|
118
|
+
self_compact: Whether the model may compact its own context in this session.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
busy: bool
|
|
122
|
+
status: Activity
|
|
123
|
+
note: str | None
|
|
124
|
+
auto_shell: bool
|
|
125
|
+
usage: Usage
|
|
126
|
+
tasks: tuple[tuple[str, str], ...]
|
|
127
|
+
frame: int
|
|
128
|
+
permission: str = "ask"
|
|
129
|
+
self_compact: bool = False
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def label(self) -> str:
|
|
133
|
+
"""Return the word the status line shows: the note, else the state."""
|
|
134
|
+
return self.note or self.status
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True, slots=True)
|
|
138
|
+
class DisplayState:
|
|
139
|
+
"""The terminal and the palette a shell row is drawn into.
|
|
140
|
+
|
|
141
|
+
Attributes:
|
|
142
|
+
theme: Active palette, so a plugin styles with the same colours as the
|
|
143
|
+
rest of the shell instead of hardcoding its own.
|
|
144
|
+
width: Current terminal width in columns.
|
|
145
|
+
height: Current terminal height in rows.
|
|
146
|
+
screen: Name of the topmost screen (``"main"``, ``"page"``, ``"toast"``),
|
|
147
|
+
so a plugin can tell an overlay from the composer.
|
|
148
|
+
scrolled_up: Whether the transcript is showing history rather than the
|
|
149
|
+
newest line, which is what the "back to bottom" badge tracks.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
theme: Theme
|
|
153
|
+
width: int
|
|
154
|
+
height: int
|
|
155
|
+
screen: str
|
|
156
|
+
scrolled_up: bool
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class ShellContext:
|
|
161
|
+
"""What a plugin reads when it draws one header or status row.
|
|
162
|
+
|
|
163
|
+
The state is rebuilt for every paint, so a builder always sees the current
|
|
164
|
+
session, model, counters, and terminal size; it is a snapshot, not a
|
|
165
|
+
subscription. It is deliberately everything the shell's own rows read —
|
|
166
|
+
a plugin should not have to guess at the shell's state to draw beside it —
|
|
167
|
+
grouped so a builder names the part it wants instead of one long field list.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
config: Resolved settings the runtime was built from.
|
|
171
|
+
session: The conversation on screen.
|
|
172
|
+
model: What the next request will ask for.
|
|
173
|
+
activity: What the agent is doing and what it has spent.
|
|
174
|
+
display: The terminal, palette, and scroll position it draws into.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
config: ZettCodeConfig
|
|
178
|
+
session: SessionState
|
|
179
|
+
model: ModelState
|
|
180
|
+
activity: ActivityState
|
|
181
|
+
display: DisplayState
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class UiSegment:
|
|
186
|
+
"""One named piece of a shell row.
|
|
187
|
+
|
|
188
|
+
Attributes:
|
|
189
|
+
name: Identity within its row. Registering a segment under a name that
|
|
190
|
+
already exists replaces that one in place, which is how a plugin
|
|
191
|
+
takes a slot over from the builtin rows.
|
|
192
|
+
builder: Called once per paint with the current :class:`ShellContext`.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
name: str
|
|
196
|
+
builder: UiBuilder
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True, slots=True)
|
|
200
|
+
class TurnContext:
|
|
201
|
+
"""What a request came to, for the items of the line it ends on.
|
|
202
|
+
|
|
203
|
+
Attributes:
|
|
204
|
+
label: How the request ended, as the line's first word: ``Processed``, or ``Continued`` for a ``/continue``.
|
|
205
|
+
seconds: Wall time of the request.
|
|
206
|
+
clock: The time of day it ended, as ``HH:MM``.
|
|
207
|
+
usage: What the request spent: the session's usage after it, less before it.
|
|
208
|
+
tools: Tool calls the request made.
|
|
209
|
+
failed_tools: How many of them failed.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
label: str
|
|
213
|
+
seconds: float
|
|
214
|
+
clock: str
|
|
215
|
+
usage: Usage
|
|
216
|
+
tools: int = 0
|
|
217
|
+
failed_tools: int = 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
#: Called once when a request ends with its :class:`TurnContext`; returns the item's text, or nothing to leave it out.
|
|
221
|
+
TurnBuilder: TypeAlias = Callable[[TurnContext], "str | None"]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True, slots=True)
|
|
225
|
+
class TurnSegment:
|
|
226
|
+
"""One named item of the line a request ends on.
|
|
227
|
+
|
|
228
|
+
Attributes:
|
|
229
|
+
name: Identity, and the switch it has in ``[statusline]`` and ``/statusline``; it starts with ``turn-``.
|
|
230
|
+
Registering a segment under a name that exists replaces that one in place.
|
|
231
|
+
summary: One line saying what it shows, for the ``/statusline`` panel.
|
|
232
|
+
builder: Called once per request with a :class:`TurnContext`.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
name: str
|
|
236
|
+
summary: str
|
|
237
|
+
builder: TurnBuilder
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True, slots=True)
|
|
241
|
+
class UiRow:
|
|
242
|
+
"""One shell row: its two sides, each painted left to right.
|
|
243
|
+
|
|
244
|
+
The sides are the shell's frame, not a plugin's choice; ``left`` is drawn at
|
|
245
|
+
the left edge and ``right`` flush to the right, with ``StatusBar`` settling
|
|
246
|
+
which one wins when the terminal is narrow.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
region: UiRegion
|
|
250
|
+
left: tuple[UiSegment, ...] = ()
|
|
251
|
+
right: tuple[UiSegment, ...] = ()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The switch the shell's panels draw: a short rail with a round knob, and the state of one row's switch.
|
|
2
|
+
|
|
3
|
+
``/self-compact``, ``/statusline``, ``/config``'s one-model panel and ``/plugins`` all draw the same switch, so the thing
|
|
4
|
+
a reader presses looks the same wherever they press it. It lives in a module of its own because more than one page
|
|
5
|
+
draws it: a page importing another page's private helper was how it used to be spelled.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from zettcode.tui import Canvas, Theme
|
|
11
|
+
from zettcode.tui.render import Style, blend
|
|
12
|
+
|
|
13
|
+
#: Seconds a knob takes to travel to the option it was sent to.
|
|
14
|
+
SLIDE_SECONDS = 0.28
|
|
15
|
+
|
|
16
|
+
#: Columns of the rail: short enough to sit beside a name, long enough that the knob crosses nine columns in the slide
|
|
17
|
+
#: instead of the four a five-column rail gives it, which is what made it jump.
|
|
18
|
+
SWITCH_WIDTH = 10
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Rail:
|
|
22
|
+
"""One row's choices and the knob on them: where it is, where it is going, and how it gets there."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, words: tuple[str, ...], current: int) -> None:
|
|
25
|
+
self.words = words
|
|
26
|
+
self.start = current
|
|
27
|
+
self.selected = current
|
|
28
|
+
self.origin = float(current)
|
|
29
|
+
self.set_off = 0.0
|
|
30
|
+
self.moving = False
|
|
31
|
+
|
|
32
|
+
def position(self, now: float) -> float:
|
|
33
|
+
"""Return where the knob is, in options from the first: a fraction while it travels."""
|
|
34
|
+
if not self.moving:
|
|
35
|
+
return float(self.selected)
|
|
36
|
+
progress = (now - self.set_off) / SLIDE_SECONDS
|
|
37
|
+
if progress >= 1.0:
|
|
38
|
+
return float(self.selected)
|
|
39
|
+
eased = 1 - (1 - max(0.0, progress)) ** 3
|
|
40
|
+
return self.origin + (self.selected - self.origin) * eased
|
|
41
|
+
|
|
42
|
+
def choose(self, index: int, now: float, *, instant: bool) -> bool:
|
|
43
|
+
"""Send the knob to an option; say whether it has somewhere to travel."""
|
|
44
|
+
index = max(0, min(len(self.words) - 1, index))
|
|
45
|
+
if index == self.selected:
|
|
46
|
+
return False
|
|
47
|
+
self.origin = self.position(now)
|
|
48
|
+
self.selected = index
|
|
49
|
+
self.set_off = now
|
|
50
|
+
self.moving = not instant
|
|
51
|
+
if instant:
|
|
52
|
+
self.origin = float(index)
|
|
53
|
+
return self.moving
|
|
54
|
+
|
|
55
|
+
def arrive(self, now: float) -> bool:
|
|
56
|
+
"""Stop the knob if it has arrived; say whether this was its last frame."""
|
|
57
|
+
if self.moving and self.position(now) == float(self.selected):
|
|
58
|
+
self.moving = False
|
|
59
|
+
self.origin = float(self.selected)
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def draw_switch(canvas: Canvas, theme: Theme, y: int, x: int, position: float, groove: str) -> None:
|
|
65
|
+
"""Draw a switch as a short thin rail: lit up to the knob when on, the knob at the start of a groove when off.
|
|
66
|
+
|
|
67
|
+
It is the rail every panel uses, drawn the same way, with its round knob. The position is the clock's, so a slow
|
|
68
|
+
terminal drops frames instead of slowing the knob.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
canvas: Where to draw.
|
|
72
|
+
theme: The palette; the rail is drawn on its raised surface.
|
|
73
|
+
y: The row.
|
|
74
|
+
x: The column of the rail's first cell.
|
|
75
|
+
position: Where the knob is, from 0 (off) to 1 (on); a fraction while it travels.
|
|
76
|
+
groove: The colour of the empty stretch.
|
|
77
|
+
"""
|
|
78
|
+
surface = theme.surface_alt
|
|
79
|
+
knob = round(max(0.0, min(1.0, position)) * (SWITCH_WIDTH - 1))
|
|
80
|
+
lit = blend(blend(theme.muted, surface, 0.25), theme.accent, position)
|
|
81
|
+
for cell in range(SWITCH_WIDTH):
|
|
82
|
+
if cell < knob:
|
|
83
|
+
canvas.draw_text(x + cell, y, "\u2501", Style(foreground=lit, background=surface))
|
|
84
|
+
elif cell > knob:
|
|
85
|
+
canvas.draw_text(x + cell, y, "\u2500", Style(foreground=groove, background=surface))
|
|
86
|
+
colour = blend(lit, theme.text, 0.35 * position)
|
|
87
|
+
canvas.draw_text(x + knob, y, "\u25cf", Style(foreground=colour, background=surface, bold=True))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zettcode-plugin-api
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: The one package a ZettCode plugin depends on: the plugin surface, re-exported
|
|
5
|
+
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
|
+
Author: Chang-LeHung
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: api,plugin,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: zettcode-engine
|
|
19
|
+
Requires-Dist: zettcode-tui
|
|
20
|
+
Provides-Extra: types
|
|
21
|
+
Requires-Dist: zettcode-sdk; extra == 'types'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
zettcode/plugin_api/__init__.py,sha256=2kP0ceBF5fzi0AB5aXwHlbo_i4uXCVHNEj2ZuUc_brw,9644
|
|
2
|
+
zettcode/plugin_api/commands.py,sha256=Ku0aHPOVQjp79W8p8FMFhKjbmMUmgB9TMCQIOIyyTlE,17956
|
|
3
|
+
zettcode/plugin_api/entries.py,sha256=taGQFPzwj-UBchOBLrDGpRjUHGIj8qSma1UH6A_FV24,14761
|
|
4
|
+
zettcode/plugin_api/host.py,sha256=LT38RB4ipgn05SiupkoBol9B9J57RCSXW1vqK5sahQE,1476
|
|
5
|
+
zettcode/plugin_api/mixins.py,sha256=v1tdhi9I8rLwQJrj--3DUDWVQmInS87EA9QQ13yrf-8,3083
|
|
6
|
+
zettcode/plugin_api/panels.py,sha256=rtcu27JhfLSKlw5PDik-Gm1FIVRca25ecxqHEsvE41o,1729
|
|
7
|
+
zettcode/plugin_api/pets.py,sha256=q2opVI1psQqphjU0pSdAxhPmGwIHpDw_zgSZGFZVH9A,9858
|
|
8
|
+
zettcode/plugin_api/plugin.py,sha256=rF6BLQkEszdK6sD72a2qfDF7g4986nreIgoLMfJF_RU,4150
|
|
9
|
+
zettcode/plugin_api/presenters.py,sha256=EeNlDVjqUaQW4VtwEy1FVA8sWTwDwKoddYr5yK7iIJ4,13686
|
|
10
|
+
zettcode/plugin_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
zettcode/plugin_api/rows.py,sha256=d4EgXCNX17fS3UoUOC3n2rv_hDdel36o71iS_Xstr-o,3574
|
|
12
|
+
zettcode/plugin_api/state.py,sha256=FvhkdTvI_Kza0tqkFycSe8teHCD_-fpOT6HopXnlHZM,9183
|
|
13
|
+
zettcode/plugin_api/switch.py,sha256=r2ayyIY2E6a8lns3Dy6uwR7HL-6P9jmRIPEBpsrv_p4,3819
|
|
14
|
+
zettcode_plugin_api-0.4.0.dist-info/METADATA,sha256=gzqPlvor0Iv4-psRfLG-Znuc4Wi-drzcfufmLlnL5KQ,832
|
|
15
|
+
zettcode_plugin_api-0.4.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
16
|
+
zettcode_plugin_api-0.4.0.dist-info/RECORD,,
|