ommlds 0.0.0.dev489__py3-none-any.whl → 0.0.0.dev491__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.
- ommlds/.omlish-manifests.json +3 -3
- ommlds/README.md +11 -0
- ommlds/__about__.py +1 -1
- ommlds/backends/ollama/_dataclasses.py +53 -23
- ommlds/backends/ollama/protocol.py +3 -0
- ommlds/cli/_dataclasses.py +835 -356
- ommlds/cli/main.py +80 -41
- ommlds/cli/rendering/types.py +6 -0
- ommlds/cli/sessions/chat/configs.py +4 -12
- ommlds/cli/sessions/chat/{chat → drivers}/ai/configs.py +3 -1
- ommlds/cli/sessions/chat/drivers/ai/events.py +57 -0
- ommlds/cli/sessions/chat/{chat → drivers}/ai/inject.py +10 -3
- ommlds/cli/sessions/chat/{chat → drivers}/ai/rendering.py +1 -1
- ommlds/cli/sessions/chat/{chat → drivers}/ai/services.py +1 -1
- ommlds/cli/sessions/chat/{chat → drivers}/ai/tools.py +1 -1
- ommlds/cli/sessions/chat/{chat → drivers}/ai/types.py +9 -0
- ommlds/cli/sessions/chat/drivers/configs.py +25 -0
- ommlds/cli/sessions/chat/drivers/driver.py +49 -0
- ommlds/cli/sessions/chat/drivers/events/inject.py +27 -0
- ommlds/cli/sessions/chat/drivers/events/injection.py +14 -0
- ommlds/cli/sessions/chat/drivers/events/manager.py +16 -0
- ommlds/cli/sessions/chat/drivers/events/types.py +38 -0
- ommlds/cli/sessions/chat/drivers/inject.py +69 -0
- ommlds/cli/sessions/chat/{chat → drivers}/state/inject.py +3 -3
- ommlds/cli/sessions/chat/{chat → drivers}/state/types.py +1 -1
- ommlds/cli/sessions/chat/{tools → drivers/tools}/configs.py +2 -2
- ommlds/cli/sessions/chat/drivers/tools/confirmation.py +44 -0
- ommlds/cli/sessions/chat/{tools → drivers/tools}/execution.py +3 -4
- ommlds/cli/sessions/chat/{tools → drivers/tools}/fs/inject.py +3 -3
- ommlds/cli/sessions/chat/{tools → drivers/tools}/inject.py +4 -12
- ommlds/cli/sessions/chat/{tools → drivers/tools}/injection.py +1 -1
- ommlds/cli/sessions/chat/{tools → drivers/tools}/rendering.py +3 -3
- ommlds/cli/sessions/chat/{tools → drivers/tools}/todo/inject.py +3 -3
- ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/tools.py +1 -1
- ommlds/cli/sessions/chat/drivers/types.py +10 -0
- ommlds/cli/sessions/chat/{chat → drivers}/user/configs.py +0 -2
- ommlds/cli/sessions/chat/drivers/user/inject.py +40 -0
- ommlds/cli/sessions/chat/inject.py +4 -32
- ommlds/cli/sessions/chat/interfaces/bare/inject.py +61 -0
- ommlds/cli/sessions/chat/interfaces/bare/interactive.py +41 -0
- ommlds/cli/sessions/chat/{interface/bare/interface.py → interfaces/bare/oneshot.py} +5 -3
- ommlds/cli/sessions/chat/{tools/confirmation.py → interfaces/bare/tools.py} +3 -22
- ommlds/cli/sessions/chat/{interface → interfaces}/bare/user.py +1 -1
- ommlds/cli/sessions/chat/{interface → interfaces}/configs.py +8 -0
- ommlds/cli/sessions/chat/interfaces/textual/__init__.py +0 -0
- ommlds/cli/sessions/chat/interfaces/textual/app.py +217 -0
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +67 -0
- ommlds/cli/sessions/chat/{interface → interfaces}/textual/interface.py +0 -3
- ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss +51 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +104 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +114 -0
- ommlds/cli/sessions/chat/session.py +1 -1
- ommlds/minichain/backends/impls/ollama/chat.py +24 -56
- ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
- ommlds/nanochat/rustbpe/README.md +9 -0
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/RECORD +91 -73
- ommlds/cli/sessions/chat/chat/user/inject.py +0 -52
- ommlds/cli/sessions/chat/chat/user/oneshot.py +0 -25
- ommlds/cli/sessions/chat/chat/user/types.py +0 -15
- ommlds/cli/sessions/chat/driver.py +0 -43
- ommlds/cli/sessions/chat/interface/bare/inject.py +0 -32
- ommlds/cli/sessions/chat/interface/textual/app.py +0 -191
- ommlds/cli/sessions/chat/interface/textual/inject.py +0 -27
- ommlds/cli/sessions/chat/interface/textual/user.py +0 -20
- /ommlds/cli/sessions/chat/{chat → drivers}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{chat → drivers}/ai/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{chat → drivers}/ai/injection.py +0 -0
- /ommlds/cli/sessions/chat/{chat/state → drivers/events}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{chat/user → drivers/phases}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{phases → drivers/phases}/inject.py +0 -0
- /ommlds/cli/sessions/chat/{phases → drivers/phases}/injection.py +0 -0
- /ommlds/cli/sessions/chat/{phases → drivers/phases}/manager.py +0 -0
- /ommlds/cli/sessions/chat/{phases → drivers/phases}/types.py +0 -0
- /ommlds/cli/sessions/chat/{interface → drivers/state}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{chat → drivers}/state/configs.py +0 -0
- /ommlds/cli/sessions/chat/{chat → drivers}/state/inmemory.py +0 -0
- /ommlds/cli/sessions/chat/{chat → drivers}/state/storage.py +0 -0
- /ommlds/cli/sessions/chat/{interface/bare → drivers/tools}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{interface/textual → drivers/tools/fs}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{tools → drivers/tools}/fs/configs.py +0 -0
- /ommlds/cli/sessions/chat/{phases → drivers/tools/todo}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{tools → drivers/tools}/todo/configs.py +0 -0
- /ommlds/cli/sessions/chat/{tools → drivers/tools/weather}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/configs.py +0 -0
- /ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/inject.py +0 -0
- /ommlds/cli/sessions/chat/{tools/fs → drivers/user}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{tools/todo → interfaces}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{tools/weather → interfaces/bare}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{interface → interfaces}/base.py +0 -0
- /ommlds/cli/sessions/chat/{interface → interfaces}/inject.py +0 -0
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omdev.tui import textual as tx
|
|
5
|
+
from omlish import check
|
|
6
|
+
|
|
7
|
+
from ...... import minichain as mc
|
|
8
|
+
from ...drivers.driver import ChatDriver
|
|
9
|
+
from ...drivers.events.types import AiDeltaChatEvent
|
|
10
|
+
from ...drivers.events.types import AiMessagesChatEvent
|
|
11
|
+
from .styles import read_app_css
|
|
12
|
+
from .widgets.input import InputOuter
|
|
13
|
+
from .widgets.input import InputTextArea
|
|
14
|
+
from .widgets.messages import AiMessage
|
|
15
|
+
from .widgets.messages import StaticAiMessage
|
|
16
|
+
from .widgets.messages import StreamAiMessage
|
|
17
|
+
from .widgets.messages import UserMessage
|
|
18
|
+
from .widgets.messages import WelcomeMessage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ChatDriverEventQueue = ta.NewType('ChatDriverEventQueue', asyncio.Queue)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChatApp(tx.App):
|
|
31
|
+
ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
driver: ChatDriver,
|
|
37
|
+
event_queue: ChatDriverEventQueue,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
self._chat_driver = driver
|
|
42
|
+
self._event_queue = event_queue
|
|
43
|
+
|
|
44
|
+
def get_driver_class(self) -> type[tx.Driver]:
|
|
45
|
+
return tx.get_pending_writes_driver_class(super().get_driver_class())
|
|
46
|
+
|
|
47
|
+
CSS: ta.ClassVar[str] = read_app_css()
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
|
|
51
|
+
def compose(self) -> tx.ComposeResult:
|
|
52
|
+
with tx.VerticalScroll(id='messages-scroll'):
|
|
53
|
+
yield tx.Static(id='messages-container')
|
|
54
|
+
|
|
55
|
+
yield InputOuter(id='input-outer')
|
|
56
|
+
|
|
57
|
+
#
|
|
58
|
+
|
|
59
|
+
def _get_input_text_area(self) -> InputTextArea:
|
|
60
|
+
return self.query_one('#input', InputTextArea)
|
|
61
|
+
|
|
62
|
+
def _get_messages_scroll(self) -> tx.VerticalScroll:
|
|
63
|
+
return self.query_one('#messages-scroll', tx.VerticalScroll)
|
|
64
|
+
|
|
65
|
+
def _get_messages_container(self) -> tx.Static:
|
|
66
|
+
return self.query_one('#messages-container', tx.Static)
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
|
|
70
|
+
def _is_messages_at_bottom(self, threshold: int = 3) -> bool:
|
|
71
|
+
return (ms := self._get_messages_scroll()).scroll_y >= (ms.max_scroll_y - threshold)
|
|
72
|
+
|
|
73
|
+
def _scroll_messages_to_bottom(self) -> None:
|
|
74
|
+
self._get_messages_scroll().scroll_end(animate=False)
|
|
75
|
+
|
|
76
|
+
def _anchor_messages(self) -> None:
|
|
77
|
+
if (ms := self._get_messages_scroll()).max_scroll_y:
|
|
78
|
+
ms.anchor()
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
|
|
82
|
+
_pending_mount_messages: list[tx.Widget] | None = None
|
|
83
|
+
|
|
84
|
+
async def _enqueue_mount_messages(self, *messages: tx.Widget) -> None:
|
|
85
|
+
if (lst := self._pending_mount_messages) is None:
|
|
86
|
+
lst = self._pending_mount_messages = []
|
|
87
|
+
|
|
88
|
+
lst.extend(messages)
|
|
89
|
+
|
|
90
|
+
_stream_ai_message: StreamAiMessage | None = None
|
|
91
|
+
|
|
92
|
+
async def _finalize_stream_ai_message(self) -> None:
|
|
93
|
+
if self._stream_ai_message is None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
await self._stream_ai_message.stop_stream()
|
|
97
|
+
self._stream_ai_message = None
|
|
98
|
+
|
|
99
|
+
async def _append_stream_ai_message_content(self, content: str) -> None:
|
|
100
|
+
if (sam := self._stream_ai_message) is not None:
|
|
101
|
+
was_at_bottom = self._is_messages_at_bottom()
|
|
102
|
+
|
|
103
|
+
await sam.append_content(content)
|
|
104
|
+
|
|
105
|
+
self.call_after_refresh(lambda: self._get_messages_container().scroll_end(animate=False))
|
|
106
|
+
|
|
107
|
+
if was_at_bottom:
|
|
108
|
+
self.call_after_refresh(self._anchor_messages)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
await self._mount_messages(StreamAiMessage(content))
|
|
112
|
+
|
|
113
|
+
async def _mount_messages(self, *messages: tx.Widget) -> None:
|
|
114
|
+
was_at_bottom = self._is_messages_at_bottom()
|
|
115
|
+
|
|
116
|
+
msg_ctr = self._get_messages_container()
|
|
117
|
+
|
|
118
|
+
for msg in [*(self._pending_mount_messages or []), *messages]:
|
|
119
|
+
if isinstance(msg, AiMessage):
|
|
120
|
+
await self._finalize_stream_ai_message()
|
|
121
|
+
|
|
122
|
+
await msg_ctr.mount(msg)
|
|
123
|
+
|
|
124
|
+
if isinstance(msg, StreamAiMessage):
|
|
125
|
+
self._stream_ai_message = check.replacing_none(self._stream_ai_message, msg)
|
|
126
|
+
await msg.write_initial_content()
|
|
127
|
+
|
|
128
|
+
self._pending_mount_messages = None
|
|
129
|
+
|
|
130
|
+
self.call_after_refresh(lambda: msg_ctr.scroll_end(animate=False))
|
|
131
|
+
|
|
132
|
+
if was_at_bottom:
|
|
133
|
+
self.call_after_refresh(self._anchor_messages)
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
|
|
137
|
+
_event_queue_task: asyncio.Task[None] | None = None
|
|
138
|
+
|
|
139
|
+
async def _event_queue_task_main(self) -> None:
|
|
140
|
+
while True:
|
|
141
|
+
ev = await self._event_queue.get()
|
|
142
|
+
if ev is None:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if isinstance(ev, AiMessagesChatEvent):
|
|
146
|
+
wx: list[tx.Widget] = []
|
|
147
|
+
|
|
148
|
+
for ai_msg in ev.chat:
|
|
149
|
+
if isinstance(ai_msg, mc.AiMessage):
|
|
150
|
+
wx.append(
|
|
151
|
+
StaticAiMessage(
|
|
152
|
+
check.isinstance(ai_msg.c, str),
|
|
153
|
+
markdown=True,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if wx:
|
|
158
|
+
await self._enqueue_mount_messages(*wx)
|
|
159
|
+
self.call_later(self._mount_messages)
|
|
160
|
+
|
|
161
|
+
elif isinstance(ev, AiDeltaChatEvent):
|
|
162
|
+
cd = check.isinstance(ev.delta, mc.ContentAiDelta)
|
|
163
|
+
cc = check.isinstance(cd.c, str)
|
|
164
|
+
self.call_later(self. _append_stream_ai_message_content, cc)
|
|
165
|
+
|
|
166
|
+
#
|
|
167
|
+
|
|
168
|
+
# def _schedule_after_refresh(self) -> None:
|
|
169
|
+
# self.call_after_refresh(self._after_refresh)
|
|
170
|
+
|
|
171
|
+
# def _after_refresh(self) -> None:
|
|
172
|
+
# self.after_repaint()
|
|
173
|
+
#
|
|
174
|
+
# self._schedule_after_refresh()
|
|
175
|
+
|
|
176
|
+
# def after_repaint(self) -> None:
|
|
177
|
+
# # from omdev.tui.textual.debug.dominfo import inspect_dom_node # noqa
|
|
178
|
+
#
|
|
179
|
+
# pass
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
|
|
183
|
+
async def on_mount(self) -> None:
|
|
184
|
+
# self._schedule_after_refresh()
|
|
185
|
+
|
|
186
|
+
check.state(self._event_queue_task is None)
|
|
187
|
+
self._event_queue_task = asyncio.create_task(self._event_queue_task_main())
|
|
188
|
+
|
|
189
|
+
await self._chat_driver.start()
|
|
190
|
+
|
|
191
|
+
self._get_input_text_area().focus()
|
|
192
|
+
|
|
193
|
+
await self._mount_messages(
|
|
194
|
+
WelcomeMessage(
|
|
195
|
+
'Hello!',
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def on_unmount(self) -> None:
|
|
200
|
+
await self._chat_driver.stop()
|
|
201
|
+
|
|
202
|
+
if (eqt := self._event_queue_task) is not None:
|
|
203
|
+
await self._event_queue.put(None)
|
|
204
|
+
await eqt
|
|
205
|
+
|
|
206
|
+
async def on_input_text_area_submitted(self, event: InputTextArea.Submitted) -> None:
|
|
207
|
+
self._get_input_text_area().clear()
|
|
208
|
+
|
|
209
|
+
await self._finalize_stream_ai_message()
|
|
210
|
+
|
|
211
|
+
await self._mount_messages(
|
|
212
|
+
UserMessage(
|
|
213
|
+
event.text,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
await self._chat_driver.send_user_messages([mc.UserMessage(event.text)])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FIXME:
|
|
3
|
+
- too lazy to lazy import guts like every other proper inject module lol >_<
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from omlish import inject as inj
|
|
8
|
+
from omlish import lang
|
|
9
|
+
|
|
10
|
+
from ...drivers.events.injection import event_callbacks
|
|
11
|
+
from ..base import ChatInterface
|
|
12
|
+
from ..configs import InterfaceConfig
|
|
13
|
+
from .app import ChatApp
|
|
14
|
+
from .app import ChatDriverEventQueue
|
|
15
|
+
from .interface import TextualChatInterface
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
with lang.auto_proxy_import(globals()):
|
|
19
|
+
from ...drivers.tools import confirmation as _tools_confirmation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def bind_textual(cfg: InterfaceConfig = InterfaceConfig()) -> inj.Elements:
|
|
26
|
+
els: list[inj.Elemental] = [
|
|
27
|
+
inj.bind(ChatInterface, to_ctor=TextualChatInterface, singleton=True),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
els.extend([
|
|
33
|
+
inj.bind(ChatApp, singleton=True),
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
|
|
38
|
+
els.extend([
|
|
39
|
+
inj.bind(ChatDriverEventQueue, to_const=asyncio.Queue()),
|
|
40
|
+
|
|
41
|
+
event_callbacks().bind_item(to_fn=inj.KwargsTarget.of(
|
|
42
|
+
lambda eq: lambda ev: eq.put(ev),
|
|
43
|
+
eq=ChatDriverEventQueue,
|
|
44
|
+
)),
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
|
|
49
|
+
if cfg.enable_tools:
|
|
50
|
+
if cfg.dangerous_no_tool_confirmation:
|
|
51
|
+
els.append(inj.bind(
|
|
52
|
+
_tools_confirmation.ToolExecutionConfirmation,
|
|
53
|
+
to_ctor=_tools_confirmation.UnsafeAlwaysAllowToolExecutionConfirmation,
|
|
54
|
+
singleton=True,
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
# els.append(inj.bind(
|
|
59
|
+
# _tools_confirmation.ToolExecutionConfirmation,
|
|
60
|
+
# to_ctor=_tools.InteractiveToolExecutionConfirmation,
|
|
61
|
+
# singleton=True,
|
|
62
|
+
# ))
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
|
|
67
|
+
return inj.as_elements(*els)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from ...driver import ChatDriver
|
|
2
1
|
from ..base import ChatInterface
|
|
3
2
|
from .app import ChatApp
|
|
4
3
|
|
|
@@ -10,12 +9,10 @@ class TextualChatInterface(ChatInterface):
|
|
|
10
9
|
def __init__(
|
|
11
10
|
self,
|
|
12
11
|
*,
|
|
13
|
-
driver: ChatDriver,
|
|
14
12
|
app: ChatApp,
|
|
15
13
|
) -> None:
|
|
16
14
|
super().__init__()
|
|
17
15
|
|
|
18
|
-
self._driver = driver
|
|
19
16
|
self._app = app
|
|
20
17
|
|
|
21
18
|
async def run(self) -> None:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import io as _io
|
|
2
|
+
|
|
3
|
+
from omlish import lang as _lang
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@_lang.cached_function
|
|
10
|
+
def read_app_css() -> str:
|
|
11
|
+
tcss_rsrcs = [
|
|
12
|
+
rsrc
|
|
13
|
+
for rsrc in _lang.get_relative_resources(globals=globals()).values()
|
|
14
|
+
if rsrc.name.endswith('.tcss')
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
out = _io.StringIO()
|
|
18
|
+
|
|
19
|
+
for i, rsrc in enumerate(tcss_rsrcs):
|
|
20
|
+
if i:
|
|
21
|
+
out.write('\n\n')
|
|
22
|
+
|
|
23
|
+
out.write(f'/*** {rsrc.name} ***/\n')
|
|
24
|
+
out.write('\n')
|
|
25
|
+
|
|
26
|
+
out.write(rsrc.read_text().strip())
|
|
27
|
+
out.write('\n')
|
|
28
|
+
|
|
29
|
+
return out.getvalue()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#input-outer {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
#input-vertical {
|
|
7
|
+
width: 100%;
|
|
8
|
+
height: auto;
|
|
9
|
+
|
|
10
|
+
margin: 0 2 1 2;
|
|
11
|
+
|
|
12
|
+
padding: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#input-vertical2 {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: auto;
|
|
18
|
+
|
|
19
|
+
border: round $foreground-muted;
|
|
20
|
+
|
|
21
|
+
padding: 0 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#input-horizontal {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#input-glyph {
|
|
30
|
+
width: auto;
|
|
31
|
+
|
|
32
|
+
padding: 0 1 0 0;
|
|
33
|
+
|
|
34
|
+
background: transparent;
|
|
35
|
+
color: $primary;
|
|
36
|
+
|
|
37
|
+
text-style: bold;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#input {
|
|
41
|
+
width: 1fr;
|
|
42
|
+
height: auto;
|
|
43
|
+
max-height: 16;
|
|
44
|
+
|
|
45
|
+
border: none;
|
|
46
|
+
|
|
47
|
+
padding: 0;
|
|
48
|
+
|
|
49
|
+
background: transparent;
|
|
50
|
+
color: $text;
|
|
51
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* Container */
|
|
2
|
+
|
|
3
|
+
#messages-scroll {
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 1fr;
|
|
6
|
+
|
|
7
|
+
padding: 0 2 0 2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#messages-container {
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: auto;
|
|
13
|
+
|
|
14
|
+
margin-top: 1;
|
|
15
|
+
margin-bottom: 0;
|
|
16
|
+
|
|
17
|
+
layout: stream;
|
|
18
|
+
text-align: left;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/* Welcome */
|
|
23
|
+
|
|
24
|
+
.welcome-message {
|
|
25
|
+
margin: 1;
|
|
26
|
+
|
|
27
|
+
border: round;
|
|
28
|
+
|
|
29
|
+
padding: 1;
|
|
30
|
+
|
|
31
|
+
text-align: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/* User */
|
|
36
|
+
|
|
37
|
+
.user-message {
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: auto;
|
|
40
|
+
|
|
41
|
+
margin-top: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.user-message-outer {
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.user-message-glyph {
|
|
50
|
+
width: auto;
|
|
51
|
+
height: auto;
|
|
52
|
+
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: $primary;
|
|
55
|
+
|
|
56
|
+
text-style: bold;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.user-message-inner {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/* Ai */
|
|
66
|
+
|
|
67
|
+
.ai-message {
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: auto;
|
|
70
|
+
|
|
71
|
+
margin-top: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ai-message-outer {
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: auto;
|
|
77
|
+
|
|
78
|
+
align: left top;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ai-message-glyph {
|
|
82
|
+
width: auto;
|
|
83
|
+
height: auto;
|
|
84
|
+
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: $primary;
|
|
87
|
+
|
|
88
|
+
text-style: bold;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ai-message-inner {
|
|
92
|
+
width: 100%;
|
|
93
|
+
height: auto;
|
|
94
|
+
|
|
95
|
+
padding: 0;
|
|
96
|
+
|
|
97
|
+
Markdown {
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: auto;
|
|
100
|
+
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omdev.tui import textual as tx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InputTextArea(tx.TextArea):
|
|
11
|
+
@dc.dataclass()
|
|
12
|
+
class Submitted(tx.Message):
|
|
13
|
+
text: str
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs: ta.Any) -> None:
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
|
|
18
|
+
async def _on_key(self, event: tx.Key) -> None:
|
|
19
|
+
if event.key == 'enter':
|
|
20
|
+
event.prevent_default()
|
|
21
|
+
event.stop()
|
|
22
|
+
|
|
23
|
+
if text := self.text.strip():
|
|
24
|
+
self.post_message(self.Submitted(text))
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
await super()._on_key(event)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputOuter(tx.Static):
|
|
31
|
+
def compose(self) -> tx.ComposeResult:
|
|
32
|
+
with tx.Vertical(id='input-vertical'):
|
|
33
|
+
with tx.Vertical(id='input-vertical2'):
|
|
34
|
+
with tx.Horizontal(id='input-horizontal'):
|
|
35
|
+
yield tx.Static('>', id='input-glyph')
|
|
36
|
+
yield InputTextArea(placeholder='...', id='input')
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omdev.tui import textual as tx
|
|
5
|
+
from omlish import lang
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Message(tx.Static, lang.Abstract):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WelcomeMessage(Message):
|
|
19
|
+
def __init__(self, content: str) -> None:
|
|
20
|
+
super().__init__(content)
|
|
21
|
+
|
|
22
|
+
self.add_class('welcome-message')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserMessage(Message):
|
|
29
|
+
def __init__(self, content: str) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
self.add_class('user-message')
|
|
33
|
+
|
|
34
|
+
self._content = content
|
|
35
|
+
|
|
36
|
+
def compose(self) -> tx.ComposeResult:
|
|
37
|
+
with tx.Horizontal(classes='user-message-outer'):
|
|
38
|
+
yield tx.Static('> ', classes='user-message-glyph')
|
|
39
|
+
with tx.Vertical(classes='user-message-inner'):
|
|
40
|
+
yield tx.Static(self._content)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AiMessage(Message, lang.Abstract):
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
self.add_class('ai-message')
|
|
51
|
+
|
|
52
|
+
def compose(self) -> tx.ComposeResult:
|
|
53
|
+
with tx.Horizontal(classes='ai-message-outer'):
|
|
54
|
+
yield tx.Static('< ', classes='ai-message-glyph')
|
|
55
|
+
with tx.Vertical(classes='ai-message-inner'):
|
|
56
|
+
yield from self._compose_content()
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def _compose_content(self) -> ta.Generator:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StaticAiMessage(AiMessage):
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
content: str,
|
|
67
|
+
*,
|
|
68
|
+
markdown: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
|
|
72
|
+
self._content = content
|
|
73
|
+
self._markdown = markdown
|
|
74
|
+
|
|
75
|
+
def _compose_content(self) -> ta.Generator:
|
|
76
|
+
if self._markdown:
|
|
77
|
+
yield tx.Markdown(self._content)
|
|
78
|
+
else:
|
|
79
|
+
yield tx.Static(self._content)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StreamAiMessage(AiMessage):
|
|
83
|
+
def __init__(self, content: str) -> None:
|
|
84
|
+
super().__init__()
|
|
85
|
+
|
|
86
|
+
self._content = content
|
|
87
|
+
|
|
88
|
+
def _compose_content(self) -> ta.Generator:
|
|
89
|
+
yield tx.Markdown('')
|
|
90
|
+
|
|
91
|
+
_stream_: tx.MarkdownStream | None = None
|
|
92
|
+
|
|
93
|
+
def _stream(self) -> tx.MarkdownStream:
|
|
94
|
+
if self._stream_ is None:
|
|
95
|
+
self._stream_ = tx.Markdown.get_stream(self.query_one(tx.Markdown))
|
|
96
|
+
return self._stream_
|
|
97
|
+
|
|
98
|
+
async def write_initial_content(self) -> None:
|
|
99
|
+
if self._content:
|
|
100
|
+
await self._stream().write(self._content)
|
|
101
|
+
|
|
102
|
+
async def append_content(self, content: str) -> None:
|
|
103
|
+
if not content:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self._content += content
|
|
107
|
+
await self._stream().write(content)
|
|
108
|
+
|
|
109
|
+
async def stop_stream(self) -> None:
|
|
110
|
+
if (stream := self._stream_) is None:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
await stream.stop()
|
|
114
|
+
self._stream_ = None
|