citrus-code 0.1.0__tar.gz → 0.1.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.
- {citrus_code-0.1.0 → citrus_code-0.1.1}/PKG-INFO +41 -6
- {citrus_code-0.1.0 → citrus_code-0.1.1}/README.md +40 -5
- {citrus_code-0.1.0 → citrus_code-0.1.1}/pyproject.toml +1 -1
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/events.py +14 -1
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/chat_completions.py +28 -4
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/deepseek.py +8 -10
- citrus_code-0.1.1/src/citrus_code/ai/retry.py +42 -0
- citrus_code-0.1.1/src/citrus_code/cli/commands.py +59 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/main.py +3 -2
- citrus_code-0.1.1/src/citrus_code/cli/output.py +41 -0
- citrus_code-0.1.1/src/citrus_code/cli/preview.py +122 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/prompts.py +7 -1
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/render.py +94 -50
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/terminal.py +71 -33
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/theme.py +11 -7
- citrus_code-0.1.1/src/citrus_code/cli/welcome.py +57 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/__init__.py +26 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/edit.py +10 -9
- citrus_code-0.1.1/src/citrus_code/coding/tools/factory.py +75 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/find.py +9 -27
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/grep.py +18 -60
- citrus_code-0.1.1/src/citrus_code/coding/tools/local_files.py +31 -0
- citrus_code-0.1.1/src/citrus_code/coding/tools/local_search.py +110 -0
- citrus_code-0.1.1/src/citrus_code/coding/tools/local_shell.py +109 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/ls.py +9 -9
- citrus_code-0.1.1/src/citrus_code/coding/tools/operations.py +110 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/read.py +8 -6
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/shell.py +23 -92
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/write.py +11 -13
- citrus_code-0.1.0/src/citrus_code/cli/preview.py +0 -55
- citrus_code-0.1.0/src/citrus_code/coding/tools/factory.py +0 -57
- {citrus_code-0.1.0 → citrus_code-0.1.1}/LICENSE +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/__main__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/agent.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/cancellation.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/context.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/events.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/hooks.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/loop.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/message_queue.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/state.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/tool_runner.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/agent/types.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/completion.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/provider.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/chat_stream_types.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/fake.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/providers/tool_call_buffer.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/registry.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/ai/types.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/cli/session_files.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/coding_session.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/compaction/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/compaction/controller.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/compaction/summarize.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/compaction/tokens.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/context_hooks.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/history.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/project_context.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/__init__.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/manager.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/projection.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/recovery.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/storage.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/session/types.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/bash.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/edit_text.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/mutation.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/paths.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/powershell.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/processes.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/coding/tools/truncate.py +0 -0
- {citrus_code-0.1.0 → citrus_code-0.1.1}/src/citrus_code/config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: citrus-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A small Python coding agent inspired by pi, built as a learning project.
|
|
5
5
|
Keywords: coding-agent,agent,cli,learning,deepseek
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,6 +20,10 @@ Project-URL: Issues, https://github.com/big-orange947/citrus-code/issues
|
|
|
20
20
|
Project-URL: Documentation, https://github.com/big-orange947/citrus-code#readme
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="https://raw.githubusercontent.com/big-orange947/citrus-code/main/assets/citrus-banner.svg" alt="Citrus Code:绿叶像素橘子,参考 pi 的 Python coding agent" width="960">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
23
27
|
# Citrus Code
|
|
24
28
|
|
|
25
29
|
**参考 pi 的核心设计,带初学者用 Python 一步步实现一个精简的 coding agent。**
|
|
@@ -49,6 +53,16 @@ Description-Content-Type: text/markdown
|
|
|
49
53
|
|
|
50
54
|
本项目是学习实现,并非 pi 的官方 Python 移植版,也不承诺与其接口和功能完全一致。
|
|
51
55
|
|
|
56
|
+
## 终端界面与项目标识
|
|
57
|
+
|
|
58
|
+
Citrus Code 使用自己的绿叶像素橘子作为项目标识,README 和终端欢迎页采用同一角色与配色。
|
|
59
|
+
可复用的透明背景图标位于 [assets/citrus-logo.svg](https://github.com/big-orange947/citrus-code/blob/main/assets/citrus-logo.svg),页首横幅位于
|
|
60
|
+
[assets/citrus-banner.svg](https://github.com/big-orange947/citrus-code/blob/main/assets/citrus-banner.svg)。终端角色直接由字符块绘制,运行时无需加载图片。
|
|
61
|
+
|
|
62
|
+
CLI 提供居中的像素欢迎页、随输入内容增高的输入栏、闪烁块状光标,以及输入 `/` 时展开的命令列表。
|
|
63
|
+
任务运行中显示旋转指示,助手回复支持 Markdown 和代码高亮,工具执行结果以卡片呈现。
|
|
64
|
+
欢迎页使用独立终端屏幕,首次发送任务或命令后返回普通终端对话;最近的缩放重绘调整仍待实际交互确认。
|
|
65
|
+
|
|
52
66
|
## 当前进度
|
|
53
67
|
|
|
54
68
|
| 模块 | 当前能力 |
|
|
@@ -56,7 +70,7 @@ Description-Content-Type: text/markdown
|
|
|
56
70
|
| 消息类型 | 系统、用户、助手、工具结果消息;文字块、图片块与工具调用块 |
|
|
57
71
|
| Provider 接口 | 统一模型信息和异步事件流,提供注册与查找 |
|
|
58
72
|
| FakeProvider | 使用预设文字模拟流式响应,便于理解异步生成器 |
|
|
59
|
-
| DeepSeek 接入 | HTTP 请求、SSE 读取、非思考模式、Flash
|
|
73
|
+
| DeepSeek 接入 | HTTP 请求、SSE 读取、非思考模式、Flash 图片输入、临时错误退避重试、用量统计和错误事件 |
|
|
60
74
|
| 工具调用解析 | 按索引拼接多个工具调用的参数,转换成统一工具事件 |
|
|
61
75
|
| 生成截断处理 | 默认输出上限 32,768 tokens;回复被截断时不执行该回复中的工具调用 |
|
|
62
76
|
| 完整回答接口 | `complete()` 消费事件流并返回最终助手消息 |
|
|
@@ -66,22 +80,25 @@ Description-Content-Type: text/markdown
|
|
|
66
80
|
| coding 工具 | read、write、edit、bash、powershell、grep、find、ls |
|
|
67
81
|
| 输出控制 | read/命令输出按 2000 行或 50KB 截断;搜索工具提供数量和字节限制 |
|
|
68
82
|
| 工具运行器 | 按名称查找工具,将执行结果或异常转换成工具结果消息 |
|
|
83
|
+
| 工具执行操作 | 文件、命令和搜索后端可替换;默认本机执行,尚未接入沙箱 |
|
|
69
84
|
| 会话存储 | JSONL 追加保存、加载校验、消息树与分支选择、当前分支上下文重建 |
|
|
70
85
|
| CodingSession | 按事件保存消息、安全切换分支、取消与排队消息入口 |
|
|
71
86
|
| CLI | 交互与单次任务、流式输出、运行中追加消息、会话恢复、末尾缺失工具结果修复 |
|
|
72
|
-
| 终端展示 |
|
|
87
|
+
| 终端展示 | 原创像素块柑橘标识、简洁欢迎面板、横线输入栏与块状光标、命令提示列表、运行动画、Markdown 流式预览和工具结果卡片 |
|
|
73
88
|
| 项目指令 | 启动时读取工作目录下的 AGENTS.md,通过模型前 Hook 组装当前请求 |
|
|
74
89
|
| 上下文压缩 | 估计达到 600,000 tokens 时生成摘要,保留当前要求及近期完整交互,保存压缩记录 |
|
|
75
90
|
|
|
76
91
|
工具名称、主要参数和默认组合参考 pi。实现细节、系统程序依赖、图片能力和取消处理的边界见 [工具说明](https://github.com/big-orange947/citrus-code/blob/main/docs/tools.md)。
|
|
77
92
|
|
|
93
|
+
可替换执行操作的接口与使用方式见 [工具后端说明](https://github.com/big-orange947/citrus-code/blob/main/docs/tool-operations.md),从 `0.1.1` 起提供。
|
|
94
|
+
|
|
78
95
|
会话存储的接口、文件格式和恢复边界见 [会话说明](https://github.com/big-orange947/citrus-code/blob/main/docs/sessions.md)。带自动保存的任务通过 `CodingSession.prompt()` 运行。命令入口见 [CLI 使用说明](https://github.com/big-orange947/citrus-code/blob/main/docs/cli.md)。
|
|
79
96
|
|
|
80
97
|
自动压缩策略与失败边界见 [上下文压缩说明](https://github.com/big-orange947/citrus-code/blob/main/docs/compaction.md)。当前固定假设模型支持 1,000,000 tokens;估计达到 60% 时压缩。摘要调用会消耗模型额度。
|
|
81
98
|
|
|
82
99
|
## 快速开始
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
要体验仓库中的最新功能,可以从源码安装,需要 Python 3.12 或更新版本,以及 [uv](https://docs.astral.sh/uv/)。
|
|
85
102
|
先克隆仓库并安装锁定的依赖:
|
|
86
103
|
|
|
87
104
|
```powershell
|
|
@@ -106,7 +123,7 @@ uv run citrus --cwd "D:\project\your-project"
|
|
|
106
123
|
|
|
107
124
|
## 安装发行版
|
|
108
125
|
|
|
109
|
-
需要 Python 3.12
|
|
126
|
+
需要 Python 3.12 或更新版本。以下命令从 PyPI 安装最新发行版。`0.1.1` 加入了新版界面、模型请求自动重试和可替换工具后端。安装包名为 `citrus-code`,启动命令为 `citrus`。
|
|
110
127
|
|
|
111
128
|
使用 uv 安装:
|
|
112
129
|
|
|
@@ -123,6 +140,16 @@ pipx install citrus-code
|
|
|
123
140
|
python -m pip install citrus-code
|
|
124
141
|
```
|
|
125
142
|
|
|
143
|
+
已有安装可按自己的安装方式升级:
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
uv tool upgrade citrus-code
|
|
147
|
+
# 或
|
|
148
|
+
pipx upgrade citrus-code
|
|
149
|
+
# 或
|
|
150
|
+
python -m pip install --upgrade citrus-code
|
|
151
|
+
```
|
|
152
|
+
|
|
126
153
|
在 PowerShell 中配置自己的模型密钥并启动:
|
|
127
154
|
|
|
128
155
|
```powershell
|
|
@@ -157,6 +184,8 @@ uv run citrus --resume
|
|
|
157
184
|
|
|
158
185
|
输入框中 Enter 发送,Alt+Enter 或 Ctrl+J 换行,Tab 补全命令。助手回答支持 Markdown;较长的工具结果默认只展示预览,使用 `--verbose` 启动可查看完整工具返回。升级依赖前先退出正在运行的旧 CLI,避免 Windows 锁住启动程序。
|
|
159
186
|
|
|
187
|
+
输入 `/` 自动展开带说明的命令列表。输入区使用上下横线和闪烁块状光标,运行时持续显示旋转指示。模型临时断流、超时和部分 HTTP 错误最多自动重试 3 次,默认等待 2、4、8 秒;失败尝试的临时回复清除,已完成的工具结果保留,重试等待可用 Ctrl+C 取消。
|
|
188
|
+
|
|
160
189
|
| 操作 | 命令或按键 |
|
|
161
190
|
| --- | --- |
|
|
162
191
|
| 查看帮助 | `/help` |
|
|
@@ -219,6 +248,7 @@ citrus --cwd "D:\project\poirot" --resume latest --max-output-tokens 65536
|
|
|
219
248
|
|
|
220
249
|
```text
|
|
221
250
|
citrus-code/
|
|
251
|
+
├── assets/ # 像素橘子图标与 README 品牌横幅
|
|
222
252
|
├── src/citrus_code/
|
|
223
253
|
│ ├── config.py # 从环境变量加载连接配置
|
|
224
254
|
│ ├── __main__.py # python -m citrus_code 入口
|
|
@@ -263,6 +293,8 @@ citrus-code/
|
|
|
263
293
|
│ │ └── recovery.py # 为缺失结果补充执行状态未知记录
|
|
264
294
|
│ └── tools/ # 具体 coding 工具
|
|
265
295
|
│ ├── factory.py # 默认、只读和全部工具组合
|
|
296
|
+
│ ├── operations.py # 文件、命令与搜索的接口
|
|
297
|
+
│ ├── local_files.py / local_shell.py / local_search.py
|
|
266
298
|
│ ├── read.py / write.py
|
|
267
299
|
│ ├── edit.py / edit_text.py
|
|
268
300
|
│ ├── bash.py / powershell.py / shell.py
|
|
@@ -375,8 +407,9 @@ Windows 上如需使用 PowerShell,可添加 `--shell powershell`。上述示
|
|
|
375
407
|
| 10 | `coding/tools/` | `coding_tools.py`、`coding_agent.py` | pi 风格工具、精确修改和输出控制 |
|
|
376
408
|
| 11 | `coding/session/` | `session_storage.py` | 消息树、追加存储、加载与分支恢复 |
|
|
377
409
|
| 12 | `coding/coding_session.py`、`coding/history.py`、`cli/` | `uv run citrus --fake` | 按事件保存、取消、消息队列与终端交互 |
|
|
378
|
-
| 13 | `coding/project_context.py`、`coding/context_hooks.py`、`cli/prompts.py` | CLI
|
|
410
|
+
| 13 | `coding/project_context.py`、`coding/context_hooks.py`、`cli/prompts.py` | 在工作目录中添加 AGENTS.md 后启动 CLI | 项目约定、系统提示组装与历史消息的区别 |
|
|
379
411
|
| 14 | `coding/compaction/`、`coding/session/projection.py`、`agent/context.py` | 见上下文压缩说明 | 安全边界、完整工具交互、摘要与原始历史的区别 |
|
|
412
|
+
| 15 | `coding/tools/operations.py`、`local_*.py`、`factory.py` | `tool_operations.py` | 工具定义与执行环境分离、Protocol 和可替换实现 |
|
|
380
413
|
|
|
381
414
|
上表中的源码路径以 `src/citrus_code/` 为基准,示例路径以 `examples/` 为基准。早期原型位于 `examples/prototype/`,仅保留作学习对照,不作为当前版本入口。
|
|
382
415
|
|
|
@@ -389,6 +422,8 @@ Windows 上如需使用 PowerShell,可添加 `--shell powershell`。上述示
|
|
|
389
422
|
- [x] Hooks:模型调用前后、工具执行前后的扩展点。
|
|
390
423
|
- [x] 写入、精确编辑、搜索和命令执行工具。
|
|
391
424
|
- [x] 工具输出截断与命令超时处理。
|
|
425
|
+
- [x] 文件、命令和搜索的可替换执行操作接口。
|
|
426
|
+
- [ ] 容器或其他隔离环境的执行适配器。
|
|
392
427
|
- [x] Agent 主动取消与嵌套事件流关闭。
|
|
393
428
|
- [ ] 工具增量事件。
|
|
394
429
|
- [x] 第一版 CLI 交互与终端流式展示代码。
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/big-orange947/citrus-code/main/assets/citrus-banner.svg" alt="Citrus Code:绿叶像素橘子,参考 pi 的 Python coding agent" width="960">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Citrus Code
|
|
2
6
|
|
|
3
7
|
**参考 pi 的核心设计,带初学者用 Python 一步步实现一个精简的 coding agent。**
|
|
@@ -27,6 +31,16 @@
|
|
|
27
31
|
|
|
28
32
|
本项目是学习实现,并非 pi 的官方 Python 移植版,也不承诺与其接口和功能完全一致。
|
|
29
33
|
|
|
34
|
+
## 终端界面与项目标识
|
|
35
|
+
|
|
36
|
+
Citrus Code 使用自己的绿叶像素橘子作为项目标识,README 和终端欢迎页采用同一角色与配色。
|
|
37
|
+
可复用的透明背景图标位于 [assets/citrus-logo.svg](https://github.com/big-orange947/citrus-code/blob/main/assets/citrus-logo.svg),页首横幅位于
|
|
38
|
+
[assets/citrus-banner.svg](https://github.com/big-orange947/citrus-code/blob/main/assets/citrus-banner.svg)。终端角色直接由字符块绘制,运行时无需加载图片。
|
|
39
|
+
|
|
40
|
+
CLI 提供居中的像素欢迎页、随输入内容增高的输入栏、闪烁块状光标,以及输入 `/` 时展开的命令列表。
|
|
41
|
+
任务运行中显示旋转指示,助手回复支持 Markdown 和代码高亮,工具执行结果以卡片呈现。
|
|
42
|
+
欢迎页使用独立终端屏幕,首次发送任务或命令后返回普通终端对话;最近的缩放重绘调整仍待实际交互确认。
|
|
43
|
+
|
|
30
44
|
## 当前进度
|
|
31
45
|
|
|
32
46
|
| 模块 | 当前能力 |
|
|
@@ -34,7 +48,7 @@
|
|
|
34
48
|
| 消息类型 | 系统、用户、助手、工具结果消息;文字块、图片块与工具调用块 |
|
|
35
49
|
| Provider 接口 | 统一模型信息和异步事件流,提供注册与查找 |
|
|
36
50
|
| FakeProvider | 使用预设文字模拟流式响应,便于理解异步生成器 |
|
|
37
|
-
| DeepSeek 接入 | HTTP 请求、SSE 读取、非思考模式、Flash
|
|
51
|
+
| DeepSeek 接入 | HTTP 请求、SSE 读取、非思考模式、Flash 图片输入、临时错误退避重试、用量统计和错误事件 |
|
|
38
52
|
| 工具调用解析 | 按索引拼接多个工具调用的参数,转换成统一工具事件 |
|
|
39
53
|
| 生成截断处理 | 默认输出上限 32,768 tokens;回复被截断时不执行该回复中的工具调用 |
|
|
40
54
|
| 完整回答接口 | `complete()` 消费事件流并返回最终助手消息 |
|
|
@@ -44,22 +58,25 @@
|
|
|
44
58
|
| coding 工具 | read、write、edit、bash、powershell、grep、find、ls |
|
|
45
59
|
| 输出控制 | read/命令输出按 2000 行或 50KB 截断;搜索工具提供数量和字节限制 |
|
|
46
60
|
| 工具运行器 | 按名称查找工具,将执行结果或异常转换成工具结果消息 |
|
|
61
|
+
| 工具执行操作 | 文件、命令和搜索后端可替换;默认本机执行,尚未接入沙箱 |
|
|
47
62
|
| 会话存储 | JSONL 追加保存、加载校验、消息树与分支选择、当前分支上下文重建 |
|
|
48
63
|
| CodingSession | 按事件保存消息、安全切换分支、取消与排队消息入口 |
|
|
49
64
|
| CLI | 交互与单次任务、流式输出、运行中追加消息、会话恢复、末尾缺失工具结果修复 |
|
|
50
|
-
| 终端展示 |
|
|
65
|
+
| 终端展示 | 原创像素块柑橘标识、简洁欢迎面板、横线输入栏与块状光标、命令提示列表、运行动画、Markdown 流式预览和工具结果卡片 |
|
|
51
66
|
| 项目指令 | 启动时读取工作目录下的 AGENTS.md,通过模型前 Hook 组装当前请求 |
|
|
52
67
|
| 上下文压缩 | 估计达到 600,000 tokens 时生成摘要,保留当前要求及近期完整交互,保存压缩记录 |
|
|
53
68
|
|
|
54
69
|
工具名称、主要参数和默认组合参考 pi。实现细节、系统程序依赖、图片能力和取消处理的边界见 [工具说明](https://github.com/big-orange947/citrus-code/blob/main/docs/tools.md)。
|
|
55
70
|
|
|
71
|
+
可替换执行操作的接口与使用方式见 [工具后端说明](https://github.com/big-orange947/citrus-code/blob/main/docs/tool-operations.md),从 `0.1.1` 起提供。
|
|
72
|
+
|
|
56
73
|
会话存储的接口、文件格式和恢复边界见 [会话说明](https://github.com/big-orange947/citrus-code/blob/main/docs/sessions.md)。带自动保存的任务通过 `CodingSession.prompt()` 运行。命令入口见 [CLI 使用说明](https://github.com/big-orange947/citrus-code/blob/main/docs/cli.md)。
|
|
57
74
|
|
|
58
75
|
自动压缩策略与失败边界见 [上下文压缩说明](https://github.com/big-orange947/citrus-code/blob/main/docs/compaction.md)。当前固定假设模型支持 1,000,000 tokens;估计达到 60% 时压缩。摘要调用会消耗模型额度。
|
|
59
76
|
|
|
60
77
|
## 快速开始
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
要体验仓库中的最新功能,可以从源码安装,需要 Python 3.12 或更新版本,以及 [uv](https://docs.astral.sh/uv/)。
|
|
63
80
|
先克隆仓库并安装锁定的依赖:
|
|
64
81
|
|
|
65
82
|
```powershell
|
|
@@ -84,7 +101,7 @@ uv run citrus --cwd "D:\project\your-project"
|
|
|
84
101
|
|
|
85
102
|
## 安装发行版
|
|
86
103
|
|
|
87
|
-
需要 Python 3.12
|
|
104
|
+
需要 Python 3.12 或更新版本。以下命令从 PyPI 安装最新发行版。`0.1.1` 加入了新版界面、模型请求自动重试和可替换工具后端。安装包名为 `citrus-code`,启动命令为 `citrus`。
|
|
88
105
|
|
|
89
106
|
使用 uv 安装:
|
|
90
107
|
|
|
@@ -101,6 +118,16 @@ pipx install citrus-code
|
|
|
101
118
|
python -m pip install citrus-code
|
|
102
119
|
```
|
|
103
120
|
|
|
121
|
+
已有安装可按自己的安装方式升级:
|
|
122
|
+
|
|
123
|
+
```powershell
|
|
124
|
+
uv tool upgrade citrus-code
|
|
125
|
+
# 或
|
|
126
|
+
pipx upgrade citrus-code
|
|
127
|
+
# 或
|
|
128
|
+
python -m pip install --upgrade citrus-code
|
|
129
|
+
```
|
|
130
|
+
|
|
104
131
|
在 PowerShell 中配置自己的模型密钥并启动:
|
|
105
132
|
|
|
106
133
|
```powershell
|
|
@@ -135,6 +162,8 @@ uv run citrus --resume
|
|
|
135
162
|
|
|
136
163
|
输入框中 Enter 发送,Alt+Enter 或 Ctrl+J 换行,Tab 补全命令。助手回答支持 Markdown;较长的工具结果默认只展示预览,使用 `--verbose` 启动可查看完整工具返回。升级依赖前先退出正在运行的旧 CLI,避免 Windows 锁住启动程序。
|
|
137
164
|
|
|
165
|
+
输入 `/` 自动展开带说明的命令列表。输入区使用上下横线和闪烁块状光标,运行时持续显示旋转指示。模型临时断流、超时和部分 HTTP 错误最多自动重试 3 次,默认等待 2、4、8 秒;失败尝试的临时回复清除,已完成的工具结果保留,重试等待可用 Ctrl+C 取消。
|
|
166
|
+
|
|
138
167
|
| 操作 | 命令或按键 |
|
|
139
168
|
| --- | --- |
|
|
140
169
|
| 查看帮助 | `/help` |
|
|
@@ -197,6 +226,7 @@ citrus --cwd "D:\project\poirot" --resume latest --max-output-tokens 65536
|
|
|
197
226
|
|
|
198
227
|
```text
|
|
199
228
|
citrus-code/
|
|
229
|
+
├── assets/ # 像素橘子图标与 README 品牌横幅
|
|
200
230
|
├── src/citrus_code/
|
|
201
231
|
│ ├── config.py # 从环境变量加载连接配置
|
|
202
232
|
│ ├── __main__.py # python -m citrus_code 入口
|
|
@@ -241,6 +271,8 @@ citrus-code/
|
|
|
241
271
|
│ │ └── recovery.py # 为缺失结果补充执行状态未知记录
|
|
242
272
|
│ └── tools/ # 具体 coding 工具
|
|
243
273
|
│ ├── factory.py # 默认、只读和全部工具组合
|
|
274
|
+
│ ├── operations.py # 文件、命令与搜索的接口
|
|
275
|
+
│ ├── local_files.py / local_shell.py / local_search.py
|
|
244
276
|
│ ├── read.py / write.py
|
|
245
277
|
│ ├── edit.py / edit_text.py
|
|
246
278
|
│ ├── bash.py / powershell.py / shell.py
|
|
@@ -353,8 +385,9 @@ Windows 上如需使用 PowerShell,可添加 `--shell powershell`。上述示
|
|
|
353
385
|
| 10 | `coding/tools/` | `coding_tools.py`、`coding_agent.py` | pi 风格工具、精确修改和输出控制 |
|
|
354
386
|
| 11 | `coding/session/` | `session_storage.py` | 消息树、追加存储、加载与分支恢复 |
|
|
355
387
|
| 12 | `coding/coding_session.py`、`coding/history.py`、`cli/` | `uv run citrus --fake` | 按事件保存、取消、消息队列与终端交互 |
|
|
356
|
-
| 13 | `coding/project_context.py`、`coding/context_hooks.py`、`cli/prompts.py` | CLI
|
|
388
|
+
| 13 | `coding/project_context.py`、`coding/context_hooks.py`、`cli/prompts.py` | 在工作目录中添加 AGENTS.md 后启动 CLI | 项目约定、系统提示组装与历史消息的区别 |
|
|
357
389
|
| 14 | `coding/compaction/`、`coding/session/projection.py`、`agent/context.py` | 见上下文压缩说明 | 安全边界、完整工具交互、摘要与原始历史的区别 |
|
|
390
|
+
| 15 | `coding/tools/operations.py`、`local_*.py`、`factory.py` | `tool_operations.py` | 工具定义与执行环境分离、Protocol 和可替换实现 |
|
|
358
391
|
|
|
359
392
|
上表中的源码路径以 `src/citrus_code/` 为基准,示例路径以 `examples/` 为基准。早期原型位于 `examples/prototype/`,仅保留作学习对照,不作为当前版本入口。
|
|
360
393
|
|
|
@@ -367,6 +400,8 @@ Windows 上如需使用 PowerShell,可添加 `--shell powershell`。上述示
|
|
|
367
400
|
- [x] Hooks:模型调用前后、工具执行前后的扩展点。
|
|
368
401
|
- [x] 写入、精确编辑、搜索和命令执行工具。
|
|
369
402
|
- [x] 工具输出截断与命令超时处理。
|
|
403
|
+
- [x] 文件、命令和搜索的可替换执行操作接口。
|
|
404
|
+
- [ ] 容器或其他隔离环境的执行适配器。
|
|
370
405
|
- [x] Agent 主动取消与嵌套事件流关闭。
|
|
371
406
|
- [ ] 工具增量事件。
|
|
372
407
|
- [x] 第一版 CLI 交互与终端流式展示代码。
|
|
@@ -62,6 +62,18 @@ class ErrorEvent(BaseModel):
|
|
|
62
62
|
type: Literal["error"] = "error"
|
|
63
63
|
reason: Literal["error", "aborted"]
|
|
64
64
|
error: AssistantMessage
|
|
65
|
+
retryable: bool = False
|
|
66
|
+
retry_after_seconds: float | None = Field(default=None, ge=0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RetryEvent(BaseModel):
|
|
70
|
+
model_config = ConfigDict(extra="forbid")
|
|
71
|
+
|
|
72
|
+
type: Literal["retry"] = "retry"
|
|
73
|
+
attempt: int = Field(ge=1)
|
|
74
|
+
max_retries: int = Field(ge=1)
|
|
75
|
+
delay_seconds: float = Field(ge=0)
|
|
76
|
+
error_message: str
|
|
65
77
|
|
|
66
78
|
|
|
67
79
|
AssistantMessageEvent = Annotated[
|
|
@@ -73,6 +85,7 @@ AssistantMessageEvent = Annotated[
|
|
|
73
85
|
| ToolCallDeltaEvent
|
|
74
86
|
| ToolCallEndEvent
|
|
75
87
|
| DoneEvent
|
|
88
|
+
| RetryEvent
|
|
76
89
|
| ErrorEvent,
|
|
77
90
|
Field(discriminator="type"),
|
|
78
|
-
]
|
|
91
|
+
]
|
|
@@ -44,6 +44,10 @@ from .tool_call_buffer import ToolCallBuffer
|
|
|
44
44
|
from .chat_stream_types import ChatStreamChunk
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class IncompleteStreamError(RuntimeError):
|
|
48
|
+
"""The response ended before its required completion markers arrived."""
|
|
49
|
+
|
|
50
|
+
|
|
47
51
|
def to_text(content: str | Iterable[TextContent | ImageContent]) -> str:
|
|
48
52
|
if isinstance(content, str):
|
|
49
53
|
return content
|
|
@@ -271,14 +275,14 @@ async def stream_chat_data(
|
|
|
271
275
|
|
|
272
276
|
data_lines.append(value)
|
|
273
277
|
|
|
274
|
-
raise
|
|
278
|
+
raise IncompleteStreamError(
|
|
275
279
|
"The chat stream ended without a [DONE] marker"
|
|
276
280
|
)
|
|
277
281
|
|
|
278
282
|
async def stream_assistant_events(
|
|
279
283
|
model: ModelSpec,
|
|
280
284
|
data_stream: AsyncIterator[str],
|
|
281
|
-
) ->
|
|
285
|
+
) -> AsyncGenerator[AssistantMessageEvent, None]:
|
|
282
286
|
message = AssistantMessage(
|
|
283
287
|
content=[],
|
|
284
288
|
api=model.api,
|
|
@@ -399,7 +403,7 @@ async def stream_assistant_events(
|
|
|
399
403
|
finish_reason = choice.finish_reason
|
|
400
404
|
|
|
401
405
|
if finish_reason is None:
|
|
402
|
-
raise
|
|
406
|
+
raise IncompleteStreamError(
|
|
403
407
|
"The response ended without a finish reason"
|
|
404
408
|
)
|
|
405
409
|
|
|
@@ -478,8 +482,26 @@ async def stream_assistant_events(
|
|
|
478
482
|
|
|
479
483
|
except Exception as exc:
|
|
480
484
|
message.stop_reason = "error"
|
|
485
|
+
retryable = isinstance(exc, (
|
|
486
|
+
httpx.TimeoutException, httpx.NetworkError,
|
|
487
|
+
httpx.RemoteProtocolError, IncompleteStreamError,
|
|
488
|
+
))
|
|
489
|
+
retry_after: float | None = None
|
|
490
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
491
|
+
retryable = exc.response.status_code in (408, 429, 500, 502, 503, 504)
|
|
492
|
+
header = exc.response.headers.get("Retry-After", "").strip()
|
|
493
|
+
if header.isascii() and header.isdigit():
|
|
494
|
+
retry_after = min(30.0, float(header))
|
|
481
495
|
detail = str(exc).strip()
|
|
482
|
-
if isinstance(exc, httpx.
|
|
496
|
+
if isinstance(exc, (httpx.RemoteProtocolError, IncompleteStreamError)):
|
|
497
|
+
detail = (
|
|
498
|
+
"模型接口的流式响应未完整接收,连接就已中断。"
|
|
499
|
+
"可能与服务端、代理或网络有关,不能仅凭此错误确定具体来源。"
|
|
500
|
+
"此前已完成的工具操作不受影响;本条未完整回复中的工具调用未执行。"
|
|
501
|
+
"可以继续当前会话,先检查实际文件状态,再接着完成任务。"
|
|
502
|
+
f" 原始错误:{detail or '服务端响应传输不完整。'}"
|
|
503
|
+
)
|
|
504
|
+
elif isinstance(exc, httpx.ReadTimeout):
|
|
483
505
|
detail = (
|
|
484
506
|
"等待模型响应数据超时(ReadTimeout)。"
|
|
485
507
|
"这是模型接口读取超时,不代表上一条工具命令失败。"
|
|
@@ -496,6 +518,8 @@ async def stream_assistant_events(
|
|
|
496
518
|
yield ErrorEvent(
|
|
497
519
|
reason="error",
|
|
498
520
|
error=message,
|
|
521
|
+
retryable=retryable,
|
|
522
|
+
retry_after_seconds=retry_after,
|
|
499
523
|
)
|
|
500
524
|
|
|
501
525
|
finally:
|
|
@@ -4,6 +4,7 @@ import httpx
|
|
|
4
4
|
from pydantic import SecretStr
|
|
5
5
|
|
|
6
6
|
from ..events import AssistantMessageEvent
|
|
7
|
+
from ..retry import stream_with_retries
|
|
7
8
|
from ..types import (
|
|
8
9
|
ImageContent, Message, ModelContext, ModelOptions, ModelSpec,
|
|
9
10
|
TextContent, ToolResultMessage, UserMessage,
|
|
@@ -91,14 +92,11 @@ class DeepSeekProvider:
|
|
|
91
92
|
|
|
92
93
|
body["thinking"] = {"type": "disabled"}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return stream_assistant_events(
|
|
102
|
-
model=model,
|
|
103
|
-
data_stream=data_stream,
|
|
95
|
+
return stream_with_retries(
|
|
96
|
+
lambda: stream_assistant_events(
|
|
97
|
+
model=model,
|
|
98
|
+
data_stream=stream_chat_data(
|
|
99
|
+
client=self._client, model=model, api_key=self._api_key, body=body,
|
|
100
|
+
),
|
|
101
|
+
),
|
|
104
102
|
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Retry an uncommitted model response; tool execution stays outside this layer."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator, Callable
|
|
5
|
+
from contextlib import aclosing
|
|
6
|
+
|
|
7
|
+
from .events import AssistantMessageEvent, DoneEvent, ErrorEvent, RetryEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def stream_with_retries(
|
|
11
|
+
create_stream: Callable[[], AsyncGenerator[AssistantMessageEvent, None]],
|
|
12
|
+
*,
|
|
13
|
+
max_retries: int = 3,
|
|
14
|
+
) -> AsyncGenerator[AssistantMessageEvent, None]:
|
|
15
|
+
if max_retries < 0:
|
|
16
|
+
raise ValueError("max_retries must not be negative")
|
|
17
|
+
for attempt in range(max_retries + 1):
|
|
18
|
+
failure: ErrorEvent | None = None
|
|
19
|
+
async with aclosing(create_stream()) as stream:
|
|
20
|
+
async for event in stream:
|
|
21
|
+
if isinstance(event, ErrorEvent):
|
|
22
|
+
failure = event
|
|
23
|
+
break
|
|
24
|
+
yield event
|
|
25
|
+
if isinstance(event, DoneEvent):
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if failure is None:
|
|
29
|
+
raise RuntimeError("The model stream ended without a terminal event")
|
|
30
|
+
if not failure.retryable or failure.reason != "error" or attempt == max_retries:
|
|
31
|
+
yield failure
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
delay = min(30.0, max(2.0 ** (attempt + 1), failure.retry_after_seconds or 0.0))
|
|
35
|
+
yield RetryEvent(
|
|
36
|
+
attempt=attempt + 1,
|
|
37
|
+
max_retries=max_retries,
|
|
38
|
+
delay_seconds=delay,
|
|
39
|
+
error_message=failure.error.error_message or "模型请求暂时失败",
|
|
40
|
+
)
|
|
41
|
+
# Cancellation propagates, including while waiting between attempts.
|
|
42
|
+
await asyncio.sleep(delay)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Slash command descriptions shared by help and input completion."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.completion import Completer, CompleteEvent, Completion
|
|
7
|
+
from prompt_toolkit.document import Document
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Command:
|
|
12
|
+
name: str
|
|
13
|
+
description: str
|
|
14
|
+
argument: str = ""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def usage(self) -> str:
|
|
18
|
+
return f"{self.name} {self.argument}" if self.argument else self.name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
COMMANDS = (
|
|
22
|
+
Command("/help", "显示帮助"),
|
|
23
|
+
Command("/status", "查看运行状态、模型和消息队列"),
|
|
24
|
+
Command("/history", "显示当前分支的对话"),
|
|
25
|
+
Command("/tree", "列出会话节点与当前分支"),
|
|
26
|
+
Command("/checkout", "空闲且无排队消息时切换分支,支持唯一 ID 前缀", "<节点ID>"),
|
|
27
|
+
Command("/steer", "向正在运行的 Agent 追加引导消息", "<消息>"),
|
|
28
|
+
Command("/follow", "排队,等待 Agent 正常完成后处理", "<消息>"),
|
|
29
|
+
Command("/clear-queue", "清空尚未接纳的排队消息"),
|
|
30
|
+
Command("/recover", "空闲且无排队消息时补齐缺失的工具结果"),
|
|
31
|
+
Command("/abort", "取消当前任务,等待收尾"),
|
|
32
|
+
Command("/quit", "取消当前任务并退出"),
|
|
33
|
+
Command("/exit", "退出,等同于 /quit"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_command_input(document: Document) -> bool:
|
|
38
|
+
return (
|
|
39
|
+
document.text.startswith("/")
|
|
40
|
+
and document.cursor_position == len(document.text)
|
|
41
|
+
and not any(character.isspace() for character in document.text)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SlashCommandCompleter(Completer):
|
|
46
|
+
def get_completions(
|
|
47
|
+
self, document: Document, complete_event: CompleteEvent,
|
|
48
|
+
) -> Iterable[Completion]:
|
|
49
|
+
if not is_command_input(document):
|
|
50
|
+
return
|
|
51
|
+
prefix = document.text
|
|
52
|
+
for command in COMMANDS:
|
|
53
|
+
if command.name.startswith(prefix):
|
|
54
|
+
yield Completion(
|
|
55
|
+
command.name,
|
|
56
|
+
start_position=-len(prefix),
|
|
57
|
+
display=command.usage,
|
|
58
|
+
display_meta=command.description,
|
|
59
|
+
)
|
|
@@ -218,7 +218,8 @@ async def run_application(options: Options) -> int:
|
|
|
218
218
|
raise ValueError("恢复时必须使用会话记录的模型,当前尚未提供模型切换入口")
|
|
219
219
|
tools = create_tools(cwd, settings.tools)
|
|
220
220
|
system_prompt = build_system_prompt(
|
|
221
|
-
cwd, settings.tools, context_files,
|
|
221
|
+
cwd, settings.tools, context_files,
|
|
222
|
+
model_id=model.id, supports_images=model.supports_images,
|
|
222
223
|
)
|
|
223
224
|
if options.resume is None:
|
|
224
225
|
manager.save(path)
|
|
@@ -233,7 +234,7 @@ async def run_application(options: Options) -> int:
|
|
|
233
234
|
max_turns=options.max_turns,
|
|
234
235
|
)
|
|
235
236
|
console.welcome(
|
|
236
|
-
cwd, model,
|
|
237
|
+
cwd, model,
|
|
237
238
|
fake=fake, resumed=options.resume is not None, interactive=options.prompt is None,
|
|
238
239
|
)
|
|
239
240
|
if recovered:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Flush Rich transcript output while the prompt renderer is suspended."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit import PromptSession
|
|
8
|
+
from prompt_toolkit.application import run_in_terminal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TerminalOutput(StringIO):
|
|
12
|
+
def __init__(self, original: TextIO, prompt: PromptSession[str]) -> None:
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.original = original
|
|
15
|
+
self.prompt = prompt
|
|
16
|
+
self._drain_lock = asyncio.Lock()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def encoding(self) -> str:
|
|
20
|
+
return self.original.encoding or "utf-8"
|
|
21
|
+
|
|
22
|
+
def isatty(self) -> bool:
|
|
23
|
+
return self.original.isatty()
|
|
24
|
+
|
|
25
|
+
# Keep StringIO.fileno unsupported: Rich must not bypass the buffer via
|
|
26
|
+
# the Windows console handle while prompt_toolkit is drawing the input.
|
|
27
|
+
async def drain(self) -> None:
|
|
28
|
+
async with self._drain_lock:
|
|
29
|
+
text = self.getvalue()
|
|
30
|
+
if not text:
|
|
31
|
+
return
|
|
32
|
+
self.seek(0)
|
|
33
|
+
self.truncate(0)
|
|
34
|
+
|
|
35
|
+
def write() -> None:
|
|
36
|
+
output = self.prompt.app.output
|
|
37
|
+
output.enable_autowrap()
|
|
38
|
+
output.write_raw(text)
|
|
39
|
+
output.flush()
|
|
40
|
+
|
|
41
|
+
await run_in_terminal(write)
|