zettcode-plugin-api 0.4.1__tar.gz → 0.4.2__tar.gz
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-0.4.1 → zettcode_plugin_api-0.4.2}/.gitignore +5 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/PKG-INFO +1 -1
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/__init__.py +95 -3
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/commands.py +54 -11
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/entries.py +4 -4
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/presenters.py +50 -66
- zettcode_plugin_api-0.4.2/src/zettcode/plugin_api/services.py +33 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/state.py +1 -1
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/pyproject.toml +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/host.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/mixins.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/panels.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/pets.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/plugin.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/py.typed +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/rows.py +0 -0
- {zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/switch.py +0 -0
|
@@ -35,3 +35,8 @@ docs/internal/palettes.html
|
|
|
35
35
|
|
|
36
36
|
# The release order is generated from the pyproject.toml files (`make order`); it is not kept in git.
|
|
37
37
|
packages/order.txt
|
|
38
|
+
|
|
39
|
+
# The desktop and phone apps build the gateway's page into their own folders, and electron-builder writes its
|
|
40
|
+
# installers under release/.
|
|
41
|
+
clients/desktop/renderer/
|
|
42
|
+
clients/desktop/release/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""What a plugin imports: the contract, and the one dependency it needs.
|
|
2
2
|
|
|
3
|
-
A ZettCode plugin is a class published under the ``zettcode.
|
|
3
|
+
A ZettCode plugin is a class published under the ``zettcode.plugin`` entry point, and everything it may touch is
|
|
4
4
|
re-exported here: the base classes and hooks of the agent side and of the terminal's rows, the command a handler returns,
|
|
5
5
|
the row model and the presenter it implements, the pet format, the link to the shell, and the few widgets a panel is
|
|
6
6
|
drawn with.
|
|
@@ -29,6 +29,20 @@ _EXPORTS = {
|
|
|
29
29
|
"AgentBuild": "zettcode.engine.plugins",
|
|
30
30
|
"BusyPolicy": ".commands",
|
|
31
31
|
"CONTENT_INDENT": ".presenters",
|
|
32
|
+
"ContextReport": "zettcode.engine.extensions.context",
|
|
33
|
+
"ContextShare": "zettcode.engine.extensions.context",
|
|
34
|
+
"Ledger": "zettcode.engine.storage.ledger",
|
|
35
|
+
"SessionInfo": "zettcode.engine.storage.metadata",
|
|
36
|
+
"ContextPageService": ".services",
|
|
37
|
+
"ContextPageSnapshot": ".services",
|
|
38
|
+
"ContextPageWriter": ".services",
|
|
39
|
+
"ContextReportService": ".services",
|
|
40
|
+
"SessionStoreService": ".services",
|
|
41
|
+
"UsageLedgerService": ".services",
|
|
42
|
+
"SESSION_STORE": ".services",
|
|
43
|
+
"USAGE_LEDGER": ".services",
|
|
44
|
+
"CONTEXT_PAGE": ".services",
|
|
45
|
+
"CONTEXT_REPORT": ".services",
|
|
32
46
|
"Canvas": "zettcode.tui",
|
|
33
47
|
"ChangesEntry": ".entries",
|
|
34
48
|
"Command": ".commands",
|
|
@@ -54,6 +68,8 @@ _EXPORTS = {
|
|
|
54
68
|
"HOST_LINK": ".host",
|
|
55
69
|
"Host": "zettcode.tui",
|
|
56
70
|
"HostLink": ".host",
|
|
71
|
+
"INBOX": "zettcode.engine.inbox",
|
|
72
|
+
"Inbox": "zettcode.engine.inbox",
|
|
57
73
|
"MarkdownEntry": ".entries",
|
|
58
74
|
"Mention": "zettcode.mention",
|
|
59
75
|
"MentionProvider": "zettcode.mention",
|
|
@@ -62,10 +78,12 @@ _EXPORTS = {
|
|
|
62
78
|
"ModelState": ".state",
|
|
63
79
|
"MouseAction": "zettcode.tui",
|
|
64
80
|
"MouseEvent": "zettcode.tui",
|
|
81
|
+
"NextPresenter": ".presenters",
|
|
65
82
|
"NoticeLevel": ".entries",
|
|
66
83
|
"Pet": ".pets",
|
|
67
84
|
"PetSource": ".pets",
|
|
68
85
|
"PlainCommandContext": "zettcode.engine.plugins.commands",
|
|
86
|
+
"PlainCommandHost": "zettcode.engine.plugins.commands",
|
|
69
87
|
"Plugin": "zettcode.engine.plugins",
|
|
70
88
|
"PluginAgentMixin": "zettcode.engine.plugins",
|
|
71
89
|
"PluginContext": "zettcode.engine.plugins",
|
|
@@ -81,6 +99,14 @@ _EXPORTS = {
|
|
|
81
99
|
"PluginTurnMixin": "zettcode.engine.plugins",
|
|
82
100
|
"PluginUiMixin": ".mixins",
|
|
83
101
|
"Plugins": "zettcode.engine.plugins",
|
|
102
|
+
"LLM": "zettcode.engine.plugins",
|
|
103
|
+
"ModelAccess": "zettcode.engine.plugins",
|
|
104
|
+
"PLUGIN_AGENT": "zettcode.engine.plugins",
|
|
105
|
+
"PLUGIN_COMMANDS": "zettcode.engine.plugins",
|
|
106
|
+
"PLUGIN_CONFIG": "zettcode.engine.plugins",
|
|
107
|
+
"PLUGIN_SETTINGS": "zettcode.engine.plugins",
|
|
108
|
+
"PLUGIN_TUI": "zettcode.engine.plugins",
|
|
109
|
+
"PLUGIN_WORKSPACE": "zettcode.engine.plugins",
|
|
84
110
|
"Presenter": ".presenters",
|
|
85
111
|
"Presenters": ".presenters",
|
|
86
112
|
"Provider": "zettcode.providers",
|
|
@@ -103,6 +129,9 @@ _EXPORTS = {
|
|
|
103
129
|
"TuiGroup": "zettcode.plugin_tui",
|
|
104
130
|
"TuiPlugin": ".plugin",
|
|
105
131
|
"TuiPlugins": ".plugin",
|
|
132
|
+
"Trigger": "zettcode.engine.plugins",
|
|
133
|
+
"TriggerEvent": "zettcode.engine.plugins",
|
|
134
|
+
"TriggerScope": "zettcode.engine.plugins",
|
|
106
135
|
"TurnCommandResult": "zettcode.engine.plugins.commands",
|
|
107
136
|
"TurnSegment": ".state",
|
|
108
137
|
"UiBuilder": ".state",
|
|
@@ -111,6 +140,7 @@ _EXPORTS = {
|
|
|
111
140
|
"UiSegment": ".state",
|
|
112
141
|
"UiSide": ".state",
|
|
113
142
|
"WaitEntry": ".entries",
|
|
143
|
+
"Waiting": "zettcode.engine.inbox",
|
|
114
144
|
"Widget": "zettcode.tui",
|
|
115
145
|
"WriteEntry": ".entries",
|
|
116
146
|
"ZettCodeConfig": "zettcode.config",
|
|
@@ -125,10 +155,20 @@ _EXPORTS = {
|
|
|
125
155
|
|
|
126
156
|
if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
127
157
|
from zettcode.config import ModelConfig, ZettCodeConfig
|
|
158
|
+
from zettcode.engine.extensions.context import ContextReport, ContextShare
|
|
159
|
+
from zettcode.engine.inbox import INBOX, Inbox, Waiting
|
|
128
160
|
from zettcode.engine.plugins import (
|
|
161
|
+
LLM,
|
|
162
|
+
PLUGIN_AGENT,
|
|
163
|
+
PLUGIN_COMMANDS,
|
|
164
|
+
PLUGIN_CONFIG,
|
|
165
|
+
PLUGIN_SETTINGS,
|
|
166
|
+
PLUGIN_TUI,
|
|
167
|
+
PLUGIN_WORKSPACE,
|
|
129
168
|
AgentBuild,
|
|
130
169
|
Contribution,
|
|
131
170
|
ExtensionFactory,
|
|
171
|
+
ModelAccess,
|
|
132
172
|
Plugin,
|
|
133
173
|
PluginAgentMixin,
|
|
134
174
|
PluginContext,
|
|
@@ -143,8 +183,30 @@ if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
|
143
183
|
PluginSetupMixin,
|
|
144
184
|
PluginToolMixin,
|
|
145
185
|
PluginTurnMixin,
|
|
186
|
+
Trigger,
|
|
187
|
+
TriggerEvent,
|
|
188
|
+
TriggerScope,
|
|
146
189
|
)
|
|
147
|
-
from zettcode.engine.plugins.commands import
|
|
190
|
+
from zettcode.engine.plugins.commands import (
|
|
191
|
+
PlainCommandContext,
|
|
192
|
+
PlainCommandHost,
|
|
193
|
+
TextCommandResult,
|
|
194
|
+
TurnCommandResult,
|
|
195
|
+
)
|
|
196
|
+
from zettcode.engine.service_contracts import (
|
|
197
|
+
CONTEXT_PAGE,
|
|
198
|
+
CONTEXT_REPORT,
|
|
199
|
+
SESSION_STORE,
|
|
200
|
+
USAGE_LEDGER,
|
|
201
|
+
ContextPageService,
|
|
202
|
+
ContextPageSnapshot,
|
|
203
|
+
ContextPageWriter,
|
|
204
|
+
ContextReportService,
|
|
205
|
+
SessionStoreService,
|
|
206
|
+
UsageLedgerService,
|
|
207
|
+
)
|
|
208
|
+
from zettcode.engine.storage.ledger import Ledger
|
|
209
|
+
from zettcode.engine.storage.metadata import SessionInfo
|
|
148
210
|
from zettcode.mention import Mention, MentionProvider
|
|
149
211
|
from zettcode.plugin_tui import TuiGroup
|
|
150
212
|
from zettcode.providers import Provider
|
|
@@ -191,7 +253,7 @@ if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
|
191
253
|
from .panels import bottom_panel, pressed
|
|
192
254
|
from .pets import Pet, PetSource
|
|
193
255
|
from .plugin import TuiPlugin, TuiPlugins
|
|
194
|
-
from .presenters import CONTENT_INDENT, Presenter, Presenters
|
|
256
|
+
from .presenters import CONTENT_INDENT, NextPresenter, Presenter, Presenters
|
|
195
257
|
from .rows import Row
|
|
196
258
|
from .state import (
|
|
197
259
|
Activity,
|
|
@@ -236,6 +298,20 @@ __all__ = [
|
|
|
236
298
|
"AgentBuild",
|
|
237
299
|
"BusyPolicy",
|
|
238
300
|
"CONTENT_INDENT",
|
|
301
|
+
"CONTEXT_PAGE",
|
|
302
|
+
"CONTEXT_REPORT",
|
|
303
|
+
"SESSION_STORE",
|
|
304
|
+
"USAGE_LEDGER",
|
|
305
|
+
"ContextReport",
|
|
306
|
+
"ContextShare",
|
|
307
|
+
"Ledger",
|
|
308
|
+
"SessionInfo",
|
|
309
|
+
"ContextPageService",
|
|
310
|
+
"ContextPageSnapshot",
|
|
311
|
+
"ContextPageWriter",
|
|
312
|
+
"ContextReportService",
|
|
313
|
+
"SessionStoreService",
|
|
314
|
+
"UsageLedgerService",
|
|
239
315
|
"Canvas",
|
|
240
316
|
"ChangesEntry",
|
|
241
317
|
"Command",
|
|
@@ -261,6 +337,8 @@ __all__ = [
|
|
|
261
337
|
"HOST_LINK",
|
|
262
338
|
"Host",
|
|
263
339
|
"HostLink",
|
|
340
|
+
"INBOX",
|
|
341
|
+
"Inbox",
|
|
264
342
|
"MarkdownEntry",
|
|
265
343
|
"Mention",
|
|
266
344
|
"MentionProvider",
|
|
@@ -269,10 +347,12 @@ __all__ = [
|
|
|
269
347
|
"ModelState",
|
|
270
348
|
"MouseAction",
|
|
271
349
|
"MouseEvent",
|
|
350
|
+
"NextPresenter",
|
|
272
351
|
"NoticeLevel",
|
|
273
352
|
"Pet",
|
|
274
353
|
"PetSource",
|
|
275
354
|
"PlainCommandContext",
|
|
355
|
+
"PlainCommandHost",
|
|
276
356
|
"Plugin",
|
|
277
357
|
"PluginAgentMixin",
|
|
278
358
|
"PluginContext",
|
|
@@ -288,6 +368,14 @@ __all__ = [
|
|
|
288
368
|
"PluginTurnMixin",
|
|
289
369
|
"PluginUiMixin",
|
|
290
370
|
"Plugins",
|
|
371
|
+
"LLM",
|
|
372
|
+
"ModelAccess",
|
|
373
|
+
"PLUGIN_AGENT",
|
|
374
|
+
"PLUGIN_COMMANDS",
|
|
375
|
+
"PLUGIN_CONFIG",
|
|
376
|
+
"PLUGIN_SETTINGS",
|
|
377
|
+
"PLUGIN_TUI",
|
|
378
|
+
"PLUGIN_WORKSPACE",
|
|
291
379
|
"Presenter",
|
|
292
380
|
"Presenters",
|
|
293
381
|
"Provider",
|
|
@@ -310,6 +398,9 @@ __all__ = [
|
|
|
310
398
|
"TuiGroup",
|
|
311
399
|
"TuiPlugin",
|
|
312
400
|
"TuiPlugins",
|
|
401
|
+
"Trigger",
|
|
402
|
+
"TriggerEvent",
|
|
403
|
+
"TriggerScope",
|
|
313
404
|
"TurnCommandResult",
|
|
314
405
|
"TurnSegment",
|
|
315
406
|
"UiBuilder",
|
|
@@ -318,6 +409,7 @@ __all__ = [
|
|
|
318
409
|
"UiSegment",
|
|
319
410
|
"UiSide",
|
|
320
411
|
"WaitEntry",
|
|
412
|
+
"Waiting",
|
|
321
413
|
"Widget",
|
|
322
414
|
"WriteEntry",
|
|
323
415
|
"ZettCodeConfig",
|
|
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from collections.abc import Awaitable, Callable, Sequence
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Literal,
|
|
9
|
+
from typing import TYPE_CHECKING, Literal, TypeAlias
|
|
10
10
|
|
|
11
11
|
from zettcode.plugin_api.entries import ThinkingEntry
|
|
12
12
|
from zettcode.registry import Provider
|
|
@@ -45,7 +45,7 @@ class CommandUi(ABC):
|
|
|
45
45
|
"""Show a one-off toast, replacing any that is still on screen."""
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class CommandAgent(
|
|
48
|
+
class CommandAgent(ABC):
|
|
49
49
|
"""What a command may read of the application agent, without importing the terminal that implements it.
|
|
50
50
|
|
|
51
51
|
A command needs to know which session it is in and where that session works; the rest of the agent is the
|
|
@@ -53,17 +53,21 @@ class CommandAgent(Protocol):
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
@property
|
|
56
|
+
@abstractmethod
|
|
56
57
|
def session_id(self) -> str:
|
|
57
58
|
"""Return the session this shell is working in."""
|
|
58
59
|
|
|
59
60
|
@property
|
|
61
|
+
@abstractmethod
|
|
60
62
|
def workspace(self) -> Path:
|
|
61
63
|
"""Return the directory the session works in."""
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
#: Set once when the agent is built. It stays a plain attribute because the engine's `AgentBase` declares it
|
|
66
|
+
#: that way; a property here would need the agent to expose a backer, which is not this contract's to arrange.
|
|
67
|
+
#: A command reads it and does not assign it.
|
|
68
|
+
runtime: ZettCodeEngine
|
|
66
69
|
|
|
70
|
+
@abstractmethod
|
|
67
71
|
def require_session(self) -> None:
|
|
68
72
|
"""Refuse to go on when the active session is another process's.
|
|
69
73
|
|
|
@@ -72,29 +76,35 @@ class CommandAgent(Protocol):
|
|
|
72
76
|
"""
|
|
73
77
|
|
|
74
78
|
|
|
75
|
-
class CommandTranscript(
|
|
79
|
+
class CommandTranscript(ABC):
|
|
76
80
|
"""What a command may add to the conversation: a note, an error, or a row of Markdown."""
|
|
77
81
|
|
|
82
|
+
@abstractmethod
|
|
78
83
|
def notice(self, text: str) -> object:
|
|
79
84
|
"""Add a line that says something happened."""
|
|
80
85
|
|
|
86
|
+
@abstractmethod
|
|
81
87
|
def error(self, text: str) -> object:
|
|
82
88
|
"""Add a line that says something failed."""
|
|
83
89
|
|
|
90
|
+
@abstractmethod
|
|
84
91
|
def markdown(self, text: str) -> object:
|
|
85
92
|
"""Add a row of Markdown, which a command's own answer is."""
|
|
86
93
|
|
|
94
|
+
@abstractmethod
|
|
87
95
|
def start_compaction(self) -> ThinkingEntry:
|
|
88
96
|
"""Open the row that shows a compaction working."""
|
|
89
97
|
|
|
98
|
+
@abstractmethod
|
|
90
99
|
def complete_compaction(self, text: str = "") -> None:
|
|
91
100
|
"""Close the running compaction row and stamp its elapsed time."""
|
|
92
101
|
|
|
102
|
+
@abstractmethod
|
|
93
103
|
def clear(self) -> None:
|
|
94
104
|
"""Forget every row: the conversation starts over on screen, as a new session's does."""
|
|
95
105
|
|
|
96
106
|
|
|
97
|
-
class CommandHost(
|
|
107
|
+
class CommandHost(ABC):
|
|
98
108
|
"""What a command that acts on the application, and not only on the agent, may ask of the shell.
|
|
99
109
|
|
|
100
110
|
A command that opens a panel, switches the session or changes the model needs more than a place to write:
|
|
@@ -110,23 +120,35 @@ class CommandHost(Protocol):
|
|
|
110
120
|
commands: Every command the shell knows, in the order ``/help`` lists them.
|
|
111
121
|
"""
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
@property
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def client(self) -> Client:
|
|
126
|
+
"""Return the SDK client over the application agent."""
|
|
115
127
|
|
|
116
128
|
@property
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def commands(self) -> tuple[Command, ...]:
|
|
131
|
+
"""Return every command the shell knows, in the order ``/help`` lists them."""
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
@abstractmethod
|
|
117
135
|
def agent(self) -> CommandAgent:
|
|
118
|
-
"""Return the application agent
|
|
136
|
+
"""Return the application agent, read-only."""
|
|
119
137
|
|
|
120
138
|
@property
|
|
139
|
+
@abstractmethod
|
|
121
140
|
def transcript(self) -> CommandTranscript:
|
|
122
|
-
"""Return the conversation as drawn
|
|
141
|
+
"""Return the conversation as drawn, read-only."""
|
|
123
142
|
|
|
143
|
+
@abstractmethod
|
|
124
144
|
def close_page(self) -> None:
|
|
125
145
|
"""Close the page a command presented and return to the composer."""
|
|
126
146
|
|
|
147
|
+
@abstractmethod
|
|
127
148
|
def request_layout(self) -> None:
|
|
128
149
|
"""Ask for the screen to be laid out again, after a change in what it shows."""
|
|
129
150
|
|
|
151
|
+
@abstractmethod
|
|
130
152
|
async def wait_for_frame(self) -> None:
|
|
131
153
|
"""Wait until the next frame has reached the terminal.
|
|
132
154
|
|
|
@@ -135,30 +157,39 @@ class CommandHost(Protocol):
|
|
|
135
157
|
work begins instead of after it.
|
|
136
158
|
"""
|
|
137
159
|
|
|
160
|
+
@abstractmethod
|
|
138
161
|
def offer(self, command: str) -> None:
|
|
139
162
|
"""Put ``command`` and a space in the draft, which opens the menu of the words it takes."""
|
|
140
163
|
|
|
164
|
+
@abstractmethod
|
|
141
165
|
def quit(self) -> None:
|
|
142
166
|
"""Leave the application."""
|
|
143
167
|
|
|
168
|
+
@abstractmethod
|
|
144
169
|
def select_session(self, session_id: str) -> None:
|
|
145
170
|
"""Switch to a session picked in a panel: close the panel and resume behind the interface."""
|
|
146
171
|
|
|
172
|
+
@abstractmethod
|
|
147
173
|
async def resume_session(self, session_id: str) -> None:
|
|
148
174
|
"""Restore a stored session without holding the event loop for the work."""
|
|
149
175
|
|
|
176
|
+
@abstractmethod
|
|
150
177
|
async def fork_session(self) -> str:
|
|
151
178
|
"""Carry this conversation into a new session and continue there; return the new id."""
|
|
152
179
|
|
|
180
|
+
@abstractmethod
|
|
153
181
|
async def rename_session(self, title: str) -> str:
|
|
154
182
|
"""Name the active session and return the name as stored."""
|
|
155
183
|
|
|
184
|
+
@abstractmethod
|
|
156
185
|
async def run_continue(self) -> None:
|
|
157
186
|
"""Carry on the request that was interrupted."""
|
|
158
187
|
|
|
188
|
+
@abstractmethod
|
|
159
189
|
async def run_side_question(self, question: str) -> None:
|
|
160
190
|
"""Answer a question that is kept out of the conversation."""
|
|
161
191
|
|
|
192
|
+
@abstractmethod
|
|
162
193
|
async def prompt(self, text: str, *, shown: str | None = None, cache: str | None = None) -> bool:
|
|
163
194
|
"""Run ``text`` as one turn of this conversation, exactly as if the reader had typed it.
|
|
164
195
|
|
|
@@ -189,15 +220,19 @@ class CommandHost(Protocol):
|
|
|
189
220
|
mid-request says ``when_busy="queue"``, and this then runs when the request in flight has ended.
|
|
190
221
|
"""
|
|
191
222
|
|
|
223
|
+
@abstractmethod
|
|
192
224
|
def change_model(self, name: ModelInfo | str) -> ModelInfo:
|
|
193
225
|
"""Switch models and announce the change in the transcript."""
|
|
194
226
|
|
|
227
|
+
@abstractmethod
|
|
195
228
|
def select_model(self, name: ModelInfo) -> None:
|
|
196
229
|
"""Apply a model picked in a panel and close the panel."""
|
|
197
230
|
|
|
231
|
+
@abstractmethod
|
|
198
232
|
def change_effort(self, name: str) -> str:
|
|
199
233
|
"""Switch reasoning effort and announce the change."""
|
|
200
234
|
|
|
235
|
+
@abstractmethod
|
|
201
236
|
def change_permission(self, name: str) -> str:
|
|
202
237
|
"""Set how much of the shell may run unasked and announce the change."""
|
|
203
238
|
|
|
@@ -208,6 +243,8 @@ class CommandContext:
|
|
|
208
243
|
|
|
209
244
|
Attributes:
|
|
210
245
|
argument: Trimmed text after the command name; ``""`` when none.
|
|
246
|
+
arguments: The same words split on whitespace, so a command taking several values — daemon names, short
|
|
247
|
+
values without spaces — can read them without parsing ``argument`` itself.
|
|
211
248
|
ui: Surfaces the command may write to while it runs.
|
|
212
249
|
host: The shell, for a command that acts on the application itself; ``None`` where there is no shell,
|
|
213
250
|
such as a handler called directly in a test. Use :meth:`require_host` in a command that needs it.
|
|
@@ -216,6 +253,12 @@ class CommandContext:
|
|
|
216
253
|
argument: str
|
|
217
254
|
ui: CommandUi
|
|
218
255
|
host: CommandHost | None = None
|
|
256
|
+
arguments: tuple[str, ...] = ()
|
|
257
|
+
|
|
258
|
+
def __post_init__(self) -> None:
|
|
259
|
+
"""Derive the list form when the caller did not give one."""
|
|
260
|
+
if not self.arguments:
|
|
261
|
+
object.__setattr__(self, "arguments", tuple(self.argument.split()))
|
|
219
262
|
|
|
220
263
|
def require_host(self) -> CommandHost:
|
|
221
264
|
"""Return the shell, or raise ``RuntimeError`` when the command was run without one."""
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""The conversation's entries: plain data, one class for each way a row can look.
|
|
2
2
|
|
|
3
3
|
An entry says *what happened* — a user asked, a tool ran, the model is thinking —
|
|
4
|
-
and nothing about how that is drawn. The drawing is ``presenters.py``,
|
|
5
|
-
|
|
6
|
-
does to it, is ``transcript.py``.
|
|
4
|
+
and nothing about how that is drawn. The drawing is ``presenters.py``, whose chain each presenter takes part in
|
|
5
|
+
through ``handle``, claiming a row by its :class:`EntryKind` or, for a tool, by its name; the list of entries, and
|
|
6
|
+
what the agent does to it, is ``transcript.py``.
|
|
7
7
|
|
|
8
8
|
Two base classes carry what the concrete ones share. :class:`Entry` is identity
|
|
9
9
|
and a place for the presenter to leave its rendered rows; :class:`TimedEntry`
|
|
@@ -222,7 +222,7 @@ class ToolEntry(TimedEntry):
|
|
|
222
222
|
|
|
223
223
|
A tool whose row shows something of its own has an entry of its own that adds it
|
|
224
224
|
(:class:`ReadEntry`, :class:`WriteEntry`, :class:`EditEntry`); they are all ``tool`` rows,
|
|
225
|
-
folded and timed alike, and the presenter
|
|
225
|
+
folded and timed alike, and the presenter for each claims it by the tool's own name.
|
|
226
226
|
Which class a call gets is decided by the render handler that describes it
|
|
227
227
|
(:class:`~.rendering.ToolRow`), so nothing here, and nothing in the transcript, names a tool.
|
|
228
228
|
"""
|
{zettcode_plugin_api-0.4.1 → zettcode_plugin_api-0.4.2}/src/zettcode/plugin_api/presenters.py
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""What a plugin implements to draw a row: the base class, and how a drawn block is cached.
|
|
2
2
|
|
|
3
|
-
A presenter turns one kind of
|
|
3
|
+
A presenter turns one kind of row into lines. The built-in ones live in `zettcode.presenters`; a plugin registers its
|
|
4
4
|
own through `ctx.tui.register_presenter`, and the row it draws is the same kind of object, so this — the base class and
|
|
5
|
-
the rule for when a row is drawn again — is part of the plugin contract, not part of the terminal.
|
|
5
|
+
the rule for when a row is drawn again — is part of the plugin contract, not part of the terminal. A presenter takes
|
|
6
|
+
part in the registry's chain through its own `handle(entry, next)`: it returns itself to claim the row, or calls
|
|
7
|
+
`next(entry)` to leave it to the presenters after it.
|
|
6
8
|
|
|
7
9
|
The registry (`Presenters`) is here and not with the built-in rows because `Presenter.block` takes it as its `owner`:
|
|
8
10
|
the base class names the registry in its own signature, so moving the registry out would make the base class import its
|
|
@@ -15,14 +17,14 @@ the width, the palette, the registry and what the row looks like, so a frame tha
|
|
|
15
17
|
from __future__ import annotations
|
|
16
18
|
|
|
17
19
|
from abc import ABC, abstractmethod
|
|
18
|
-
from collections.abc import Iterable
|
|
20
|
+
from collections.abc import Callable, Iterable
|
|
19
21
|
from dataclasses import fields as dataclass_fields
|
|
20
22
|
from typing import Any, ClassVar, Generic, TypeVar
|
|
21
23
|
|
|
22
24
|
from zettcode.tui import LineSource, Span, StaticLines, Style, TextLine, Theme
|
|
23
25
|
from zettcode.tui.render import inset_line, layout_rich_lines, wrap_words
|
|
24
26
|
|
|
25
|
-
from .entries import Entry
|
|
27
|
+
from .entries import Entry
|
|
26
28
|
|
|
27
29
|
#: Columns a row's content is inset by, past the gutter marker.
|
|
28
30
|
CONTENT_INDENT = 2
|
|
@@ -30,6 +32,9 @@ CONTENT_INDENT = 2
|
|
|
30
32
|
#: The entry a presenter accepts; each subclass pins it to one entry type.
|
|
31
33
|
E = TypeVar("E", bound=Entry)
|
|
32
34
|
|
|
35
|
+
#: The rest of the chain: asks the presenters after this one who draws the entry.
|
|
36
|
+
NextPresenter = Callable[[Entry], "Presenter[Any]"]
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
#: The entry fields that are not part of how a row looks: its identity, and the
|
|
35
40
|
#: slots the cache itself lives in.
|
|
@@ -55,15 +60,9 @@ def _look_fields(entry_type: type) -> tuple[str, ...]:
|
|
|
55
60
|
class Presenter(Generic[E], ABC):
|
|
56
61
|
"""Turns one kind of entry into rows.
|
|
57
62
|
|
|
58
|
-
The type parameter is the entry the presenter handles,
|
|
59
|
-
kind that routes it there, so :meth:`lines` receives exactly that entry.
|
|
63
|
+
The type parameter is the entry the presenter handles: :meth:`handle` claims it, then :meth:`lines` draws it.
|
|
60
64
|
|
|
61
65
|
Attributes:
|
|
62
|
-
kind: The kind of entry this presenter presents; one presenter per kind.
|
|
63
|
-
entry_type: The entry class this presenter is for, when it is for one family of a kind
|
|
64
|
-
rather than the kind as a whole: the presenter of an edit row is the one for
|
|
65
|
-
``EditEntry``, though an edit row is a ``tool`` row like any other. The registry looks
|
|
66
|
-
for the entry's own class first (and its parents') and the kind second.
|
|
67
66
|
gutter: Whether the rows open with a marker the presenter draws in the
|
|
68
67
|
gutter's own column — the user's arrow, the wait row's sparkle, a
|
|
69
68
|
tool's outcome. Every other row is inset past the gutter, so every
|
|
@@ -75,11 +74,18 @@ class Presenter(Generic[E], ABC):
|
|
|
75
74
|
kind, or to beat another plugin, raises this.
|
|
76
75
|
"""
|
|
77
76
|
|
|
78
|
-
kind: ClassVar[EntryKind]
|
|
79
|
-
entry_type: ClassVar[type[Entry] | None] = None
|
|
80
77
|
gutter: ClassVar[bool] = False
|
|
81
78
|
priority: ClassVar[int] = 0
|
|
82
79
|
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def handle(self, entry: Entry, next: NextPresenter) -> Presenter[Any]:
|
|
82
|
+
"""Return the presenter that draws ``entry``, handing it to ``next`` when this one does not claim it.
|
|
83
|
+
|
|
84
|
+
This is the chain's one decision: every presenter is asked in turn, and each answers for itself. A
|
|
85
|
+
presenter returns ``self`` to claim the entry, or calls ``next(entry)`` to pass it on. The registry orders
|
|
86
|
+
the chain and starts it, but never matches an entry itself.
|
|
87
|
+
"""
|
|
88
|
+
|
|
83
89
|
@abstractmethod
|
|
84
90
|
def lines(self, entry: E, width: int, theme: Theme, frame: int) -> list[TextLine]:
|
|
85
91
|
"""Return the rows this entry paints at the available width."""
|
|
@@ -202,71 +208,49 @@ class Indented(LineSource):
|
|
|
202
208
|
|
|
203
209
|
|
|
204
210
|
class Presenters:
|
|
205
|
-
"""The
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
When more than one presenter could draw the same rows, the one with the higher
|
|
212
|
-
:attr:`Presenter.priority` wins, and an equal priority goes to the later registration. That is
|
|
213
|
-
what makes a transcript's own presenters the default: they are registered first, at priority 0,
|
|
214
|
-
so anything a plugin registers for the same rows wins without having to outrank them.
|
|
215
|
-
:meth:`replacing` appends, which is the same thing for a caller holding a registry rather than a
|
|
216
|
-
plugin container.
|
|
217
|
-
|
|
218
|
-
A presenter for a whole kind is a claim on every row of that kind, so it beats the families
|
|
219
|
-
registered before it at its priority: a plugin that draws every tool row draws every tool row.
|
|
220
|
-
A family registered after it, or at a higher priority, is a refinement and keeps its own.
|
|
211
|
+
"""The chain of presenters one transcript asks, strongest claim first, with a fallback last.
|
|
212
|
+
|
|
213
|
+
Every presenter in the chain is asked through :meth:`Presenter.handle`: it decides whether it draws the entry
|
|
214
|
+
and hands it to the next presenter when it does not. The chain is ordered by :attr:`Presenter.priority` and then
|
|
215
|
+
by registration order, later first. The registry only starts the chain; it never matches an entry itself.
|
|
221
216
|
"""
|
|
222
217
|
|
|
223
218
|
def __init__(self, presenters: Iterable[Presenter[Any]]) -> None:
|
|
224
|
-
"""
|
|
225
|
-
self.
|
|
226
|
-
self.
|
|
227
|
-
for
|
|
228
|
-
rank = (presenter.priority, order)
|
|
229
|
-
if presenter.entry_type is not None:
|
|
230
|
-
current = self._by_type.get(presenter.entry_type)
|
|
231
|
-
if current is None or rank >= current[:2]:
|
|
232
|
-
self._by_type[presenter.entry_type] = (*rank, presenter)
|
|
233
|
-
else:
|
|
234
|
-
current = self._by_kind.get(presenter.kind)
|
|
235
|
-
if current is None or rank >= current[:2]:
|
|
236
|
-
self._by_kind[presenter.kind] = (*rank, presenter)
|
|
219
|
+
"""Build the chain; a later registration wins at equal priority."""
|
|
220
|
+
self._registered = tuple(presenters)
|
|
221
|
+
ordered = sorted(enumerate(self._registered), key=lambda pair: (pair[1].priority, pair[0]), reverse=True)
|
|
222
|
+
self._chain: tuple[Presenter[Any], ...] = tuple(presenter for _, presenter in ordered)
|
|
237
223
|
|
|
238
224
|
@property
|
|
239
225
|
def presenters(self) -> tuple[Presenter[Any], ...]:
|
|
240
|
-
"""Return the presenters
|
|
226
|
+
"""Return the presenters in registration order, which is what a new registry is built from."""
|
|
227
|
+
return self._registered
|
|
241
228
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
presenter
|
|
247
|
-
for priority, order, presenter in self._by_type.values()
|
|
248
|
-
if self._by_kind.get(presenter.kind) is None or (priority, order) > self._by_kind[presenter.kind][:2]
|
|
249
|
-
)
|
|
250
|
-
return tuple(live)
|
|
229
|
+
@property
|
|
230
|
+
def chain(self) -> tuple[Presenter[Any], ...]:
|
|
231
|
+
"""Return the presenters in the order the chain asks them."""
|
|
232
|
+
return self._chain
|
|
251
233
|
|
|
252
234
|
def replacing(self, *presenters: Presenter[Any]) -> Presenters:
|
|
253
|
-
"""Return a registry that
|
|
254
|
-
return Presenters((*self.
|
|
235
|
+
"""Return a registry that appends the given presenters to this chain."""
|
|
236
|
+
return Presenters((*self._registered, *presenters))
|
|
255
237
|
|
|
256
238
|
def for_entry(self, entry: Entry) -> Presenter[Any]:
|
|
257
|
-
"""
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
kind = self._by_kind.get(entry.kind)
|
|
265
|
-
if family is not None and (kind is None or family[:2] > kind[:2]):
|
|
266
|
-
return family[2]
|
|
267
|
-
if kind is None:
|
|
239
|
+
"""Ask the chain, presenter by presenter, which one draws the entry, else raise.
|
|
240
|
+
|
|
241
|
+
The registry only starts the chain: each presenter's :meth:`Presenter.handle` decides whether it claims
|
|
242
|
+
the entry and hands it to the next when it does not.
|
|
243
|
+
"""
|
|
244
|
+
chain = self._chain
|
|
245
|
+
if not chain:
|
|
268
246
|
raise ValueError(f"No presenter for a {entry.kind.value} row")
|
|
269
|
-
|
|
247
|
+
|
|
248
|
+
def ask(current: Entry, index: int) -> Presenter[Any]:
|
|
249
|
+
if index == len(chain):
|
|
250
|
+
raise ValueError(f"No presenter for a {current.kind.value} row")
|
|
251
|
+
return chain[index].handle(current, lambda entry: ask(entry, index + 1))
|
|
252
|
+
|
|
253
|
+
return ask(entry, 0)
|
|
270
254
|
|
|
271
255
|
def lines(self, entry: Entry, width: int, theme: Theme, frame: int) -> list[TextLine]:
|
|
272
256
|
"""Return the rows an entry paints, rendered now and not cached."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Re-exports the page-facing service contracts the engine defines.
|
|
2
|
+
|
|
3
|
+
The engine owns the names and the ABCs (they are implemented by engine objects, and the engine cannot import this
|
|
4
|
+
package); this module is the one a plugin imports, so a page depends on the contract rather than on the engine path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zettcode.engine.service_contracts import (
|
|
10
|
+
CONTEXT_PAGE,
|
|
11
|
+
CONTEXT_REPORT,
|
|
12
|
+
SESSION_STORE,
|
|
13
|
+
USAGE_LEDGER,
|
|
14
|
+
ContextPageService,
|
|
15
|
+
ContextPageSnapshot,
|
|
16
|
+
ContextPageWriter,
|
|
17
|
+
ContextReportService,
|
|
18
|
+
SessionStoreService,
|
|
19
|
+
UsageLedgerService,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"CONTEXT_PAGE",
|
|
24
|
+
"CONTEXT_REPORT",
|
|
25
|
+
"SESSION_STORE",
|
|
26
|
+
"USAGE_LEDGER",
|
|
27
|
+
"ContextPageService",
|
|
28
|
+
"ContextPageSnapshot",
|
|
29
|
+
"ContextPageWriter",
|
|
30
|
+
"ContextReportService",
|
|
31
|
+
"SessionStoreService",
|
|
32
|
+
"UsageLedgerService",
|
|
33
|
+
]
|
|
@@ -33,7 +33,7 @@ UiRegion: TypeAlias = Literal["header", "status"]
|
|
|
33
33
|
#: registration method it calls, or the builtin segment it replaces, does.
|
|
34
34
|
UiSide: TypeAlias = Literal["left", "right"]
|
|
35
35
|
|
|
36
|
-
#: What one segment paints: a styled line, a plain string, a :class:`~zettcode.
|
|
36
|
+
#: What one segment paints: a styled line, a plain string, a :class:`~zettcode.plugin_api.rows.Row` of
|
|
37
37
|
#: named parts the reader may switch off, or nothing. The alias quotes its members because
|
|
38
38
|
#: :class:`TextLine` is imported only for the checker; an alias evaluates eagerly, so the names must stay strings.
|
|
39
39
|
UiSegmentValue: TypeAlias = "TextLine | Row | str | None"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|