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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any