ommlds 0.0.0.dev490__py3-none-any.whl → 0.0.0.dev492__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.
Files changed (88) hide show
  1. ommlds/.omlish-manifests.json +9 -7
  2. ommlds/README.md +11 -0
  3. ommlds/__about__.py +1 -1
  4. ommlds/backends/ollama/_dataclasses.py +53 -23
  5. ommlds/backends/ollama/protocol.py +3 -0
  6. ommlds/cli/_dataclasses.py +439 -289
  7. ommlds/cli/main.py +42 -34
  8. ommlds/cli/rendering/types.py +6 -0
  9. ommlds/cli/sessions/chat/configs.py +2 -2
  10. ommlds/cli/sessions/chat/{agents → drivers}/ai/inject.py +3 -1
  11. ommlds/cli/sessions/chat/{agents → drivers}/configs.py +1 -1
  12. ommlds/cli/sessions/chat/{agents/agent.py → drivers/driver.py} +7 -1
  13. ommlds/cli/sessions/chat/{agents → drivers}/inject.py +13 -6
  14. ommlds/cli/sessions/chat/{agents → drivers}/tools/configs.py +0 -2
  15. ommlds/cli/sessions/chat/drivers/tools/confirmation.py +44 -0
  16. ommlds/cli/sessions/chat/{agents → drivers}/tools/execution.py +2 -3
  17. ommlds/cli/sessions/chat/{agents → drivers}/tools/inject.py +1 -13
  18. ommlds/cli/sessions/chat/{agents → drivers}/tools/rendering.py +1 -1
  19. ommlds/cli/sessions/chat/drivers/types.py +10 -0
  20. ommlds/cli/sessions/chat/{agents → drivers}/user/inject.py +5 -5
  21. ommlds/cli/sessions/chat/inject.py +2 -2
  22. ommlds/cli/sessions/chat/interfaces/bare/inject.py +23 -0
  23. ommlds/cli/sessions/chat/interfaces/bare/interactive.py +12 -6
  24. ommlds/cli/sessions/chat/interfaces/bare/oneshot.py +5 -5
  25. ommlds/cli/sessions/chat/{agents/tools/confirmation.py → interfaces/bare/tools.py} +2 -21
  26. ommlds/cli/sessions/chat/interfaces/bare/user.py +1 -1
  27. ommlds/cli/sessions/chat/interfaces/configs.py +8 -0
  28. ommlds/cli/sessions/chat/interfaces/inject.py +1 -1
  29. ommlds/cli/sessions/chat/interfaces/textual/app.py +154 -103
  30. ommlds/cli/sessions/chat/interfaces/textual/inject.py +34 -9
  31. ommlds/cli/sessions/chat/interfaces/textual/interface.py +85 -0
  32. ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
  33. ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss +3 -1
  34. ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
  35. ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +131 -9
  36. ommlds/cli/sessions/chat/interfaces/textual/tools.py +37 -0
  37. ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
  38. ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
  39. ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +164 -0
  40. ommlds/minichain/backends/impls/ollama/chat.py +50 -56
  41. ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
  42. ommlds/minichain/backends/impls/openai/names.py +3 -1
  43. ommlds/nanochat/rustbpe/README.md +9 -0
  44. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/METADATA +6 -6
  45. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/RECORD +88 -78
  46. /ommlds/cli/sessions/chat/{agents → drivers}/__init__.py +0 -0
  47. /ommlds/cli/sessions/chat/{agents → drivers}/ai/__init__.py +0 -0
  48. /ommlds/cli/sessions/chat/{agents → drivers}/ai/configs.py +0 -0
  49. /ommlds/cli/sessions/chat/{agents → drivers}/ai/events.py +0 -0
  50. /ommlds/cli/sessions/chat/{agents → drivers}/ai/injection.py +0 -0
  51. /ommlds/cli/sessions/chat/{agents → drivers}/ai/rendering.py +0 -0
  52. /ommlds/cli/sessions/chat/{agents → drivers}/ai/services.py +0 -0
  53. /ommlds/cli/sessions/chat/{agents → drivers}/ai/tools.py +0 -0
  54. /ommlds/cli/sessions/chat/{agents → drivers}/ai/types.py +0 -0
  55. /ommlds/cli/sessions/chat/{agents → drivers}/events/__init__.py +0 -0
  56. /ommlds/cli/sessions/chat/{agents → drivers}/events/inject.py +0 -0
  57. /ommlds/cli/sessions/chat/{agents → drivers}/events/injection.py +0 -0
  58. /ommlds/cli/sessions/chat/{agents → drivers}/events/manager.py +0 -0
  59. /ommlds/cli/sessions/chat/{agents → drivers}/events/types.py +0 -0
  60. /ommlds/cli/sessions/chat/{agents → drivers}/phases/__init__.py +0 -0
  61. /ommlds/cli/sessions/chat/{agents → drivers}/phases/inject.py +0 -0
  62. /ommlds/cli/sessions/chat/{agents → drivers}/phases/injection.py +0 -0
  63. /ommlds/cli/sessions/chat/{agents → drivers}/phases/manager.py +0 -0
  64. /ommlds/cli/sessions/chat/{agents → drivers}/phases/types.py +0 -0
  65. /ommlds/cli/sessions/chat/{agents → drivers}/state/__init__.py +0 -0
  66. /ommlds/cli/sessions/chat/{agents → drivers}/state/configs.py +0 -0
  67. /ommlds/cli/sessions/chat/{agents → drivers}/state/inject.py +0 -0
  68. /ommlds/cli/sessions/chat/{agents → drivers}/state/inmemory.py +0 -0
  69. /ommlds/cli/sessions/chat/{agents → drivers}/state/storage.py +0 -0
  70. /ommlds/cli/sessions/chat/{agents → drivers}/state/types.py +0 -0
  71. /ommlds/cli/sessions/chat/{agents → drivers}/tools/__init__.py +0 -0
  72. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/__init__.py +0 -0
  73. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/configs.py +0 -0
  74. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/inject.py +0 -0
  75. /ommlds/cli/sessions/chat/{agents → drivers}/tools/injection.py +0 -0
  76. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/__init__.py +0 -0
  77. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/configs.py +0 -0
  78. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/inject.py +0 -0
  79. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/__init__.py +0 -0
  80. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/configs.py +0 -0
  81. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/inject.py +0 -0
  82. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/tools.py +0 -0
  83. /ommlds/cli/sessions/chat/{agents → drivers}/user/__init__.py +0 -0
  84. /ommlds/cli/sessions/chat/{agents → drivers}/user/configs.py +0 -0
  85. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/WHEEL +0 -0
  86. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/entry_points.txt +0 -0
  87. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/licenses/LICENSE +0 -0
  88. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/top_level.txt +0 -0
@@ -1,135 +1,91 @@
1
1
  import asyncio
2
- import dataclasses as dc
3
- import io
4
2
  import typing as ta
5
3
 
6
4
  from omdev.tui import textual as tx
7
5
  from omlish import check
8
6
  from omlish import lang
7
+ from omlish.logs import all as logs
9
8
 
10
9
  from ...... import minichain as mc
11
- from ...agents.agent import ChatAgent
12
- from ...agents.events.types import AiMessagesChatEvent
10
+ from ...drivers.driver import ChatDriver
11
+ from ...drivers.events.types import AiDeltaChatEvent
12
+ from ...drivers.events.types import AiMessagesChatEvent
13
+ from .styles import read_app_css
14
+ from .widgets.input import InputOuter
15
+ from .widgets.input import InputTextArea
16
+ from .widgets.messages import AiMessage
17
+ from .widgets.messages import StaticAiMessage
18
+ from .widgets.messages import StreamAiMessage
19
+ from .widgets.messages import ToolConfirmationMessage
20
+ from .widgets.messages import UserMessage
21
+ from .widgets.messages import WelcomeMessage
13
22
 
14
23
 
15
- ##
16
-
17
-
18
- ChatAgentEventQueue = ta.NewType('ChatAgentEventQueue', asyncio.Queue)
24
+ log, alog = logs.get_module_loggers(globals())
19
25
 
20
26
 
21
27
  ##
22
28
 
23
29
 
24
- class WelcomeMessage(tx.Static):
25
- def __init__(self, content: str) -> None:
26
- super().__init__(content)
27
-
28
- self.add_class('welcome-message')
29
-
30
-
31
- class UserMessage(tx.Static):
32
- def __init__(self, content: str) -> None:
33
- super().__init__(content)
34
-
35
- self.add_class('user-message')
36
-
37
-
38
- class AiMessage(tx.Static):
39
- def __init__(self, content: str) -> None:
40
- super().__init__(content)
41
-
42
- self.add_class('ai-message')
30
+ ChatDriverEventQueue = ta.NewType('ChatDriverEventQueue', asyncio.Queue)
43
31
 
44
32
 
45
33
  ##
46
34
 
47
35
 
48
- class InputTextArea(tx.TextArea):
49
- @dc.dataclass()
50
- class Submitted(tx.Message):
51
- text: str
52
-
53
- def __init__(self, **kwargs: ta.Any) -> None:
54
- super().__init__(**kwargs)
55
-
56
- async def _on_key(self, event: tx.Key) -> None:
57
- if event.key == 'enter':
58
- event.prevent_default()
59
- event.stop()
60
-
61
- if text := self.text.strip():
62
- self.post_message(self.Submitted(text))
63
-
64
- else:
65
- await super()._on_key(event)
66
-
67
-
68
- ##
69
-
70
-
71
- @lang.cached_function
72
- def _read_app_css() -> str:
73
- tcss_rsrcs = [
74
- rsrc
75
- for rsrc in lang.get_relative_resources('.styles', globals=globals()).values()
76
- if rsrc.name.endswith('.tcss')
77
- ]
78
-
79
- out = io.StringIO()
80
-
81
- for i, rsrc in enumerate(tcss_rsrcs):
82
- if i:
83
- out.write('\n\n')
84
-
85
- out.write(f'/* {rsrc.name} */\n')
86
- out.write('\n')
87
-
88
- out.write(rsrc.read_text().strip())
89
- out.write('\n')
90
-
91
- return out.getvalue()
92
-
93
-
94
- #
36
+ class ChatAppGetter(lang.CachedFunc0['ChatApp']):
37
+ pass
95
38
 
96
39
 
97
40
  class ChatApp(tx.App):
41
+ ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
42
+
98
43
  def __init__(
99
44
  self,
100
45
  *,
101
- agent: ChatAgent,
102
- event_queue: ChatAgentEventQueue,
46
+ chat_driver: ChatDriver,
47
+ chat_driver_event_queue: ChatDriverEventQueue,
103
48
  ) -> None:
104
49
  super().__init__()
105
50
 
106
- self._agent = agent
107
- self._event_queue = event_queue
51
+ tx.setup_app_devtools(self, port=41932)
108
52
 
109
- CSS: ta.ClassVar[str] = _read_app_css()
53
+ self._chat_driver = chat_driver
54
+ self._chat_driver_event_queue = chat_driver_event_queue
110
55
 
111
- ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
56
+ self._chat_driver_action_queue: asyncio.Queue[ta.Any] = asyncio.Queue()
57
+
58
+ def get_driver_class(self) -> type[tx.Driver]:
59
+ return tx.get_pending_writes_driver_class(super().get_driver_class())
60
+
61
+ CSS: ta.ClassVar[str] = read_app_css()
112
62
 
113
63
  #
114
64
 
115
65
  def compose(self) -> tx.ComposeResult:
116
- with tx.VerticalScroll(id='messages-scroll'):
117
- yield tx.Static(id='messages-container')
66
+ yield tx.VerticalScroll(id='messages-container')
118
67
 
119
- with tx.Static(id='input-outer'):
120
- with tx.Vertical(id='input-vertical'):
121
- with tx.Vertical(id='input-vertical2'):
122
- with tx.Horizontal(id='input-horizontal'):
123
- yield tx.Static('>', id='input-glyph')
124
- yield InputTextArea(placeholder='...', id='input')
68
+ yield InputOuter(id='input-outer')
125
69
 
126
70
  #
127
71
 
128
72
  def _get_input_text_area(self) -> InputTextArea:
129
73
  return self.query_one('#input', InputTextArea)
130
74
 
131
- def _get_messages_container(self) -> tx.Static:
132
- return self.query_one('#messages-container', tx.Static)
75
+ def _get_messages_container(self) -> tx.VerticalScroll:
76
+ return self.query_one('#messages-container', tx.VerticalScroll)
77
+
78
+ #
79
+
80
+ def _is_messages_at_bottom(self, threshold: int = 3) -> bool:
81
+ return (ms := self._get_messages_container()).scroll_y >= (ms.max_scroll_y - threshold)
82
+
83
+ def _scroll_messages_to_bottom(self) -> None:
84
+ self._get_messages_container().scroll_end(animate=False)
85
+
86
+ def _anchor_messages(self) -> None:
87
+ if (ms := self._get_messages_container()).max_scroll_y:
88
+ ms.anchor()
133
89
 
134
90
  #
135
91
 
@@ -141,34 +97,73 @@ class ChatApp(tx.App):
141
97
 
142
98
  lst.extend(messages)
143
99
 
100
+ _stream_ai_message: StreamAiMessage | None = None
101
+
102
+ async def _finalize_stream_ai_message(self) -> None:
103
+ if self._stream_ai_message is None:
104
+ return
105
+
106
+ await self._stream_ai_message.stop_stream()
107
+ self._stream_ai_message = None
108
+
109
+ async def _append_stream_ai_message_content(self, content: str) -> None:
110
+ if (sam := self._stream_ai_message) is not None:
111
+ was_at_bottom = self._is_messages_at_bottom()
112
+
113
+ await sam.append_content(content)
114
+
115
+ self.call_after_refresh(self._scroll_messages_to_bottom)
116
+
117
+ if was_at_bottom:
118
+ self.call_after_refresh(self._anchor_messages)
119
+
120
+ else:
121
+ await self._mount_messages(StreamAiMessage(content))
122
+
144
123
  async def _mount_messages(self, *messages: tx.Widget) -> None:
124
+ was_at_bottom = self._is_messages_at_bottom()
125
+
145
126
  msg_ctr = self._get_messages_container()
146
127
 
147
128
  for msg in [*(self._pending_mount_messages or []), *messages]:
129
+ if isinstance(msg, (AiMessage, ToolConfirmationMessage)):
130
+ await self._finalize_stream_ai_message()
131
+
148
132
  await msg_ctr.mount(msg)
149
133
 
134
+ if isinstance(msg, StreamAiMessage):
135
+ self._stream_ai_message = check.replacing_none(self._stream_ai_message, msg)
136
+ await msg.write_initial_content()
137
+
150
138
  self._pending_mount_messages = None
151
139
 
152
- self.call_after_refresh(lambda: msg_ctr.scroll_end(animate=False))
140
+ self.call_after_refresh(self._scroll_messages_to_bottom)
141
+
142
+ if was_at_bottom:
143
+ self.call_after_refresh(self._anchor_messages)
153
144
 
154
145
  #
155
146
 
156
- _event_queue_task: asyncio.Task[None] | None = None
147
+ _chat_driver_event_task: asyncio.Task[None] | None = None
157
148
 
158
- async def _event_queue_task_main(self) -> None:
149
+ @logs.async_exception_logging(alog)
150
+ async def _chat_driver_event_task_main(self) -> None:
159
151
  while True:
160
- ev = await self._event_queue.get()
152
+ ev = await self._chat_driver_event_queue.get()
161
153
  if ev is None:
162
154
  break
163
155
 
156
+ await alog.debug(lambda: f'Got chat driver event: {ev!r}')
157
+
164
158
  if isinstance(ev, AiMessagesChatEvent):
165
159
  wx: list[tx.Widget] = []
166
160
 
167
161
  for ai_msg in ev.chat:
168
162
  if isinstance(ai_msg, mc.AiMessage):
169
163
  wx.append(
170
- AiMessage(
164
+ StaticAiMessage(
171
165
  check.isinstance(ai_msg.c, str),
166
+ markdown=True,
172
167
  ),
173
168
  )
174
169
 
@@ -176,13 +171,46 @@ class ChatApp(tx.App):
176
171
  await self._enqueue_mount_messages(*wx)
177
172
  self.call_later(self._mount_messages)
178
173
 
174
+ elif isinstance(ev, AiDeltaChatEvent):
175
+ if isinstance(ev.delta, mc.ContentAiDelta):
176
+ cc = check.isinstance(ev.delta.c, str)
177
+ self.call_later(self._append_stream_ai_message_content, cc)
178
+
179
+ elif isinstance(ev.delta, mc.ToolUseAiDelta):
180
+ pass
181
+
182
+ #
183
+
184
+ _chat_driver_action_task: asyncio.Task[None] | None = None
185
+
186
+ @logs.async_exception_logging(alog)
187
+ async def _chat_driver_action_task_main(self) -> None:
188
+ while True:
189
+ ac = await self._chat_driver_action_queue.get()
190
+ if ac is None:
191
+ break
192
+
193
+ await alog.debug(lambda: f'Got chat driver action: {ac!r}')
194
+
195
+ if isinstance(ac, mc.UserMessage):
196
+ try:
197
+ await self._chat_driver.send_user_messages([ac])
198
+ except Exception as e: # noqa
199
+ raise
200
+
201
+ else:
202
+ raise TypeError(ac) # noqa
203
+
179
204
  #
180
205
 
181
206
  async def on_mount(self) -> None:
182
- check.state(self._event_queue_task is None)
183
- self._event_queue_task = asyncio.create_task(self._event_queue_task_main())
207
+ check.state(self._chat_driver_event_task is None)
208
+ self._chat_driver_event_task = asyncio.create_task(self._chat_driver_event_task_main())
184
209
 
185
- await self._agent.start()
210
+ await self._chat_driver.start()
211
+
212
+ check.state(self._chat_driver_action_task is None)
213
+ self._chat_driver_action_task = asyncio.create_task(self._chat_driver_action_task_main())
186
214
 
187
215
  self._get_input_text_area().focus()
188
216
 
@@ -193,19 +221,42 @@ class ChatApp(tx.App):
193
221
  )
194
222
 
195
223
  async def on_unmount(self) -> None:
196
- await self._agent.stop()
224
+ if (cdt := self._chat_driver_event_task) is not None:
225
+ await self._chat_driver_event_queue.put(None)
226
+ await cdt
227
+
228
+ await self._chat_driver.stop()
197
229
 
198
- if (eqt := self._event_queue_task) is not None:
199
- await self._event_queue.put(None)
200
- await eqt
230
+ if (cet := self._chat_driver_event_task) is not None:
231
+ await self._chat_driver_event_queue.put(None)
232
+ await cet
201
233
 
234
+ @tx.on(InputTextArea.Submitted)
202
235
  async def on_input_text_area_submitted(self, event: InputTextArea.Submitted) -> None:
203
236
  self._get_input_text_area().clear()
204
237
 
238
+ await self._finalize_stream_ai_message()
239
+
205
240
  await self._mount_messages(
206
241
  UserMessage(
207
242
  event.text,
208
243
  ),
209
244
  )
210
245
 
211
- await self._agent.send_user_messages([mc.UserMessage(event.text)])
246
+ await self._chat_driver_action_queue.put(mc.UserMessage(event.text))
247
+
248
+ #
249
+
250
+ async def confirm_tool_use(self, message: str) -> bool:
251
+ fut: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
252
+
253
+ tcm = ToolConfirmationMessage(message, fut)
254
+
255
+ async def inner() -> None:
256
+ await self._mount_messages(tcm)
257
+
258
+ self.call_later(inner)
259
+
260
+ ret = await fut
261
+
262
+ return ret
@@ -5,39 +5,64 @@ FIXME:
5
5
  import asyncio
6
6
 
7
7
  from omlish import inject as inj
8
+ from omlish import lang
8
9
 
9
- from ...agents.events.injection import event_callbacks
10
+ from ...drivers.events.injection import event_callbacks
10
11
  from ..base import ChatInterface
11
- from .app import ChatAgentEventQueue
12
- from .app import ChatApp
13
- from .interface import TextualChatInterface
12
+ from ..configs import InterfaceConfig
13
+
14
+
15
+ with lang.auto_proxy_import(globals()):
16
+ from ...drivers.tools import confirmation as _tools_confirmation
17
+ from . import app as _app
18
+ from . import interface as _interface
19
+ from . import tools as _tools
14
20
 
15
21
 
16
22
  ##
17
23
 
18
24
 
19
- def bind_textual() -> inj.Elements:
25
+ def bind_textual(cfg: InterfaceConfig = InterfaceConfig()) -> inj.Elements:
20
26
  els: list[inj.Elemental] = [
21
- inj.bind(ChatInterface, to_ctor=TextualChatInterface, singleton=True),
27
+ inj.bind(ChatInterface, to_ctor=_interface.TextualChatInterface, singleton=True),
22
28
  ]
23
29
 
24
30
  #
25
31
 
26
32
  els.extend([
27
- inj.bind(ChatApp, singleton=True),
33
+ inj.bind(_app.ChatApp, singleton=True),
34
+
35
+ inj.bind_late(_app.ChatApp, _app.ChatAppGetter),
28
36
  ])
29
37
 
30
38
  #
31
39
 
32
40
  els.extend([
33
- inj.bind(ChatAgentEventQueue, to_const=asyncio.Queue()),
41
+ inj.bind(_app.ChatDriverEventQueue, to_const=asyncio.Queue()),
34
42
 
35
43
  event_callbacks().bind_item(to_fn=inj.KwargsTarget.of(
36
44
  lambda eq: lambda ev: eq.put(ev),
37
- eq=ChatAgentEventQueue,
45
+ eq=_app.ChatDriverEventQueue,
38
46
  )),
39
47
  ])
40
48
 
41
49
  #
42
50
 
51
+ if cfg.enable_tools:
52
+ if cfg.dangerous_no_tool_confirmation:
53
+ els.append(inj.bind(
54
+ _tools_confirmation.ToolExecutionConfirmation,
55
+ to_ctor=_tools_confirmation.UnsafeAlwaysAllowToolExecutionConfirmation,
56
+ singleton=True,
57
+ ))
58
+
59
+ else:
60
+ els.append(inj.bind(
61
+ _tools_confirmation.ToolExecutionConfirmation,
62
+ to_ctor=_tools.ChatAppToolExecutionConfirmation,
63
+ singleton=True,
64
+ ))
65
+
66
+ #
67
+
43
68
  return inj.as_elements(*els)
@@ -1,7 +1,90 @@
1
+ import inspect
2
+ import logging
3
+ import typing as ta
4
+
5
+ from omdev.tui import textual as tx
6
+
1
7
  from ..base import ChatInterface
2
8
  from .app import ChatApp
3
9
 
4
10
 
11
+ if ta.TYPE_CHECKING:
12
+ from textual_dev.client import DevtoolsClient
13
+
14
+
15
+ ##
16
+
17
+
18
+ def _translate_log_level(level: int) -> tuple['tx.LogGroup', 'tx.LogVerbosity']:
19
+ if level >= logging.ERROR:
20
+ return (tx.LogGroup.ERROR, tx.LogVerbosity.HIGH)
21
+ elif level >= logging.WARNING:
22
+ return (tx.LogGroup.ERROR, tx.LogVerbosity.HIGH)
23
+ elif level >= logging.INFO:
24
+ return (tx.LogGroup.INFO, tx.LogVerbosity.NORMAL)
25
+ elif level >= logging.DEBUG:
26
+ return (tx.LogGroup.DEBUG, tx.LogVerbosity.NORMAL)
27
+ else:
28
+ return (tx.LogGroup.UNDEFINED, tx.LogVerbosity.NORMAL)
29
+
30
+
31
+ class _HackLoggingHandler(logging.Handler):
32
+ """
33
+ TODO:
34
+ - reify caller from LogContextInfos
35
+ - queue worker, this blocks the asyncio thread lol
36
+ - move to omdev.tui.textual obviously
37
+ """
38
+
39
+ def __init__(self, devtools: ta.Optional['DevtoolsClient']) -> None:
40
+ super().__init__()
41
+
42
+ self._devtools = devtools
43
+
44
+ def emit(self, record: logging.LogRecord) -> None:
45
+ if (devtools := self._devtools) is not None and devtools.is_connected:
46
+ from textual_dev.client import DevtoolsLog
47
+
48
+ msg = self.format(record)
49
+
50
+ caller = inspect.Traceback(
51
+ filename=record.filename,
52
+ lineno=record.lineno,
53
+ function=record.funcName,
54
+ code_context=None,
55
+ index=None,
56
+ )
57
+
58
+ group, verbosity = _translate_log_level(record.levelno)
59
+
60
+ devtools.log(
61
+ DevtoolsLog(
62
+ msg,
63
+ caller=caller,
64
+ ),
65
+ group=group,
66
+ verbosity=verbosity,
67
+ )
68
+
69
+
70
+ def _hack_loggers(devtools: ta.Optional['DevtoolsClient']) -> None:
71
+ from omlish.logs.std.standard import _locking_logging_module_lock # noqa
72
+ from omlish.logs.std.standard import StandardConfiguredLoggingHandler
73
+
74
+ with _locking_logging_module_lock():
75
+ std_handler = next((h for h in logging.root.handlers if isinstance(h, StandardConfiguredLoggingHandler)), None)
76
+
77
+ hack_handler = _HackLoggingHandler(devtools)
78
+
79
+ if std_handler is not None:
80
+ hack_handler.setFormatter(std_handler.formatter)
81
+
82
+ for std_filter in std_handler.filters:
83
+ hack_handler.addFilter(std_filter)
84
+
85
+ logging.root.handlers = [hack_handler]
86
+
87
+
5
88
  ##
6
89
 
7
90
 
@@ -16,4 +99,6 @@ class TextualChatInterface(ChatInterface):
16
99
  self._app = app
17
100
 
18
101
  async def run(self) -> None:
102
+ _hack_loggers(self._app.devtools)
103
+
19
104
  await self._app.run_async()
@@ -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()
@@ -1,13 +1,15 @@
1
1
  #input-outer {
2
2
  width: 100%;
3
3
  height: auto;
4
+
5
+ background: $background-darken-3;
4
6
  }
5
7
 
6
8
  #input-vertical {
7
9
  width: 100%;
8
10
  height: auto;
9
11
 
10
- margin: 0 2 1 2;
12
+ margin: 0 1 1 1;
11
13
 
12
14
  padding: 0;
13
15
  }
@@ -0,0 +1,7 @@
1
+ Markdown MarkdownFence {
2
+ max-width: 95%;
3
+
4
+ overflow-x: auto;
5
+
6
+ scrollbar-size-horizontal: 1;
7
+ }