hitmos 0.0.1__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.
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - name: Read version
20
+ id: ver
21
+ run: |
22
+ VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
23
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
24
+
25
+ - name: Check if tag already exists
26
+ id: tag
27
+ run: |
28
+ if git rev-parse "v${{ steps.ver.outputs.version }}" >/dev/null 2>&1; then
29
+ echo "exists=true" >> "$GITHUB_OUTPUT"
30
+ echo "Tag v${{ steps.ver.outputs.version }} already exists — skipping release."
31
+ else
32
+ echo "exists=false" >> "$GITHUB_OUTPUT"
33
+ fi
34
+
35
+ - uses: actions/setup-python@v5
36
+ if: steps.tag.outputs.exists == 'false'
37
+ with:
38
+ python-version: "3.13"
39
+
40
+ - name: Build
41
+ if: steps.tag.outputs.exists == 'false'
42
+ run: |
43
+ pip install build
44
+ python -m build
45
+
46
+ - name: Publish to PyPI
47
+ if: steps.tag.outputs.exists == 'false'
48
+ uses: pypa/gh-action-pypi-publish@release/v1
49
+ with:
50
+ password: ${{ secrets.PYPI_TOKEN }}
51
+
52
+ - name: Create tag and GitHub release
53
+ if: steps.tag.outputs.exists == 'false'
54
+ env:
55
+ GH_TOKEN: ${{ github.token }}
56
+ run: |
57
+ git config user.name "github-actions[bot]"
58
+ git config user.email "github-actions[bot]@users.noreply.github.com"
59
+ git tag -a "v${{ steps.ver.outputs.version }}" -m "Release v${{ steps.ver.outputs.version }}"
60
+ git push origin "v${{ steps.ver.outputs.version }}"
61
+ gh release create "v${{ steps.ver.outputs.version }}" dist/* \
62
+ --title "v${{ steps.ver.outputs.version }}" \
63
+ --generate-notes
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,25 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: "Hitmos"
5
+ abstract: "AI terminal assistant powered by OpenRouter — minimal, fast, developer UX."
6
+ authors:
7
+ - family-names: "NEFOR"
8
+ given-names: "White"
9
+ email: n7for8572@gmail.com
10
+ repository-code: "https://github.com/ndugram/hitmos"
11
+ url: "https://github.com/ndugram/hitmos"
12
+ license: MIT
13
+ version: "0.1.0"
14
+ date-released: "2026-06-06"
15
+ keywords:
16
+ - ai
17
+ - cli
18
+ - assistant
19
+ - openrouter
20
+ - llm
21
+ - terminal
22
+ - chat
23
+ - deepseek
24
+ - developer-tools
25
+ - python
hitmos-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: hitmos
3
+ Version: 0.0.1
4
+ Summary: AI terminal assistant powered by OpenRouter — minimal, fast, developer UX
5
+ Project-URL: Homepage, https://github.com/ndugram/hitmos
6
+ Project-URL: Documentation, https://github.com/ndugram/hitmos
7
+ Project-URL: Repository, https://github.com/ndugram/hitmos
8
+ Project-URL: Issues, https://github.com/ndugram/hitmos/issues
9
+ Author-email: White NEFOR <n7for8572@gmail.com>
10
+ Maintainer-email: White NEFOR <n7for8572@gmail.com>
11
+ License: MIT
12
+ Keywords: ai,assistant,chat,cli,deepseek,developer-tools,llm,openrouter,python,terminal
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Terminals
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.13
26
+ Requires-Dist: fasthttp-client>=1.3.13
27
+ Requires-Dist: orjson>=3.9.0
28
+ Requires-Dist: pydantic-settings>=2.0.0
29
+ Requires-Dist: questionary>=2.1.1
30
+ Requires-Dist: rich>=15.0.0
31
+ Requires-Dist: typer>=0.26.7
32
+ Description-Content-Type: text/markdown
33
+
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/ndugram/hitmos/master/docs/logo.png" style="background:white; padding:12px; border-radius:10px; width:300">
36
+ </p>
37
+ <p align="center">
38
+ <em>AI terminal assistant powered by OpenRouter — minimal, fast, developer UX.</em>
39
+ </p>
40
+ <p align="center">
41
+ <a href="https://pypi.org/project/hitmos" target="_blank">
42
+ <img src="https://img.shields.io/pypi/v/hitmos?color=%2300d7af&label=pypi%20package" alt="Package version">
43
+ </a>
44
+ <a href="https://pypi.org/project/hitmos" target="_blank">
45
+ <img src="https://img.shields.io/pypi/pyversions/hitmos.svg?color=%2300d7af" alt="Supported Python versions">
46
+ </a>
47
+ <a href="https://pypi.org/project/hitmos" target="_blank">
48
+ <img src="https://img.shields.io/pypi/dm/hitmos?color=%2300d7af&label=downloads" alt="Monthly downloads">
49
+ </a>
50
+ <a href="https://pepy.tech/projects/hitmos" target="_blank">
51
+ <img src="https://img.shields.io/pepy/dt/hitmos?color=%2300d7af&label=total%20downloads" alt="Total downloads">
52
+ </a>
53
+ <a href="https://github.com/ndugram/hitmos" target="_blank">
54
+ <img src="https://img.shields.io/github/stars/ndugram/hitmos?style=social" alt="GitHub Stars">
55
+ </a>
56
+ </p>
57
+
58
+ ---
59
+
60
+ **Source Code**: <a href="https://github.com/ndugram/hitmos" target="_blank">https://github.com/ndugram/hitmos</a>
61
+
62
+ ---
63
+
64
+ Hitmos is a modern **AI terminal assistant** for developers, powered by <a href="https://openrouter.ai" target="_blank">OpenRouter</a>. It brings a Claude Code–styled UX to your terminal — live markdown streaming, interactive model switching, project context injection, and Tab completion, all with zero latency overhead.
65
+
66
+ Key features:
67
+
68
+ - **Streaming** — responses stream token-by-token with live Markdown rendering via <a href="https://github.com/Textualize/rich" target="_blank">rich</a>.
69
+ - **Multi-model** — switch between DeepSeek, Gemini, Claude, GPT-4o, Llama, and more with an interactive arrow-key picker.
70
+ - **Project context** — automatically injects your project files into the system prompt so the model can answer project-specific questions.
71
+ - **Tab completion** — `/model <Tab>` lists all available models; `/` completes all commands.
72
+ - **Minimal UX** — Claude Code–inspired welcome panel, `◆` status lines, clean `>` prompt.
73
+ - **Fast** — built on <a href="https://github.com/ndugram/fasthttp" target="_blank">fasthttp-client</a> with full async SSE streaming.
74
+ - **Configurable** — API key via env (`OPEN_TOKEN`, `OPENROUTER_API_KEY`) or `~/.hitmos/config.toml`.
75
+
76
+ ## Requirements
77
+
78
+ Python 3.13+
79
+
80
+ Hitmos depends on:
81
+
82
+ - <a href="https://github.com/ndugram/fasthttp" target="_blank"><code>fasthttp-client</code></a> — async HTTP transport with SSE streaming.
83
+ - <a href="https://github.com/Textualize/rich" target="_blank"><code>rich</code></a> — live Markdown rendering and styled console output.
84
+ - <a href="https://typer.tiangolo.com/" target="_blank"><code>typer</code></a> — CLI interface.
85
+ - <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> — fast JSON parsing.
86
+ - <a href="https://github.com/prompt-toolkit/python-prompt-toolkit" target="_blank"><code>prompt-toolkit</code></a> — async input with Tab completion (bundled with questionary).
87
+ - <a href="https://github.com/tmbo/questionary" target="_blank"><code>questionary</code></a> — interactive model picker.
88
+ - <a href="https://docs.pydantic.dev/" target="_blank"><code>pydantic-settings</code></a> — config management.
89
+
90
+ ## Installation
91
+
92
+ ```console
93
+ $ pip install hitmos
94
+
95
+ ---> 100%
96
+ ```
97
+
98
+ ## Quickstart
99
+
100
+ ### Save your API key
101
+
102
+ ```console
103
+ $ hitmos login
104
+ ```
105
+
106
+ Or set an environment variable:
107
+
108
+ ```console
109
+ $ export OPEN_TOKEN=sk-or-...
110
+ ```
111
+
112
+ ### Start chatting
113
+
114
+ ```console
115
+ $ hitmos
116
+ ```
117
+
118
+ You will see:
119
+
120
+ ```
121
+ ╭─────────────────────────────────────────────────────╮
122
+ │ │
123
+ │ ✻ Welcome to Hitmos │
124
+ │ │
125
+ │ /help for commands │
126
+ │ │
127
+ ╰─────────────────────────────────────────────────────╯
128
+
129
+ ◆ ~/my-project
130
+ ◆ deepseek/deepseek-chat
131
+ ◆ context 12 KB
132
+
133
+ >
134
+ ```
135
+
136
+ Ask anything about your project:
137
+
138
+ ```
139
+ > what does this project do?
140
+ > add error handling to client.py
141
+ > explain the streaming logic
142
+ ```
143
+
144
+ ## Commands
145
+
146
+ | Command | Description |
147
+ |---|---|
148
+ | `/help` | Show available commands |
149
+ | `/model` | Interactive model picker (↑↓ + Enter) |
150
+ | `/model <name>` | Switch model directly |
151
+ | `/clear` | Clear conversation history |
152
+ | `/reset` | Reset context |
153
+ | `/exit` | Exit Hitmos |
154
+
155
+ ### Switch model interactively
156
+
157
+ Type `/model` and press Enter to open the arrow-key picker:
158
+
159
+ ```
160
+ ? Select model (↑↓ navigate, Enter confirm, Ctrl+C cancel)
161
+ ❯ deepseek/deepseek-chat DeepSeek Chat · fast, cheap, great for code
162
+ deepseek/deepseek-r1 DeepSeek R1 · reasoning model
163
+ google/gemini-2.5-flash Gemini 2.5 Flash · fast multimodal
164
+ google/gemini-2.5-pro Gemini 2.5 Pro · powerful multimodal
165
+ anthropic/claude-3.5-haiku Claude 3.5 Haiku · fast Anthropic
166
+ anthropic/claude-sonnet-4-5 Claude Sonnet 4.5 · balanced Anthropic
167
+ openai/gpt-4o-mini GPT-4o Mini · OpenAI fast
168
+ openai/o4-mini o4-mini · OpenAI reasoning
169
+ meta-llama/llama-3.3-70b-instruct Llama 3.3 70B · open source
170
+ mistralai/mistral-small-3.2-24b-instruct Mistral Small 3.2 · lightweight
171
+ ```
172
+
173
+ Or use Tab completion: type `/model deep` then press `Tab`.
174
+
175
+ ## Configuration
176
+
177
+ Hitmos resolves the API key in this order:
178
+
179
+ 1. `OPEN_TOKEN` environment variable
180
+ 2. `OPENROUTER_API_KEY` environment variable
181
+ 3. `~/.hitmos/config.toml`
182
+
183
+ Config file format:
184
+
185
+ ```toml
186
+ token = "sk-or-..."
187
+ model = "deepseek/deepseek-chat"
188
+ ```
189
+
190
+ ## Contributing
191
+
192
+ Contributions are welcome! Please open an issue or pull request on <a href="https://github.com/ndugram/hitmos" target="_blank">GitHub</a>.
193
+
194
+ ## License
195
+
196
+ This project is licensed under the terms of the <a href="https://github.com/ndugram/hitmos/blob/master/LICENSE" target="_blank">MIT license</a>.
hitmos-0.0.1/README.md ADDED
@@ -0,0 +1,163 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ndugram/hitmos/master/docs/logo.png" style="background:white; padding:12px; border-radius:10px; width:300">
3
+ </p>
4
+ <p align="center">
5
+ <em>AI terminal assistant powered by OpenRouter — minimal, fast, developer UX.</em>
6
+ </p>
7
+ <p align="center">
8
+ <a href="https://pypi.org/project/hitmos" target="_blank">
9
+ <img src="https://img.shields.io/pypi/v/hitmos?color=%2300d7af&label=pypi%20package" alt="Package version">
10
+ </a>
11
+ <a href="https://pypi.org/project/hitmos" target="_blank">
12
+ <img src="https://img.shields.io/pypi/pyversions/hitmos.svg?color=%2300d7af" alt="Supported Python versions">
13
+ </a>
14
+ <a href="https://pypi.org/project/hitmos" target="_blank">
15
+ <img src="https://img.shields.io/pypi/dm/hitmos?color=%2300d7af&label=downloads" alt="Monthly downloads">
16
+ </a>
17
+ <a href="https://pepy.tech/projects/hitmos" target="_blank">
18
+ <img src="https://img.shields.io/pepy/dt/hitmos?color=%2300d7af&label=total%20downloads" alt="Total downloads">
19
+ </a>
20
+ <a href="https://github.com/ndugram/hitmos" target="_blank">
21
+ <img src="https://img.shields.io/github/stars/ndugram/hitmos?style=social" alt="GitHub Stars">
22
+ </a>
23
+ </p>
24
+
25
+ ---
26
+
27
+ **Source Code**: <a href="https://github.com/ndugram/hitmos" target="_blank">https://github.com/ndugram/hitmos</a>
28
+
29
+ ---
30
+
31
+ Hitmos is a modern **AI terminal assistant** for developers, powered by <a href="https://openrouter.ai" target="_blank">OpenRouter</a>. It brings a Claude Code–styled UX to your terminal — live markdown streaming, interactive model switching, project context injection, and Tab completion, all with zero latency overhead.
32
+
33
+ Key features:
34
+
35
+ - **Streaming** — responses stream token-by-token with live Markdown rendering via <a href="https://github.com/Textualize/rich" target="_blank">rich</a>.
36
+ - **Multi-model** — switch between DeepSeek, Gemini, Claude, GPT-4o, Llama, and more with an interactive arrow-key picker.
37
+ - **Project context** — automatically injects your project files into the system prompt so the model can answer project-specific questions.
38
+ - **Tab completion** — `/model <Tab>` lists all available models; `/` completes all commands.
39
+ - **Minimal UX** — Claude Code–inspired welcome panel, `◆` status lines, clean `>` prompt.
40
+ - **Fast** — built on <a href="https://github.com/ndugram/fasthttp" target="_blank">fasthttp-client</a> with full async SSE streaming.
41
+ - **Configurable** — API key via env (`OPEN_TOKEN`, `OPENROUTER_API_KEY`) or `~/.hitmos/config.toml`.
42
+
43
+ ## Requirements
44
+
45
+ Python 3.13+
46
+
47
+ Hitmos depends on:
48
+
49
+ - <a href="https://github.com/ndugram/fasthttp" target="_blank"><code>fasthttp-client</code></a> — async HTTP transport with SSE streaming.
50
+ - <a href="https://github.com/Textualize/rich" target="_blank"><code>rich</code></a> — live Markdown rendering and styled console output.
51
+ - <a href="https://typer.tiangolo.com/" target="_blank"><code>typer</code></a> — CLI interface.
52
+ - <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> — fast JSON parsing.
53
+ - <a href="https://github.com/prompt-toolkit/python-prompt-toolkit" target="_blank"><code>prompt-toolkit</code></a> — async input with Tab completion (bundled with questionary).
54
+ - <a href="https://github.com/tmbo/questionary" target="_blank"><code>questionary</code></a> — interactive model picker.
55
+ - <a href="https://docs.pydantic.dev/" target="_blank"><code>pydantic-settings</code></a> — config management.
56
+
57
+ ## Installation
58
+
59
+ ```console
60
+ $ pip install hitmos
61
+
62
+ ---> 100%
63
+ ```
64
+
65
+ ## Quickstart
66
+
67
+ ### Save your API key
68
+
69
+ ```console
70
+ $ hitmos login
71
+ ```
72
+
73
+ Or set an environment variable:
74
+
75
+ ```console
76
+ $ export OPEN_TOKEN=sk-or-...
77
+ ```
78
+
79
+ ### Start chatting
80
+
81
+ ```console
82
+ $ hitmos
83
+ ```
84
+
85
+ You will see:
86
+
87
+ ```
88
+ ╭─────────────────────────────────────────────────────╮
89
+ │ │
90
+ │ ✻ Welcome to Hitmos │
91
+ │ │
92
+ │ /help for commands │
93
+ │ │
94
+ ╰─────────────────────────────────────────────────────╯
95
+
96
+ ◆ ~/my-project
97
+ ◆ deepseek/deepseek-chat
98
+ ◆ context 12 KB
99
+
100
+ >
101
+ ```
102
+
103
+ Ask anything about your project:
104
+
105
+ ```
106
+ > what does this project do?
107
+ > add error handling to client.py
108
+ > explain the streaming logic
109
+ ```
110
+
111
+ ## Commands
112
+
113
+ | Command | Description |
114
+ |---|---|
115
+ | `/help` | Show available commands |
116
+ | `/model` | Interactive model picker (↑↓ + Enter) |
117
+ | `/model <name>` | Switch model directly |
118
+ | `/clear` | Clear conversation history |
119
+ | `/reset` | Reset context |
120
+ | `/exit` | Exit Hitmos |
121
+
122
+ ### Switch model interactively
123
+
124
+ Type `/model` and press Enter to open the arrow-key picker:
125
+
126
+ ```
127
+ ? Select model (↑↓ navigate, Enter confirm, Ctrl+C cancel)
128
+ ❯ deepseek/deepseek-chat DeepSeek Chat · fast, cheap, great for code
129
+ deepseek/deepseek-r1 DeepSeek R1 · reasoning model
130
+ google/gemini-2.5-flash Gemini 2.5 Flash · fast multimodal
131
+ google/gemini-2.5-pro Gemini 2.5 Pro · powerful multimodal
132
+ anthropic/claude-3.5-haiku Claude 3.5 Haiku · fast Anthropic
133
+ anthropic/claude-sonnet-4-5 Claude Sonnet 4.5 · balanced Anthropic
134
+ openai/gpt-4o-mini GPT-4o Mini · OpenAI fast
135
+ openai/o4-mini o4-mini · OpenAI reasoning
136
+ meta-llama/llama-3.3-70b-instruct Llama 3.3 70B · open source
137
+ mistralai/mistral-small-3.2-24b-instruct Mistral Small 3.2 · lightweight
138
+ ```
139
+
140
+ Or use Tab completion: type `/model deep` then press `Tab`.
141
+
142
+ ## Configuration
143
+
144
+ Hitmos resolves the API key in this order:
145
+
146
+ 1. `OPEN_TOKEN` environment variable
147
+ 2. `OPENROUTER_API_KEY` environment variable
148
+ 3. `~/.hitmos/config.toml`
149
+
150
+ Config file format:
151
+
152
+ ```toml
153
+ token = "sk-or-..."
154
+ model = "deepseek/deepseek-chat"
155
+ ```
156
+
157
+ ## Contributing
158
+
159
+ Contributions are welcome! Please open an issue or pull request on <a href="https://github.com/ndugram/hitmos" target="_blank">GitHub</a>.
160
+
161
+ ## License
162
+
163
+ This project is licensed under the terms of the <a href="https://github.com/ndugram/hitmos/blob/master/LICENSE" target="_blank">MIT license</a>.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,163 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator
3
+ from pathlib import Path
4
+
5
+ from .chat import ChatSession
6
+ from .client import OpenRouterClient, ToolCallRequest
7
+ from .commands import CommandHandler, CommandResult, CommandType
8
+ from .config import ConfigManager
9
+ from .constants import SYSTEM_PROMPT
10
+ from .context import ProjectContext
11
+ from .exceptions import AuthError, HitmosError
12
+ from .methods import dispatch
13
+ from .ui import ConsoleUI
14
+
15
+
16
+ class HitmosApp:
17
+ def __init__(self) -> None:
18
+ self._config = ConfigManager()
19
+ self._ui = ConsoleUI()
20
+ self._commands = CommandHandler()
21
+ self._session: ChatSession | None = None
22
+ self._client: OpenRouterClient | None = None
23
+
24
+ def run(self) -> None:
25
+ try:
26
+ token = self._config.resolve_token()
27
+ except AuthError as e:
28
+ self._ui.show_error(str(e))
29
+ raise SystemExit(1)
30
+
31
+ model = self._config.get_model()
32
+ self._client = OpenRouterClient(token, model)
33
+
34
+ system_prompt, ctx_kb = self._build_system_prompt()
35
+ self._session = ChatSession(system_prompt=system_prompt)
36
+
37
+ self._ui.show_welcome(model, ctx_kb)
38
+
39
+ try:
40
+ asyncio.run(self._loop())
41
+ except KeyboardInterrupt:
42
+ self._ui.show_exit()
43
+
44
+ async def _loop(self) -> None:
45
+ assert self._client is not None
46
+ assert self._session is not None
47
+
48
+ while True:
49
+ try:
50
+ text = await self._ui.get_input()
51
+ except (KeyboardInterrupt, EOFError):
52
+ self._ui.show_exit()
53
+ return
54
+
55
+ text = text.strip()
56
+ if not text:
57
+ continue
58
+
59
+ result = self._commands.parse(text)
60
+ if result is not None:
61
+ should_exit = await self._handle_command(result)
62
+ if should_exit:
63
+ return
64
+ else:
65
+ await self._handle_message(text)
66
+
67
+ def _build_system_prompt(self) -> tuple[str, int]:
68
+ ctx, kb = ProjectContext(Path.cwd()).build()
69
+ if not ctx:
70
+ return SYSTEM_PROMPT, 0
71
+ return (
72
+ "You are Hitmos, a helpful AI coding assistant. "
73
+ "Be concise, accurate, and developer-focused.\n\n"
74
+ "You have access to the user's project files below. "
75
+ "Use this context to give accurate, project-specific answers.\n\n"
76
+ + ctx,
77
+ kb,
78
+ )
79
+
80
+ async def _handle_command(self, result: CommandResult) -> bool:
81
+ assert self._client is not None
82
+ assert self._session is not None
83
+
84
+ match result.type:
85
+ case CommandType.HELP:
86
+ self._ui.show_help()
87
+ case CommandType.CLEAR:
88
+ self._session.clear()
89
+ self._ui.show_info("History cleared.")
90
+ case CommandType.RESET:
91
+ self._session.reset()
92
+ self._ui.show_info("Context reset.")
93
+ case CommandType.MODEL:
94
+ if result.arg:
95
+ self._client.model = result.arg
96
+ self._config.save_model(result.arg)
97
+ self._ui.show_success(f"Model: {result.arg}")
98
+ else:
99
+ selected = await self._ui.show_model_picker(self._client.model)
100
+ if selected:
101
+ self._client.model = selected
102
+ self._config.save_model(selected)
103
+ self._ui.show_success(f"Model: {selected}")
104
+ case CommandType.EXIT:
105
+ self._ui.show_exit()
106
+ return True
107
+ return False
108
+
109
+ async def _handle_message(self, text: str) -> None:
110
+ assert self._client is not None
111
+ assert self._session is not None
112
+
113
+ self._session.add_user(text)
114
+
115
+ try:
116
+ while True:
117
+ tool_calls: list[ToolCallRequest] = []
118
+
119
+ async def _text_gen() -> AsyncGenerator[str, None]:
120
+ async for item in self._client.stream_chat( # type: ignore[union-attr]
121
+ self._session.messages # type: ignore[union-attr]
122
+ ):
123
+ if isinstance(item, list):
124
+ tool_calls.extend(item)
125
+ else:
126
+ yield item
127
+
128
+ full_response = await self._ui.stream_response(_text_gen())
129
+
130
+ if not tool_calls:
131
+ if full_response:
132
+ self._session.add_assistant(full_response)
133
+ break
134
+
135
+ self._session.add_raw({
136
+ "role": "assistant",
137
+ "content": full_response or None,
138
+ "tool_calls": [
139
+ {
140
+ "id": tc.id,
141
+ "type": "function",
142
+ "function": {"name": tc.name, "arguments": tc.arguments},
143
+ }
144
+ for tc in tool_calls
145
+ ],
146
+ })
147
+
148
+ for tc in tool_calls:
149
+ self._ui.show_tool_call(tc.name, tc.arguments)
150
+ result = dispatch(tc.name, tc.arguments)
151
+ self._ui.show_tool_result(result)
152
+ self._session.add_raw({
153
+ "role": "tool",
154
+ "tool_call_id": tc.id,
155
+ "content": result,
156
+ })
157
+
158
+ except HitmosError as e:
159
+ self._session.pop_last()
160
+ self._ui.show_error(str(e))
161
+ except Exception as e:
162
+ self._session.pop_last()
163
+ self._ui.show_error(f"Unexpected error: {e}")
@@ -0,0 +1,34 @@
1
+ from .constants import SYSTEM_PROMPT
2
+
3
+
4
+ class ChatSession:
5
+ def __init__(self, system_prompt: str | None = None) -> None:
6
+ self._messages: list[dict] = []
7
+ self._system = system_prompt or SYSTEM_PROMPT
8
+
9
+ def add_user(self, content: str) -> None:
10
+ self._messages.append({"role": "user", "content": content})
11
+
12
+ def add_assistant(self, content: str) -> None:
13
+ self._messages.append({"role": "assistant", "content": content})
14
+
15
+ def add_raw(self, message: dict) -> None:
16
+ self._messages.append(message)
17
+
18
+ def clear(self) -> None:
19
+ self._messages.clear()
20
+
21
+ def reset(self) -> None:
22
+ self._messages.clear()
23
+
24
+ def pop_last(self) -> None:
25
+ if self._messages:
26
+ self._messages.pop()
27
+
28
+ @property
29
+ def messages(self) -> list[dict]:
30
+ return [{"role": "system", "content": self._system}] + self._messages
31
+
32
+ @property
33
+ def is_empty(self) -> bool:
34
+ return len(self._messages) == 0