ommlds 0.0.0.dev504__py3-none-any.whl → 0.0.0.dev505__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/__about__.py +1 -1
- ommlds/cli/_dataclasses.py +170 -1
- ommlds/cli/sessions/chat/interfaces/textual/app.py +34 -0
- ommlds/cli/sessions/chat/interfaces/textual/configs.py +1 -1
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +14 -0
- ommlds/cli/sessions/chat/interfaces/textual/inputhistory.py +174 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +42 -8
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/RECORD +13 -12
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev504.dist-info → ommlds-0.0.0.dev505.dist-info}/top_level.txt +0 -0
ommlds/__about__.py
CHANGED
ommlds/cli/_dataclasses.py
CHANGED
|
@@ -1320,7 +1320,6 @@ def _process_dataclass__2eae35290b327f0d934cd6747eeb9064b6f01259():
|
|
|
1320
1320
|
),
|
|
1321
1321
|
cls_names=(
|
|
1322
1322
|
('ommlds.cli.main', 'InterfaceConfig'),
|
|
1323
|
-
('ommlds.cli.main', 'TextualInterfaceConfig'),
|
|
1324
1323
|
),
|
|
1325
1324
|
)
|
|
1326
1325
|
def _process_dataclass__d65d18393f357ae0fb02bb80268c6f1473462613():
|
|
@@ -1740,6 +1739,176 @@ def _process_dataclass__76648be4a999973a966584081092052c01632d85():
|
|
|
1740
1739
|
return _process_dataclass
|
|
1741
1740
|
|
|
1742
1741
|
|
|
1742
|
+
@_register(
|
|
1743
|
+
plan_repr=(
|
|
1744
|
+
"Plans(tup=(CopyPlan(fields=('enable_tools', 'dangerous_no_tool_confirmation', 'input_history_file')), EqPlan(f"
|
|
1745
|
+
"ields=('enable_tools', 'dangerous_no_tool_confirmation', 'input_history_file')), FrozenPlan(fields=('enable_to"
|
|
1746
|
+
"ols', 'dangerous_no_tool_confirmation', 'input_history_file'), allow_dynamic_dunder_attrs=False), HashPlan(act"
|
|
1747
|
+
"ion='add', fields=('enable_tools', 'dangerous_no_tool_confirmation', 'input_history_file'), cache=False), Init"
|
|
1748
|
+
"Plan(fields=(InitPlan.Field(name='enable_tools', annotation=OpRef(name='init.fields.0.annotation'), default=Op"
|
|
1749
|
+
"Ref(name='init.fields.0.default'), default_factory=None, init=True, override=False, field_type=FieldType.INSTA"
|
|
1750
|
+
"NCE, coerce=None, validate=None, check_type=None), InitPlan.Field(name='dangerous_no_tool_confirmation', annot"
|
|
1751
|
+
"ation=OpRef(name='init.fields.1.annotation'), default=OpRef(name='init.fields.1.default'), default_factory=Non"
|
|
1752
|
+
"e, init=True, override=False, field_type=FieldType.INSTANCE, coerce=None, validate=None, check_type=None), Ini"
|
|
1753
|
+
"tPlan.Field(name='input_history_file', annotation=OpRef(name='init.fields.2.annotation'), default=OpRef(name='"
|
|
1754
|
+
"init.fields.2.default'), default_factory=None, init=True, override=False, field_type=FieldType.INSTANCE, coerc"
|
|
1755
|
+
"e=None, validate=None, check_type=None)), self_param='self', std_params=(), kw_only_params=('enable_tools', 'd"
|
|
1756
|
+
"angerous_no_tool_confirmation', 'input_history_file'), frozen=True, slots=False, post_init_params=None, init_f"
|
|
1757
|
+
"ns=(), validate_fns=()), ReprPlan(fields=(ReprPlan.Field(name='enable_tools', kw_only=True, fn=None), ReprPlan"
|
|
1758
|
+
".Field(name='dangerous_no_tool_confirmation', kw_only=True, fn=None), ReprPlan.Field(name='input_history_file'"
|
|
1759
|
+
", kw_only=True, fn=None)), id=False, terse=False, default_fn=None)))"
|
|
1760
|
+
),
|
|
1761
|
+
plan_repr_sha1='f515764cf4b50b208c232c9355d9b2ed75cf26f4',
|
|
1762
|
+
op_ref_idents=(
|
|
1763
|
+
'__dataclass__init__fields__0__annotation',
|
|
1764
|
+
'__dataclass__init__fields__0__default',
|
|
1765
|
+
'__dataclass__init__fields__1__annotation',
|
|
1766
|
+
'__dataclass__init__fields__1__default',
|
|
1767
|
+
'__dataclass__init__fields__2__annotation',
|
|
1768
|
+
'__dataclass__init__fields__2__default',
|
|
1769
|
+
),
|
|
1770
|
+
cls_names=(
|
|
1771
|
+
('ommlds.cli.main', 'TextualInterfaceConfig'),
|
|
1772
|
+
),
|
|
1773
|
+
)
|
|
1774
|
+
def _process_dataclass__f515764cf4b50b208c232c9355d9b2ed75cf26f4():
|
|
1775
|
+
def _process_dataclass(
|
|
1776
|
+
*,
|
|
1777
|
+
__dataclass__cls,
|
|
1778
|
+
__dataclass__init__fields__0__annotation,
|
|
1779
|
+
__dataclass__init__fields__0__default,
|
|
1780
|
+
__dataclass__init__fields__1__annotation,
|
|
1781
|
+
__dataclass__init__fields__1__default,
|
|
1782
|
+
__dataclass__init__fields__2__annotation,
|
|
1783
|
+
__dataclass__init__fields__2__default,
|
|
1784
|
+
__dataclass__FieldFnValidationError, # noqa
|
|
1785
|
+
__dataclass__FieldTypeValidationError, # noqa
|
|
1786
|
+
__dataclass__FnValidationError, # noqa
|
|
1787
|
+
__dataclass__FrozenInstanceError=dataclasses.FrozenInstanceError, # noqa
|
|
1788
|
+
__dataclass__FunctionType=types.FunctionType, # noqa
|
|
1789
|
+
__dataclass__HAS_DEFAULT_FACTORY=dataclasses._HAS_DEFAULT_FACTORY, # noqa
|
|
1790
|
+
__dataclass__MISSING=dataclasses.MISSING, # noqa
|
|
1791
|
+
__dataclass__None=None, # noqa
|
|
1792
|
+
__dataclass__TypeError=TypeError, # noqa
|
|
1793
|
+
__dataclass___recursive_repr=reprlib.recursive_repr, # noqa
|
|
1794
|
+
__dataclass__isinstance=isinstance, # noqa
|
|
1795
|
+
__dataclass__object_setattr=object.__setattr__, # noqa
|
|
1796
|
+
__dataclass__property=property, # noqa
|
|
1797
|
+
):
|
|
1798
|
+
def __copy__(self):
|
|
1799
|
+
if self.__class__ is not __dataclass__cls:
|
|
1800
|
+
raise TypeError(self)
|
|
1801
|
+
return __dataclass__cls( # noqa
|
|
1802
|
+
enable_tools=self.enable_tools,
|
|
1803
|
+
dangerous_no_tool_confirmation=self.dangerous_no_tool_confirmation,
|
|
1804
|
+
input_history_file=self.input_history_file,
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
__copy__.__qualname__ = f"{__dataclass__cls.__qualname__}.__copy__"
|
|
1808
|
+
if '__copy__' in __dataclass__cls.__dict__:
|
|
1809
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __copy__ in class {__dataclass__cls.__name__}")
|
|
1810
|
+
setattr(__dataclass__cls, '__copy__', __copy__)
|
|
1811
|
+
|
|
1812
|
+
def __eq__(self, other):
|
|
1813
|
+
if self is other:
|
|
1814
|
+
return True
|
|
1815
|
+
if self.__class__ is not other.__class__:
|
|
1816
|
+
return NotImplemented
|
|
1817
|
+
return (
|
|
1818
|
+
self.enable_tools == other.enable_tools and
|
|
1819
|
+
self.dangerous_no_tool_confirmation == other.dangerous_no_tool_confirmation and
|
|
1820
|
+
self.input_history_file == other.input_history_file
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
__eq__.__qualname__ = f"{__dataclass__cls.__qualname__}.__eq__"
|
|
1824
|
+
if '__eq__' in __dataclass__cls.__dict__:
|
|
1825
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __eq__ in class {__dataclass__cls.__name__}")
|
|
1826
|
+
setattr(__dataclass__cls, '__eq__', __eq__)
|
|
1827
|
+
|
|
1828
|
+
__dataclass___setattr_frozen_fields = {
|
|
1829
|
+
'enable_tools',
|
|
1830
|
+
'dangerous_no_tool_confirmation',
|
|
1831
|
+
'input_history_file',
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
def __setattr__(self, name, value):
|
|
1835
|
+
if (
|
|
1836
|
+
type(self) is __dataclass__cls
|
|
1837
|
+
or name in __dataclass___setattr_frozen_fields
|
|
1838
|
+
):
|
|
1839
|
+
raise __dataclass__FrozenInstanceError(f"cannot assign to field {name!r}")
|
|
1840
|
+
super(__dataclass__cls, self).__setattr__(name, value)
|
|
1841
|
+
|
|
1842
|
+
__setattr__.__qualname__ = f"{__dataclass__cls.__qualname__}.__setattr__"
|
|
1843
|
+
if '__setattr__' in __dataclass__cls.__dict__:
|
|
1844
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __setattr__ in class {__dataclass__cls.__name__}")
|
|
1845
|
+
setattr(__dataclass__cls, '__setattr__', __setattr__)
|
|
1846
|
+
|
|
1847
|
+
__dataclass___delattr_frozen_fields = {
|
|
1848
|
+
'enable_tools',
|
|
1849
|
+
'dangerous_no_tool_confirmation',
|
|
1850
|
+
'input_history_file',
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
def __delattr__(self, name):
|
|
1854
|
+
if (
|
|
1855
|
+
type(self) is __dataclass__cls
|
|
1856
|
+
or name in __dataclass___delattr_frozen_fields
|
|
1857
|
+
):
|
|
1858
|
+
raise __dataclass__FrozenInstanceError(f"cannot delete field {name!r}")
|
|
1859
|
+
super(__dataclass__cls, self).__delattr__(name)
|
|
1860
|
+
|
|
1861
|
+
__delattr__.__qualname__ = f"{__dataclass__cls.__qualname__}.__delattr__"
|
|
1862
|
+
if '__delattr__' in __dataclass__cls.__dict__:
|
|
1863
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __delattr__ in class {__dataclass__cls.__name__}")
|
|
1864
|
+
setattr(__dataclass__cls, '__delattr__', __delattr__)
|
|
1865
|
+
|
|
1866
|
+
def __hash__(self):
|
|
1867
|
+
return hash((
|
|
1868
|
+
self.enable_tools,
|
|
1869
|
+
self.dangerous_no_tool_confirmation,
|
|
1870
|
+
self.input_history_file,
|
|
1871
|
+
))
|
|
1872
|
+
|
|
1873
|
+
__hash__.__qualname__ = f"{__dataclass__cls.__qualname__}.__hash__"
|
|
1874
|
+
setattr(__dataclass__cls, '__hash__', __hash__)
|
|
1875
|
+
|
|
1876
|
+
def __init__(
|
|
1877
|
+
self,
|
|
1878
|
+
*,
|
|
1879
|
+
enable_tools: __dataclass__init__fields__0__annotation = __dataclass__init__fields__0__default,
|
|
1880
|
+
dangerous_no_tool_confirmation: __dataclass__init__fields__1__annotation = __dataclass__init__fields__1__default,
|
|
1881
|
+
input_history_file: __dataclass__init__fields__2__annotation = __dataclass__init__fields__2__default,
|
|
1882
|
+
) -> __dataclass__None:
|
|
1883
|
+
__dataclass__object_setattr(self, 'enable_tools', enable_tools)
|
|
1884
|
+
__dataclass__object_setattr(self, 'dangerous_no_tool_confirmation', dangerous_no_tool_confirmation)
|
|
1885
|
+
__dataclass__object_setattr(self, 'input_history_file', input_history_file)
|
|
1886
|
+
|
|
1887
|
+
__init__.__qualname__ = f"{__dataclass__cls.__qualname__}.__init__"
|
|
1888
|
+
if '__init__' in __dataclass__cls.__dict__:
|
|
1889
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __init__ in class {__dataclass__cls.__name__}")
|
|
1890
|
+
setattr(__dataclass__cls, '__init__', __init__)
|
|
1891
|
+
|
|
1892
|
+
@__dataclass___recursive_repr()
|
|
1893
|
+
def __repr__(self):
|
|
1894
|
+
parts = []
|
|
1895
|
+
parts.append(f"enable_tools={self.enable_tools!r}")
|
|
1896
|
+
parts.append(f"dangerous_no_tool_confirmation={self.dangerous_no_tool_confirmation!r}")
|
|
1897
|
+
parts.append(f"input_history_file={self.input_history_file!r}")
|
|
1898
|
+
return (
|
|
1899
|
+
f"{self.__class__.__qualname__}("
|
|
1900
|
+
f"{', '.join(parts)}"
|
|
1901
|
+
f")"
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
__repr__.__qualname__ = f"{__dataclass__cls.__qualname__}.__repr__"
|
|
1905
|
+
if '__repr__' in __dataclass__cls.__dict__:
|
|
1906
|
+
raise __dataclass__TypeError(f"Cannot overwrite attribute __repr__ in class {__dataclass__cls.__name__}")
|
|
1907
|
+
setattr(__dataclass__cls, '__repr__', __repr__)
|
|
1908
|
+
|
|
1909
|
+
return _process_dataclass
|
|
1910
|
+
|
|
1911
|
+
|
|
1743
1912
|
@_register(
|
|
1744
1913
|
plan_repr=(
|
|
1745
1914
|
"Plans(tup=(CopyPlan(fields=('enabled_tools', 'verbose')), EqPlan(fields=('enabled_tools', 'verbose')), FrozenP"
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- textual.getters.query_one
|
|
4
|
+
- AUTO_FOCUS
|
|
5
|
+
"""
|
|
1
6
|
import asyncio
|
|
2
7
|
import os
|
|
3
8
|
import typing as ta
|
|
@@ -15,6 +20,7 @@ from ...drivers.events.types import AiDeltaChatEvent
|
|
|
15
20
|
from ...drivers.events.types import AiMessagesChatEvent
|
|
16
21
|
from ...drivers.types import ChatDriver
|
|
17
22
|
from ...facades.facade import ChatFacade
|
|
23
|
+
from .inputhistory import InputHistoryManager
|
|
18
24
|
from .styles import read_app_css
|
|
19
25
|
from .widgets.input import InputOuter
|
|
20
26
|
from .widgets.input import InputTextArea
|
|
@@ -55,6 +61,7 @@ class ChatApp(tx.App):
|
|
|
55
61
|
chat_event_queue: ChatEventQueue,
|
|
56
62
|
backend_name: BackendName | None = None,
|
|
57
63
|
devtools_setup: tx.DevtoolsSetup | None = None,
|
|
64
|
+
input_history_manager: InputHistoryManager,
|
|
58
65
|
) -> None:
|
|
59
66
|
super().__init__()
|
|
60
67
|
|
|
@@ -65,6 +72,7 @@ class ChatApp(tx.App):
|
|
|
65
72
|
self._chat_driver = chat_driver
|
|
66
73
|
self._chat_event_queue = chat_event_queue
|
|
67
74
|
self._backend_name = backend_name
|
|
75
|
+
self._input_history_manager = input_history_manager
|
|
68
76
|
|
|
69
77
|
self._chat_action_queue: asyncio.Queue[ta.Any] = asyncio.Queue()
|
|
70
78
|
|
|
@@ -263,8 +271,34 @@ class ChatApp(tx.App):
|
|
|
263
271
|
),
|
|
264
272
|
)
|
|
265
273
|
|
|
274
|
+
self._input_history_manager.add(event.text)
|
|
275
|
+
|
|
266
276
|
await self._chat_action_queue.put(ChatApp.UserInput(event.text))
|
|
267
277
|
|
|
278
|
+
def _move_input_cursor_to_end(self) -> None:
|
|
279
|
+
ita = self._get_input_text_area()
|
|
280
|
+
ln = ita.document.line_count - 1
|
|
281
|
+
lt = ita.document.lines[ln]
|
|
282
|
+
ita.move_cursor((ln, len(lt)))
|
|
283
|
+
|
|
284
|
+
@tx.on(InputTextArea.HistoryPrevious)
|
|
285
|
+
async def on_input_text_area_history_previous(self, event: InputTextArea.HistoryPrevious) -> None:
|
|
286
|
+
if (entry := self._input_history_manager.get_previous(event.text)) is not None:
|
|
287
|
+
self._get_input_text_area().text = entry
|
|
288
|
+
self._move_input_cursor_to_end()
|
|
289
|
+
|
|
290
|
+
@tx.on(InputTextArea.HistoryNext)
|
|
291
|
+
async def on_input_text_area_history_next(self, event: InputTextArea.HistoryNext) -> None:
|
|
292
|
+
if (entry := self._input_history_manager.get_next(event.text)) is not None:
|
|
293
|
+
ita = self._get_input_text_area()
|
|
294
|
+
ita.text = entry
|
|
295
|
+
self._move_input_cursor_to_end()
|
|
296
|
+
else:
|
|
297
|
+
# At the end of history, clear the input
|
|
298
|
+
ita = self._get_input_text_area()
|
|
299
|
+
ita.clear()
|
|
300
|
+
self._input_history_manager.reset_position()
|
|
301
|
+
|
|
268
302
|
@tx.on(tx.Key)
|
|
269
303
|
async def on_key(self, event: tx.Key) -> None:
|
|
270
304
|
if event in self._input_focused_key_events:
|
|
@@ -20,6 +20,7 @@ with lang.auto_proxy_import(globals()):
|
|
|
20
20
|
from ...facades import ui as _facades_ui
|
|
21
21
|
from . import app as _app
|
|
22
22
|
from . import facades as _facades
|
|
23
|
+
from . import inputhistory as _inputhistory
|
|
23
24
|
from . import interface as _interface
|
|
24
25
|
from . import tools as _tools
|
|
25
26
|
|
|
@@ -94,4 +95,17 @@ def bind_textual(cfg: TextualInterfaceConfig = TextualInterfaceConfig()) -> inj.
|
|
|
94
95
|
|
|
95
96
|
#
|
|
96
97
|
|
|
98
|
+
def _make_input_history_storage() -> _inputhistory.InputHistoryStorage:
|
|
99
|
+
if cfg.input_history_file is not None:
|
|
100
|
+
return _inputhistory.FileInputHistoryStorage(path=cfg.input_history_file)
|
|
101
|
+
else:
|
|
102
|
+
return _inputhistory.InMemoryInputHistoryStorage()
|
|
103
|
+
|
|
104
|
+
els.extend([
|
|
105
|
+
inj.bind(_inputhistory.InputHistoryStorage, to_fn=_make_input_history_storage, singleton=True),
|
|
106
|
+
inj.bind(_inputhistory.InputHistoryManager, singleton=True),
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
#
|
|
110
|
+
|
|
97
111
|
return inj.as_elements(*els)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from omlish import lang
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InputHistoryStorage(lang.Abstract):
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def load(self) -> list[str]:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InMemoryInputHistoryStorage(InputHistoryStorage):
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
self._entries: list[str] = []
|
|
27
|
+
|
|
28
|
+
def load(self) -> list[str]:
|
|
29
|
+
return list(self._entries)
|
|
30
|
+
|
|
31
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
32
|
+
self._entries = list(entries)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FileInputHistoryStorage(InputHistoryStorage):
|
|
36
|
+
def __init__(self, *, path: str) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
self._path = path
|
|
40
|
+
|
|
41
|
+
def load(self) -> list[str]:
|
|
42
|
+
if not os.path.exists(self._path):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with open(self._path) as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
if isinstance(data, list) and all(isinstance(e, str) for e in data):
|
|
49
|
+
return data
|
|
50
|
+
return []
|
|
51
|
+
except (json.JSONDecodeError, OSError):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
55
|
+
try:
|
|
56
|
+
dir_path = os.path.dirname(self._path)
|
|
57
|
+
if dir_path:
|
|
58
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
59
|
+
with open(self._path, 'w') as f:
|
|
60
|
+
json.dump(list(entries), f, indent=2)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InputHistoryManager:
|
|
69
|
+
"""
|
|
70
|
+
Manages input history with readline-style navigation.
|
|
71
|
+
|
|
72
|
+
History position semantics:
|
|
73
|
+
- Position starts at len(history) (one past the end)
|
|
74
|
+
- Moving 'previous' decrements position
|
|
75
|
+
- Moving 'next' increments position
|
|
76
|
+
- Position is clamped to [0, len(history)]
|
|
77
|
+
- Position len(history) represents 'no history item selected'
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
storage: InputHistoryStorage,
|
|
84
|
+
max_entries: int = 1000,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__()
|
|
87
|
+
|
|
88
|
+
self._storage = storage
|
|
89
|
+
self._max_entries = max_entries
|
|
90
|
+
|
|
91
|
+
self._entries: list[str] = self._storage.load()
|
|
92
|
+
self._position: int = len(self._entries)
|
|
93
|
+
self._current_draft: str = ''
|
|
94
|
+
|
|
95
|
+
def add(self, text: str) -> None:
|
|
96
|
+
"""Add a new history entry and reset position."""
|
|
97
|
+
|
|
98
|
+
if not text.strip():
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Don't add duplicate consecutive entries
|
|
102
|
+
if self._entries and self._entries[-1] == text:
|
|
103
|
+
self.reset_position()
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self._entries.append(text)
|
|
107
|
+
|
|
108
|
+
# Trim to max size
|
|
109
|
+
if len(self._entries) > self._max_entries:
|
|
110
|
+
self._entries = self._entries[-self._max_entries:]
|
|
111
|
+
|
|
112
|
+
self._storage.save(self._entries)
|
|
113
|
+
self.reset_position()
|
|
114
|
+
|
|
115
|
+
def get_previous(self, text: str | None = None) -> str | None:
|
|
116
|
+
"""
|
|
117
|
+
Navigate to previous (older) history entry.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: Current text in the input field (saved as draft when at end of history)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The previous history entry, or None if at the beginning
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if not self._entries:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Save current draft if we're at the end
|
|
130
|
+
if self._position == len(self._entries) and text is not None:
|
|
131
|
+
self._current_draft = text
|
|
132
|
+
|
|
133
|
+
# Move to previous entry
|
|
134
|
+
if self._position > 0:
|
|
135
|
+
self._position -= 1
|
|
136
|
+
return self._entries[self._position]
|
|
137
|
+
|
|
138
|
+
# Already at oldest entry
|
|
139
|
+
return self._entries[0] if self._entries else None
|
|
140
|
+
|
|
141
|
+
def get_next(self, text: str | None = None) -> str | None:
|
|
142
|
+
"""
|
|
143
|
+
Navigate to next (newer) history entry.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
text: Current text in the input field (unused for now)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The next history entry, the saved draft if moving past the end, or None
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if not self._entries:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
# Move to next entry
|
|
156
|
+
if self._position < len(self._entries):
|
|
157
|
+
self._position += 1
|
|
158
|
+
|
|
159
|
+
# If we moved past the end, return the draft
|
|
160
|
+
if self._position == len(self._entries):
|
|
161
|
+
draft = self._current_draft
|
|
162
|
+
self._current_draft = ''
|
|
163
|
+
return draft
|
|
164
|
+
|
|
165
|
+
return self._entries[self._position]
|
|
166
|
+
|
|
167
|
+
# Already at newest position
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def reset_position(self) -> None:
|
|
171
|
+
"""Reset history position to the end (no history item selected)."""
|
|
172
|
+
|
|
173
|
+
self._position = len(self._entries)
|
|
174
|
+
self._current_draft = ''
|
|
@@ -12,19 +12,53 @@ class InputTextArea(tx.TextArea):
|
|
|
12
12
|
class Submitted(tx.Message):
|
|
13
13
|
text: str
|
|
14
14
|
|
|
15
|
+
@dc.dataclass()
|
|
16
|
+
class HistoryPrevious(tx.Message):
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
@dc.dataclass()
|
|
20
|
+
class HistoryNext(tx.Message):
|
|
21
|
+
text: str
|
|
22
|
+
|
|
23
|
+
@dc.dataclass()
|
|
24
|
+
class HistoryReset(tx.Message):
|
|
25
|
+
pass
|
|
26
|
+
|
|
15
27
|
def __init__(self, **kwargs: ta.Any) -> None:
|
|
16
28
|
super().__init__(**kwargs)
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
BINDINGS: ta.ClassVar[ta.Sequence[tx.Binding]] = [ # type: ignore[assignment]
|
|
31
|
+
tx.Binding(
|
|
32
|
+
'enter',
|
|
33
|
+
'submit',
|
|
34
|
+
priority=True,
|
|
35
|
+
),
|
|
36
|
+
tx.Binding(
|
|
37
|
+
'ctrl+p',
|
|
38
|
+
'history_previous',
|
|
39
|
+
),
|
|
40
|
+
tx.Binding(
|
|
41
|
+
'ctrl+n',
|
|
42
|
+
'history_next',
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def action_submit(self) -> None:
|
|
47
|
+
if text := self.text.strip():
|
|
48
|
+
self.post_message(self.Submitted(text))
|
|
49
|
+
|
|
50
|
+
def action_cursor_up(self, select: bool = False) -> None:
|
|
51
|
+
# FIXME: if empty -> history_previous
|
|
52
|
+
super().action_cursor_up(select=select)
|
|
53
|
+
|
|
54
|
+
def action_cursor_down(self, select: bool = False) -> None:
|
|
55
|
+
super().action_cursor_down(select=select)
|
|
22
56
|
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
def action_history_previous(self) -> None:
|
|
58
|
+
self.post_message(self.HistoryPrevious(self.text))
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
def action_history_next(self) -> None:
|
|
61
|
+
self.post_message(self.HistoryNext(self.text))
|
|
28
62
|
|
|
29
63
|
|
|
30
64
|
class InputOuter(tx.Static):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ommlds
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev505
|
|
4
4
|
Summary: ommlds
|
|
5
5
|
Author: wrmsr
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -14,14 +14,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
14
14
|
Requires-Python: >=3.13
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: omlish==0.0.0.
|
|
17
|
+
Requires-Dist: omlish==0.0.0.dev505
|
|
18
18
|
Provides-Extra: all
|
|
19
|
-
Requires-Dist: omdev==0.0.0.
|
|
19
|
+
Requires-Dist: omdev==0.0.0.dev505; extra == "all"
|
|
20
20
|
Requires-Dist: llama-cpp-python~=0.3; extra == "all"
|
|
21
21
|
Requires-Dist: mlx~=0.30; sys_platform == "darwin" and extra == "all"
|
|
22
22
|
Requires-Dist: mlx-lm~=0.29; sys_platform == "darwin" and extra == "all"
|
|
23
23
|
Requires-Dist: tiktoken~=0.12; extra == "all"
|
|
24
|
-
Requires-Dist: tinygrad~=0.
|
|
24
|
+
Requires-Dist: tinygrad~=0.12; extra == "all"
|
|
25
25
|
Requires-Dist: tokenizers~=0.22; extra == "all"
|
|
26
26
|
Requires-Dist: torch~=2.9; extra == "all"
|
|
27
27
|
Requires-Dist: transformers~=4.57; extra == "all"
|
|
@@ -38,13 +38,13 @@ Requires-Dist: mwparserfromhell~=0.7; extra == "all"
|
|
|
38
38
|
Requires-Dist: wikitextparser~=0.56; extra == "all"
|
|
39
39
|
Requires-Dist: lxml>=5.3; python_version < "3.13" and extra == "all"
|
|
40
40
|
Provides-Extra: omdev
|
|
41
|
-
Requires-Dist: omdev==0.0.0.
|
|
41
|
+
Requires-Dist: omdev==0.0.0.dev505; extra == "omdev"
|
|
42
42
|
Provides-Extra: backends
|
|
43
43
|
Requires-Dist: llama-cpp-python~=0.3; extra == "backends"
|
|
44
44
|
Requires-Dist: mlx~=0.30; sys_platform == "darwin" and extra == "backends"
|
|
45
45
|
Requires-Dist: mlx-lm~=0.29; sys_platform == "darwin" and extra == "backends"
|
|
46
46
|
Requires-Dist: tiktoken~=0.12; extra == "backends"
|
|
47
|
-
Requires-Dist: tinygrad~=0.
|
|
47
|
+
Requires-Dist: tinygrad~=0.12; extra == "backends"
|
|
48
48
|
Requires-Dist: tokenizers~=0.22; extra == "backends"
|
|
49
49
|
Requires-Dist: torch~=2.9; extra == "backends"
|
|
50
50
|
Requires-Dist: transformers~=4.57; extra == "backends"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
ommlds/.omlish-manifests.json,sha256=8fbwe2p3vu-qOXuBIKtwadocgl-rNP6H42qRRR_j9mY,28359
|
|
2
2
|
ommlds/README.md,sha256=xhbl2n19GznXrIzAGdlX8PAYJYsOo_Zu63I7G1UFRZE,398
|
|
3
|
-
ommlds/__about__.py,sha256=
|
|
3
|
+
ommlds/__about__.py,sha256=hz4YAxWP4zYOs_hxzZNoGRo1L2mm-6-CM0wj5bcXXbc,1901
|
|
4
4
|
ommlds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
ommlds/_hacks/__init__.py,sha256=ajfw7dMKH8UuloeQ5MSxWwgAmdWf2v8gm-K3uLP9wtY,196
|
|
6
6
|
ommlds/_hacks/funcs.py,sha256=8XseIblP7yolDUD7WQSGn1LP90IQzByVejSzphAPDyM,2861
|
|
@@ -102,7 +102,7 @@ ommlds/backends/transformers/filecache.py,sha256=ycfswt7f4qRrPSTFRhofXZaDBuDPpyp
|
|
|
102
102
|
ommlds/backends/transformers/streamers.py,sha256=Hu_9lp_kUilKjOfs7Ixqr2NoA5FuRn2eRh8JdvaBDYc,1688
|
|
103
103
|
ommlds/cli/__init__.py,sha256=-RtLrdEGN2da1KCf7YNs32jN-kJhT_kNVrcOv4x_J-w,101
|
|
104
104
|
ommlds/cli/__main__.py,sha256=1ffCb0fcUOJMzxROJmJRXQ8PSOVYv7KrcuBtT95cf0c,140
|
|
105
|
-
ommlds/cli/_dataclasses.py,sha256=
|
|
105
|
+
ommlds/cli/_dataclasses.py,sha256=ewUQSDXLkba0isAcUPxInyVBPfyfWIdHR5gg0mSeCWg,173523
|
|
106
106
|
ommlds/cli/asyncs.py,sha256=NAMzzaZq7ORjlbbBB_Y9vcM9qoBpGf4VJNtl_3p_8G4,629
|
|
107
107
|
ommlds/cli/inject.py,sha256=Bt-T-vQIbp8vh6q21Tt3wDztFZ9FPxNhH2ocrAfjArE,815
|
|
108
108
|
ommlds/cli/main.py,sha256=n_13EimSCt2x9wkn4tULk9gNOvuCHuHeDcuEYtrcUgQ,11172
|
|
@@ -210,10 +210,11 @@ ommlds/cli/sessions/chat/interfaces/bare/interactive.py,sha256=ZnYoePvXtUbhkDQ0j
|
|
|
210
210
|
ommlds/cli/sessions/chat/interfaces/bare/oneshot.py,sha256=b758OIa0gf9I_0UdxYJ6re-g8-8xndgr3R0OotUOsmc,387
|
|
211
211
|
ommlds/cli/sessions/chat/interfaces/bare/tools.py,sha256=_UsuoXLIvfpFP_We5DBBlhm6rwB3_cFA3lmFvpG9b-A,824
|
|
212
212
|
ommlds/cli/sessions/chat/interfaces/textual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
213
|
-
ommlds/cli/sessions/chat/interfaces/textual/app.py,sha256=
|
|
214
|
-
ommlds/cli/sessions/chat/interfaces/textual/configs.py,sha256
|
|
213
|
+
ommlds/cli/sessions/chat/interfaces/textual/app.py,sha256=NOYQPJH3IOT6mQRpjhmAWdG4V_VcMjHFivCXmYrlthE,10468
|
|
214
|
+
ommlds/cli/sessions/chat/interfaces/textual/configs.py,sha256=-pvG2_Uai70ohDfK4Tt8yaHnvdCs10_gaoQkr-CsOqA,213
|
|
215
215
|
ommlds/cli/sessions/chat/interfaces/textual/facades.py,sha256=zXVG7DKVl-Xtdc893O_yktHCMvM0do6hLesMd8hbqeo,411
|
|
216
|
-
ommlds/cli/sessions/chat/interfaces/textual/inject.py,sha256=
|
|
216
|
+
ommlds/cli/sessions/chat/interfaces/textual/inject.py,sha256=eBhFVZ2VmQdoTPSZvi2OSkZ-fX8Mw2TKo28bHZeACJY,3056
|
|
217
|
+
ommlds/cli/sessions/chat/interfaces/textual/inputhistory.py,sha256=Jdmwd6RTBLK8rGyz4w560cwuK6LXi_OTubVfw_-ORTM,4687
|
|
217
218
|
ommlds/cli/sessions/chat/interfaces/textual/interface.py,sha256=lHeuiMtA7DW9knuapZEOZSl9-9SmOfUxiPnd4-plLHE,445
|
|
218
219
|
ommlds/cli/sessions/chat/interfaces/textual/tools.py,sha256=KVlUmIyzqUuOHMzB9ZXGUaGsb-Tp5LAmMpB1agAHYjo,985
|
|
219
220
|
ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py,sha256=7_U5oUjwegOymeWgt6nLpFfWfjGTrlWL8m4Au8MsaFE,542
|
|
@@ -221,7 +222,7 @@ ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss,sha256=9LLwsuYjY8y
|
|
|
221
222
|
ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss,sha256=KCKlgt_EEjHVZSTwP_w2YMrR6mURLYVryOwOziAFlTw,105
|
|
222
223
|
ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss,sha256=QBpHJkBT76OkT8srUwhZOt0ZT5ZCzXuM4l8P9hP9ALw,1665
|
|
223
224
|
ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
224
|
-
ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py,sha256=
|
|
225
|
+
ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py,sha256=MMONcOi3Aq8ZzmvJn6tHNPbfV3EOol25fABxtW1h--E,1827
|
|
225
226
|
ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py,sha256=hZcentzdznaE_YPId1KsW4k8mAqsSu5zwMdTA5tIj5s,5472
|
|
226
227
|
ommlds/cli/sessions/completion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
227
228
|
ommlds/cli/sessions/completion/configs.py,sha256=jOtadH46Esx6CztbQazAgIx9Tt5uuu4pGPov3G3O5Ac,286
|
|
@@ -515,9 +516,9 @@ ommlds/wiki/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
515
516
|
ommlds/wiki/utils/io.py,sha256=UKgDJGtmpnWvIqVd2mJc2QNPOqlToEY1GEveNp6_pMo,7088
|
|
516
517
|
ommlds/wiki/utils/progress.py,sha256=EhvKcMFYtsarCQhIahlO6f0SboyAKP3UwUyrnVnP-Vk,3222
|
|
517
518
|
ommlds/wiki/utils/xml.py,sha256=sNJNkZ9rT8B-kJMO6bRz8J1USy4fyPx0m2PwTX7vxYY,3846
|
|
518
|
-
ommlds-0.0.0.
|
|
519
|
-
ommlds-0.0.0.
|
|
520
|
-
ommlds-0.0.0.
|
|
521
|
-
ommlds-0.0.0.
|
|
522
|
-
ommlds-0.0.0.
|
|
523
|
-
ommlds-0.0.0.
|
|
519
|
+
ommlds-0.0.0.dev505.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
|
520
|
+
ommlds-0.0.0.dev505.dist-info/METADATA,sha256=u0AJNyhefq8XfxNXBnnAFCVhZfdrGc9CWrDDy8k1aU0,3495
|
|
521
|
+
ommlds-0.0.0.dev505.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
522
|
+
ommlds-0.0.0.dev505.dist-info/entry_points.txt,sha256=Z5YWtX7ClfiCKdW-dd_CSVvM0h4yQpJPi-2G3q6gNFo,35
|
|
523
|
+
ommlds-0.0.0.dev505.dist-info/top_level.txt,sha256=Rbnk5d5wi58vnAXx13WFZqdQ4VX8hBCS2hEL3WeXOhY,7
|
|
524
|
+
ommlds-0.0.0.dev505.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|