@jun133/kitty 0.0.7 → 0.0.9
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.
- package/README.md +59 -20
- package/dist/App-6FETP3LH.mjs +521 -0
- package/dist/chunk-3KMC6H5K.mjs +2701 -0
- package/dist/chunk-4BN45TQG.mjs +654 -0
- package/dist/chunk-6NJJLOY3.mjs +2129 -0
- package/dist/chunk-DFDOKON5.mjs +530 -0
- package/dist/chunk-ELBEXOR7.mjs +10020 -0
- package/dist/chunk-YSWK3BGL.mjs +84 -0
- package/dist/cli.js +1321 -655
- package/dist/cli.js.map +1 -1
- package/dist/interactive-KLW4JL7R.mjs +340 -0
- package/dist/oneShot-YHDMPFQM.mjs +54 -0
- package/dist/session-XKWJHRVY.mjs +66 -0
- package/dist/tui.mjs +728 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -14,13 +14,20 @@
|
|
|
14
14
|
<img alt="agent" src="https://img.shields.io/badge/mode-agent-7c3aed">
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
小猫智能体是给本地代码仓库使用的 agent harness。
|
|
18
|
-
|
|
19
|
-
它把模型、工具、上下文、会话、变更记录和验证事实收进一个稳定的本地编程体验里,让长任务可以被推进、保存、恢复和继续。
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
小猫智能体是给本地代码仓库使用的 agent harness。
|
|
18
|
+
|
|
19
|
+
它把模型、工具、上下文、会话、变更记录和验证事实收进一个稳定的本地编程体验里,让长任务可以被推进、保存、恢复和继续。
|
|
20
|
+
|
|
21
|
+
它的主线不是自我改造,而是把本地 coding agent 做成可恢复、可验收、省钱的执行系统:
|
|
22
|
+
|
|
23
|
+
- 本地执行内核:聊天只是入口,session、workset、execution、events、memory 和 status 才是任务现场。
|
|
24
|
+
- 生产现场:`kitty status` 把当前目标、下一步、阻塞、后台、成本、恢复、skill 和 memory 汇成一眼可读的现场。
|
|
25
|
+
- Cost Kernel:即使只用一个模型,也通过稳定前缀、易变尾部、大内容压缩、按需 skill 和 cache usage 审阅来省 token。
|
|
26
|
+
- 产品级验收合同:`kitty eval` 验证真实用户路径,而不是只证明模块能 import。
|
|
27
|
+
|
|
28
|
+
## ✨ 为什么是小猫智能体
|
|
29
|
+
|
|
30
|
+
小猫智能体的核心体验很明确:
|
|
24
31
|
|
|
25
32
|
- 🧭 一个 agent 主循环负责推进任务。
|
|
26
33
|
- 🛠️ 四个 core 工具完成基础编程闭环。
|
|
@@ -37,14 +44,15 @@
|
|
|
37
44
|
| 💾 Session | 会话记录、checkpoint、todo、工作集、恢复现场、结构化可审阅 memory assets |
|
|
38
45
|
| 🗺️ Project Map | 目录、入口、脚本、测试、仓库文档和 git 事实进入短项目地图 |
|
|
39
46
|
| 🔌 Provider | OpenAI-compatible provider、请求恢复、连接诊断 |
|
|
40
|
-
| ❄️
|
|
47
|
+
| ❄️ Cost Kernel | 稳定前缀和易变尾部分离,大输出压缩,skill 默认只给索引,读取 provider usage 里的 cache hit / miss / read / write,status 显示稳定比例和最近请求命中状态 |
|
|
41
48
|
| 🛠️ Core tools | `read`、`edit`、`write`、`bash` |
|
|
42
49
|
| 🧩 Extensions | `todo`、`worktree`、`network`、`background`、`subagent`、`skills` |
|
|
43
50
|
| 🧾 Control plane | SQLite 账本记录 task lifecycle、execution、deadline、输出健康、wait policy、pid、状态和 wake 事实;host 负责等待和恢复 lead |
|
|
51
|
+
| 🧯 Production scene | `status` 把 session、background、subagent、memory、skills、cache、wake 和失败边界投影成当前现场、下一步和阻塞原因 |
|
|
44
52
|
| 📋 Plan 工作流 | `plan.md` 是当前任务总管,配合 plan skill 管理需求、事实、失败测试、目标、设计、任务、验证和收口 |
|
|
45
53
|
| 💬 产品面 | CLI、交互终端、Telegram 私聊服务 |
|
|
46
54
|
| 📎 证据记录 | session events、终端日志、崩溃记录、文件变更记录 |
|
|
47
|
-
| 🧪 Evaluation | `kitty eval`
|
|
55
|
+
| 🧪 Evaluation | `kitty eval` 暴露产品验收场景,`--run` 会跑本地可验证检查闭环 |
|
|
48
56
|
|
|
49
57
|
## ⚡ 快速开始
|
|
50
58
|
|
|
@@ -61,15 +69,43 @@ npm.cmd run build
|
|
|
61
69
|
kitty init
|
|
62
70
|
```
|
|
63
71
|
|
|
64
|
-
启动交互式 agent:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
kitty
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
如果已有会话,`kitty` 会先显示最近会话列表:输入 `1` 继续最近会话,输入 `0` 新建会话。没有历史会话时会直接新建。会话标题由模型在第一次真实对话完成后生成,后续保持稳定。
|
|
72
|
+
启动交互式 agent:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
kitty
|
|
76
|
+
```
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
如果已有会话,`kitty` 会先显示最近会话列表:输入 `1` 继续最近会话,输入 `0` 新建会话。没有历史会话时会直接新建。会话标题由模型在第一次真实对话完成后生成,后续保持稳定。
|
|
79
|
+
|
|
80
|
+
启动 Ink TUI:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
kitty tui
|
|
84
|
+
node dist/cli.js tui
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
TUI 是可替换的终端壳层,复用同一套 session、driver、工具和斜杠命令。主区显示用户输入、thinking 和回复;工具、后台任务、subagent 和上下文占用在底部现场区呈现,不灌进主对话区。按键:`Enter` 发送,`Ctrl+J` 换行,`PageUp` / `PageDown` 滚动,`Home` / `End` 跳到顶部 / 底部,鼠标滚轮滚动,`Ctrl+C` 中断当前轮。
|
|
88
|
+
|
|
89
|
+
交互模式支持本地斜杠命令。斜杠命令直接读取本地现场,不发送给模型:
|
|
90
|
+
|
|
91
|
+
| 斜杠命令 | 用途 |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `/help` | 查看当前可用斜杠命令 |
|
|
94
|
+
| `/status` | 查看当前项目现场 |
|
|
95
|
+
| `/background`、`/bg` | 查看后台任务现场 |
|
|
96
|
+
| `/memory` | 查看 runtime memory assets |
|
|
97
|
+
| `/skills` | 查看 runtime skills 健康状态 |
|
|
98
|
+
| `/events` | 查看当前 session 最近事件 |
|
|
99
|
+
| `/doctor` | 运行本地配置 preflight |
|
|
100
|
+
| `/sessions` | 查看最近会话 |
|
|
101
|
+
| `/session` | 查看当前 session id |
|
|
102
|
+
| `/copy` | 打印当前 session 对话文本 |
|
|
103
|
+
| `/export` | 打印当前 session JSON 快照 |
|
|
104
|
+
| `/clear` | 清空 UI shell 的当前输入语义 |
|
|
105
|
+
| `/reset` | 清空当前项目运行状态并退出 |
|
|
106
|
+
| `/exit`、`quit`、`q` | 退出当前会话 |
|
|
107
|
+
|
|
108
|
+
执行一次明确任务:
|
|
73
109
|
|
|
74
110
|
```bash
|
|
75
111
|
kitty "检查这个仓库并修复失败测试"
|
|
@@ -81,19 +117,20 @@ kitty "检查这个仓库并修复失败测试"
|
|
|
81
117
|
| --- | --- |
|
|
82
118
|
| `kitty` | 进入默认 agent 交互;有历史会话时先选择继续或新建,也可直接接收一次性 prompt |
|
|
83
119
|
| `kitty agent` | 显式进入 agent 模式 |
|
|
120
|
+
| `kitty tui` | 进入 Ink 终端工作台,支持主区滚动、底部输入和运行现场 |
|
|
84
121
|
| `kitty background` | 查看后台任务;`wait <id>` 等待任务;`stop <id>` 停止任务 |
|
|
85
122
|
| `kitty resume [sessionId]` | 恢复最近会话或指定会话 |
|
|
86
123
|
| `kitty sessions` | 查看最近会话 |
|
|
87
124
|
| `kitty events [sessionId]` | 查看最近会话或指定会话的机器事件 |
|
|
88
125
|
| `kitty config show` | 查看从 `.kitty/.env` 解析出的当前运行配置 |
|
|
89
126
|
| `kitty config path` | 查看当前项目 `.kitty/.env` 路径 |
|
|
90
|
-
| `kitty status` |
|
|
127
|
+
| `kitty status` | 查看当前项目现场:当前目标、下一步、阻塞、后台、恢复、成本、session、context budget、memory、skills、project map、execution、wake |
|
|
91
128
|
| `kitty memory` | 创建、查看、读取、搜索、删除 runtime memory assets,或把 memory 沉淀到 skill references |
|
|
92
129
|
| `kitty changes` | 查看记录的文件变更 |
|
|
93
130
|
| `kitty undo [changeId]` | 撤销最近一次或指定变更 |
|
|
94
131
|
| `kitty diff [path]` | 查看当前 git diff |
|
|
95
132
|
| `kitty doctor` | 检查 `.kitty` 文件、env contract、provider preset、runtime、provider 连接和下一步 |
|
|
96
|
-
| `kitty eval` |
|
|
133
|
+
| `kitty eval` | 查看产品验收场景;`kitty eval --run` 运行本地机器验收 |
|
|
97
134
|
| `kitty telegram serve` | 启动 Telegram 私聊服务 |
|
|
98
135
|
|
|
99
136
|
## 🛠️ 工具体系
|
|
@@ -126,9 +163,11 @@ Runtime skills 放在项目 `SKILL.md`、`.skills/**/SKILL.md` 或 `skills/**/SK
|
|
|
126
163
|
|
|
127
164
|
Provider 请求优先携带同 session 的近场可见对话。短会话不靠账本拼上下文;长会话超预算时摘要旧对话,保留最近对话 tail。Session memory 由模型在 turn 收口时按固定 Markdown 区块写出:`Current Focus`、`User Constraints`、`Decisions`、`Open Threads`、`Verification Facts`、`Reusable Lessons`。机器只维护格式和保存边界,不替模型判断事实重要性。
|
|
128
165
|
|
|
166
|
+
Cost Kernel 的边界很硬:省钱不靠模型路由,不靠把能力关掉,而靠上下文结构。稳定内容放在前缀,易变事实放在尾部;大段工具输出和旧历史进入压缩摘要或证据资产;skill 正文、resources、examples 不默认注入,模型需要时再加载。
|
|
167
|
+
|
|
129
168
|
Provider usage 会归一化缓存事实:DeepSeek 的 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`,OpenAI 的 `prompt_tokens_details.cached_tokens`,Anthropic 的 `cache_read_input_tokens` / `cache_creation_input_tokens`,以及 Gemini cached content tokens。`model.request` observability 事件会记录这些字段,`kitty status` 会显示最近模型请求的缓存命中和 context cache layout。OpenAI 请求会使用同 session 稳定 `prompt_cache_key`;DeepSeek 不写无效 `cache_control`,优先保持稳定前缀和命中观测。
|
|
130
169
|
|
|
131
|
-
`kitty eval --run`
|
|
170
|
+
`kitty eval` 是产品验收合同:每个场景都说明用户路径和机器证据。`kitty eval --run` 包含生产现场、恢复演练、远程入口、cache economy、skill/memory readiness 和失败边界检查。usage 字段解析、provider cache policy、stable prefix fingerprint、volatile tail、skill index boundary 和大输出压缩都必须能机器验证。真实省钱仍取决于 provider 是否返回 usage,以及同一 session 的前缀是否真的被上游缓存命中。
|
|
132
171
|
|
|
133
172
|
Session workset 记录当前会话实际读过和改过的文件。`read` 成功后记录读取事实,`edit` / `write` 成功后记录变更事实和 change id。工作集会进入 session、working memory 和 `kitty status`,让用户看到当前任务真正碰过哪些文件。
|
|
134
173
|
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TRANSCRIPT_OUTER_PADDING_X,
|
|
3
|
+
TUI_COLORS,
|
|
4
|
+
renderTranscriptLineViews
|
|
5
|
+
} from "./chunk-DFDOKON5.mjs";
|
|
6
|
+
|
|
7
|
+
// src/shell/tui/layout.ts
|
|
8
|
+
var TUI_MIN_WIDTH = 48;
|
|
9
|
+
var TUI_MIN_HEIGHT = 14;
|
|
10
|
+
var TUI_DOCK_ROWS = 2;
|
|
11
|
+
var TUI_COMPOSER_MAX_ROWS = 6;
|
|
12
|
+
var TUI_FOOTER_BORDER_TOP_ROWS = 1;
|
|
13
|
+
var TUI_FOOTER_PADDING_X = 2;
|
|
14
|
+
var TUI_FOOTER_PADDING_BOTTOM_ROWS = 1;
|
|
15
|
+
var COMPOSER_VERTICAL_PADDING_ROWS = 2;
|
|
16
|
+
function measureTuiFooterRows(composerRows) {
|
|
17
|
+
return TUI_FOOTER_BORDER_TOP_ROWS + TUI_DOCK_ROWS + COMPOSER_VERTICAL_PADDING_ROWS + normalizeComposerRows(composerRows) + TUI_FOOTER_PADDING_BOTTOM_ROWS;
|
|
18
|
+
}
|
|
19
|
+
function normalizeComposerRows(rows) {
|
|
20
|
+
return Math.max(1, Math.min(TUI_COMPOSER_MAX_ROWS, Math.floor(rows)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/shell/tui/composerEditing.ts
|
|
24
|
+
function applyComposerInput(state, input, key) {
|
|
25
|
+
if (key.return && (key.shift || key.ctrl)) {
|
|
26
|
+
return insertText(state, "\n");
|
|
27
|
+
}
|
|
28
|
+
if (key.return) {
|
|
29
|
+
return { kind: "submit", state: { cursor: 0, value: "" }, value: state.value };
|
|
30
|
+
}
|
|
31
|
+
if (key.ctrl && input.toLowerCase() === "a") {
|
|
32
|
+
return update({ ...state, cursor: 0 });
|
|
33
|
+
}
|
|
34
|
+
if (key.ctrl && input.toLowerCase() === "e") {
|
|
35
|
+
return update({ ...state, cursor: state.value.length });
|
|
36
|
+
}
|
|
37
|
+
if (key.ctrl && input.toLowerCase() === "u") {
|
|
38
|
+
return update({ cursor: 0, value: "" });
|
|
39
|
+
}
|
|
40
|
+
if (key.leftArrow) {
|
|
41
|
+
return update({ ...state, cursor: previousGraphemeOffset(state.value, state.cursor) });
|
|
42
|
+
}
|
|
43
|
+
if (key.rightArrow) {
|
|
44
|
+
return update({ ...state, cursor: nextGraphemeOffset(state.value, state.cursor) });
|
|
45
|
+
}
|
|
46
|
+
if (key.home) {
|
|
47
|
+
return update({ ...state, cursor: 0 });
|
|
48
|
+
}
|
|
49
|
+
if (key.end) {
|
|
50
|
+
return update({ ...state, cursor: state.value.length });
|
|
51
|
+
}
|
|
52
|
+
if (key.backspace || key.delete) {
|
|
53
|
+
if (state.cursor <= 0) {
|
|
54
|
+
return update(state);
|
|
55
|
+
}
|
|
56
|
+
const nextCursor = previousGraphemeOffset(state.value, state.cursor);
|
|
57
|
+
return update({
|
|
58
|
+
cursor: nextCursor,
|
|
59
|
+
value: state.value.slice(0, nextCursor) + state.value.slice(state.cursor)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (key.tab) {
|
|
63
|
+
return insertText(state, " ");
|
|
64
|
+
}
|
|
65
|
+
if (!key.ctrl && !key.meta && input) {
|
|
66
|
+
return insertText(state, normalizeTypedInput(input));
|
|
67
|
+
}
|
|
68
|
+
return update(state);
|
|
69
|
+
}
|
|
70
|
+
function insertText(state, text) {
|
|
71
|
+
if (!text) {
|
|
72
|
+
return update(state);
|
|
73
|
+
}
|
|
74
|
+
const value = state.value.slice(0, state.cursor) + text + state.value.slice(state.cursor);
|
|
75
|
+
return update({
|
|
76
|
+
cursor: state.cursor + text.length,
|
|
77
|
+
value
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function normalizeTypedInput(input) {
|
|
81
|
+
return input.replace(/\r\n/g, "\n");
|
|
82
|
+
}
|
|
83
|
+
function update(state) {
|
|
84
|
+
return { kind: "update", state };
|
|
85
|
+
}
|
|
86
|
+
var graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
87
|
+
function previousGraphemeOffset(value, cursor) {
|
|
88
|
+
if (cursor <= 0) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
let previous = 0;
|
|
92
|
+
for (const segment of graphemeSegmenter.segment(value)) {
|
|
93
|
+
if (segment.index >= cursor) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
previous = segment.index;
|
|
97
|
+
}
|
|
98
|
+
return previous;
|
|
99
|
+
}
|
|
100
|
+
function nextGraphemeOffset(value, cursor) {
|
|
101
|
+
if (cursor >= value.length) {
|
|
102
|
+
return value.length;
|
|
103
|
+
}
|
|
104
|
+
for (const segment of graphemeSegmenter.segment(value)) {
|
|
105
|
+
const end = segment.index + segment.segment.length;
|
|
106
|
+
if (end > cursor) {
|
|
107
|
+
return end;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return value.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/shell/tui/composerLayout.ts
|
|
114
|
+
import stringWidth from "string-width";
|
|
115
|
+
import wrapAnsi from "wrap-ansi";
|
|
116
|
+
var COMPOSER_FRAME = {
|
|
117
|
+
gap: 2,
|
|
118
|
+
gutter: "\u2503",
|
|
119
|
+
paddingX: 2,
|
|
120
|
+
paddingY: 1,
|
|
121
|
+
tabWidth: 2
|
|
122
|
+
};
|
|
123
|
+
function measureComposerContentWidth(containerWidth) {
|
|
124
|
+
return Math.max(
|
|
125
|
+
1,
|
|
126
|
+
Math.floor(containerWidth) - COMPOSER_FRAME.paddingX * 2 - stringWidth(COMPOSER_FRAME.gutter) - COMPOSER_FRAME.gap
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function measureComposerTextOrigin(metrics) {
|
|
130
|
+
if (!metrics.hasMeasured) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
x: metrics.left,
|
|
135
|
+
y: metrics.top
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function layoutComposer(input) {
|
|
139
|
+
const contentWidth = Math.max(1, Math.floor(input.contentWidth ?? input.frame.width));
|
|
140
|
+
const rows = wrapComposerRows(input.value, contentWidth);
|
|
141
|
+
const visibleRows = normalizeComposerRows(rows.length);
|
|
142
|
+
const visibleStart = Math.max(0, rows.length - visibleRows);
|
|
143
|
+
const visible = rows.slice(visibleStart);
|
|
144
|
+
const origin = measureComposerTextOrigin(input.frame);
|
|
145
|
+
const cursor = origin ? measureComposerCursor({
|
|
146
|
+
contentWidth,
|
|
147
|
+
cursor: input.cursor,
|
|
148
|
+
origin,
|
|
149
|
+
rows,
|
|
150
|
+
visibleStart,
|
|
151
|
+
value: input.value
|
|
152
|
+
}) : void 0;
|
|
153
|
+
const cursorCell = measureComposerCursorCell({
|
|
154
|
+
contentWidth,
|
|
155
|
+
cursor: input.cursor,
|
|
156
|
+
rows,
|
|
157
|
+
visibleStart,
|
|
158
|
+
value: input.value
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
contentWidth,
|
|
162
|
+
cursor,
|
|
163
|
+
cursorCell,
|
|
164
|
+
rows: visible,
|
|
165
|
+
visibleRows
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function composeInkCursorPosition(input) {
|
|
169
|
+
if (!input.cell || !input.rowFrame.hasMeasured) {
|
|
170
|
+
return input.fallback ? shiftInkCursorRow(input.fallback) : void 0;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
x: input.rowFrame.left + input.cell.x,
|
|
174
|
+
y: input.rowFrame.top + 1
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function measureComposerCursor(input) {
|
|
178
|
+
const cell = measureComposerCursorCell(input);
|
|
179
|
+
return {
|
|
180
|
+
x: input.origin.x + cell.x,
|
|
181
|
+
y: input.origin.y + cell.y
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function measureComposerCursorCell(input) {
|
|
185
|
+
const beforeCursor = input.value.slice(0, Math.max(0, Math.min(input.cursor, input.value.length)));
|
|
186
|
+
const rowsBeforeCursor = wrapComposerRows(beforeCursor, input.contentWidth);
|
|
187
|
+
const cursorRow = Math.max(0, rowsBeforeCursor.length - 1);
|
|
188
|
+
const cursorVisibleRow = Math.max(0, cursorRow - input.visibleStart);
|
|
189
|
+
const cursorLine = rowsBeforeCursor.at(-1) ?? "";
|
|
190
|
+
return {
|
|
191
|
+
x: Math.min(stringWidth(cursorLine), input.contentWidth),
|
|
192
|
+
y: cursorVisibleRow
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function shiftInkCursorRow(position) {
|
|
196
|
+
return {
|
|
197
|
+
x: position.x,
|
|
198
|
+
y: position.y + 1
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function wrapComposerRows(value, width) {
|
|
202
|
+
const text = value || "";
|
|
203
|
+
const rows = text.split(/\r?\n/).flatMap((line) => {
|
|
204
|
+
const wrapped = wrapAnsi(line, width, { hard: true, trim: false });
|
|
205
|
+
return wrapped.split(/\r?\n/).map((row) => row.trim() ? row : "");
|
|
206
|
+
});
|
|
207
|
+
return rows.length > 0 ? rows : [""];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/shell/tui/inkGeometry.ts
|
|
211
|
+
function measureAbsoluteBox(node) {
|
|
212
|
+
if (!node?.yogaNode) {
|
|
213
|
+
return {
|
|
214
|
+
hasMeasured: false,
|
|
215
|
+
left: 0,
|
|
216
|
+
top: 0,
|
|
217
|
+
width: 0
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
let left = 0;
|
|
221
|
+
let top = 0;
|
|
222
|
+
let current = node;
|
|
223
|
+
while (current?.yogaNode) {
|
|
224
|
+
const layout = current.yogaNode.getComputedLayout();
|
|
225
|
+
left += layout.left;
|
|
226
|
+
top += layout.top;
|
|
227
|
+
current = current.parentNode;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
hasMeasured: true,
|
|
231
|
+
left,
|
|
232
|
+
top,
|
|
233
|
+
width: node.yogaNode.getComputedLayout().width
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/shell/tui/components/Composer.ts
|
|
238
|
+
function createComposerComponent(kit) {
|
|
239
|
+
const { React, Box, Text, useCursor, useInput } = kit;
|
|
240
|
+
return function Composer(props) {
|
|
241
|
+
const [draft, setDraft] = React.useState({ cursor: 0, value: "" });
|
|
242
|
+
const contentRef = React.useRef(null);
|
|
243
|
+
const cursorRowRef = React.useRef(null);
|
|
244
|
+
const [measuredFrame, setMeasuredFrame] = React.useState({
|
|
245
|
+
hasMeasured: false,
|
|
246
|
+
left: 0,
|
|
247
|
+
top: 0,
|
|
248
|
+
width: props.frame.width
|
|
249
|
+
});
|
|
250
|
+
const [measuredCursorRow, setMeasuredCursorRow] = React.useState({
|
|
251
|
+
hasMeasured: false,
|
|
252
|
+
left: 0,
|
|
253
|
+
top: 0,
|
|
254
|
+
width: props.frame.width
|
|
255
|
+
});
|
|
256
|
+
React.useEffect(() => {
|
|
257
|
+
const next = measureAbsoluteBox(contentRef.current);
|
|
258
|
+
setMeasuredFrame((previous) => previous.hasMeasured === next.hasMeasured && previous.left === next.left && previous.top === next.top && previous.width === next.width ? previous : next);
|
|
259
|
+
const nextCursorRow = measureAbsoluteBox(cursorRowRef.current);
|
|
260
|
+
setMeasuredCursorRow((previous) => previous.hasMeasured === nextCursorRow.hasMeasured && previous.left === nextCursorRow.left && previous.top === nextCursorRow.top && previous.width === nextCursorRow.width ? previous : nextCursorRow);
|
|
261
|
+
});
|
|
262
|
+
const contentWidth = measuredFrame.hasMeasured ? measuredFrame.width : measureComposerContentWidth(props.frame.width);
|
|
263
|
+
const layout = layoutComposer({
|
|
264
|
+
contentWidth,
|
|
265
|
+
cursor: draft.cursor,
|
|
266
|
+
frame: measuredFrame.hasMeasured ? measuredFrame : props.frame,
|
|
267
|
+
value: draft.value
|
|
268
|
+
});
|
|
269
|
+
const { setCursorPosition } = useCursor();
|
|
270
|
+
const cursorPosition = composeInkCursorPosition({
|
|
271
|
+
cell: layout.cursorCell,
|
|
272
|
+
fallback: layout.cursor,
|
|
273
|
+
rowFrame: measuredCursorRow
|
|
274
|
+
});
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
props.controller.updateComposerVisibleRows(layout.visibleRows);
|
|
277
|
+
}, [props.controller, layout.visibleRows]);
|
|
278
|
+
setCursorPosition(cursorPosition);
|
|
279
|
+
useInput((input, key) => {
|
|
280
|
+
const action = applyComposerInput(draft, input, key);
|
|
281
|
+
setDraft(action.state);
|
|
282
|
+
if (action.kind === "submit") {
|
|
283
|
+
props.controller.submitInput(action.value);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return React.createElement(
|
|
287
|
+
Box,
|
|
288
|
+
{
|
|
289
|
+
flexDirection: "row",
|
|
290
|
+
backgroundColor: TUI_COLORS.panelStrong,
|
|
291
|
+
paddingX: COMPOSER_FRAME.paddingX,
|
|
292
|
+
paddingY: COMPOSER_FRAME.paddingY,
|
|
293
|
+
minHeight: 3,
|
|
294
|
+
width: "100%"
|
|
295
|
+
},
|
|
296
|
+
React.createElement(Text, { color: TUI_COLORS.user, wrap: "truncate-end" }, COMPOSER_FRAME.gutter),
|
|
297
|
+
React.createElement(
|
|
298
|
+
Box,
|
|
299
|
+
{
|
|
300
|
+
flexDirection: "column",
|
|
301
|
+
ref: contentRef,
|
|
302
|
+
width: layout.contentWidth,
|
|
303
|
+
marginLeft: COMPOSER_FRAME.gap
|
|
304
|
+
},
|
|
305
|
+
...draft.value ? layout.rows.map((row, index) => React.createElement(
|
|
306
|
+
Box,
|
|
307
|
+
{
|
|
308
|
+
key: index,
|
|
309
|
+
ref: layout.cursorCell?.y === index ? cursorRowRef : void 0,
|
|
310
|
+
height: 1,
|
|
311
|
+
width: layout.contentWidth
|
|
312
|
+
},
|
|
313
|
+
React.createElement(
|
|
314
|
+
Text,
|
|
315
|
+
{ color: TUI_COLORS.text, wrap: "truncate-end" },
|
|
316
|
+
row || " "
|
|
317
|
+
)
|
|
318
|
+
)) : [React.createElement(
|
|
319
|
+
Box,
|
|
320
|
+
{
|
|
321
|
+
key: "placeholder",
|
|
322
|
+
ref: cursorRowRef,
|
|
323
|
+
height: 1,
|
|
324
|
+
width: layout.contentWidth
|
|
325
|
+
},
|
|
326
|
+
React.createElement(
|
|
327
|
+
Text,
|
|
328
|
+
{ color: TUI_COLORS.muted, wrap: "truncate-end" },
|
|
329
|
+
"\u8F93\u5165\u6D88\u606F"
|
|
330
|
+
)
|
|
331
|
+
)]
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/shell/tui/components/RuntimeDock.ts
|
|
338
|
+
function createRuntimeDockComponent(kit) {
|
|
339
|
+
const { React, Box, Text } = kit;
|
|
340
|
+
return function RuntimeDock(props) {
|
|
341
|
+
return React.createElement(
|
|
342
|
+
Box,
|
|
343
|
+
{
|
|
344
|
+
flexDirection: "column",
|
|
345
|
+
width: "100%"
|
|
346
|
+
},
|
|
347
|
+
React.createElement(
|
|
348
|
+
Box,
|
|
349
|
+
{ flexDirection: "row" },
|
|
350
|
+
props.dock.work.active ? React.createElement(Text, { color: TUI_COLORS.user }, "\u25A3 ") : null,
|
|
351
|
+
React.createElement(Text, { color: props.dock.work.active ? TUI_COLORS.user : TUI_COLORS.muted }, props.dock.work.label),
|
|
352
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, " \xB7 "),
|
|
353
|
+
React.createElement(Text, { color: props.dock.work.active ? TUI_COLORS.text : TUI_COLORS.muted }, props.dock.work.detail)
|
|
354
|
+
),
|
|
355
|
+
React.createElement(
|
|
356
|
+
Box,
|
|
357
|
+
{ marginTop: 0 },
|
|
358
|
+
null,
|
|
359
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, "\u540E\u53F0\u4EFB\u52A1 "),
|
|
360
|
+
React.createElement(Text, { color: readFactColor(props.dock.background) }, props.dock.background),
|
|
361
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, " "),
|
|
362
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, "\u5B50\u4EE3\u7406 "),
|
|
363
|
+
React.createElement(Text, { color: readFactColor(props.dock.subagent) }, props.dock.subagent),
|
|
364
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, " "),
|
|
365
|
+
React.createElement(Text, { color: TUI_COLORS.muted }, "\u4E0A\u4E0B\u6587 "),
|
|
366
|
+
React.createElement(Text, { color: TUI_COLORS.text }, props.dock.context)
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function readFactColor(value) {
|
|
372
|
+
if (value.includes("\u5931\u8D25") || value.includes("\u9519\u8BEF") || value.includes("\u5361\u4F4F")) {
|
|
373
|
+
return TUI_COLORS.error;
|
|
374
|
+
}
|
|
375
|
+
if (value.includes("\u8FD0\u884C") || value.includes("\u7B49\u5F85") || value.includes("\u6267\u884C")) {
|
|
376
|
+
return TUI_COLORS.warning;
|
|
377
|
+
}
|
|
378
|
+
if (value.includes("\u5B8C\u6210")) {
|
|
379
|
+
return TUI_COLORS.success;
|
|
380
|
+
}
|
|
381
|
+
return TUI_COLORS.muted;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/shell/tui/components/Transcript.ts
|
|
385
|
+
function createTranscriptComponent(kit) {
|
|
386
|
+
const { React, Box, Text } = kit;
|
|
387
|
+
return function Transcript(props) {
|
|
388
|
+
const rows = renderTranscriptLineViews(props.state.transcript, props.viewport.width).slice(props.state.scroll.offset, props.state.scroll.offset + props.viewport.height);
|
|
389
|
+
return React.createElement(
|
|
390
|
+
Box,
|
|
391
|
+
{
|
|
392
|
+
flexDirection: "column",
|
|
393
|
+
width: "100%",
|
|
394
|
+
height: props.viewport.height,
|
|
395
|
+
overflow: "hidden",
|
|
396
|
+
backgroundColor: TUI_COLORS.background,
|
|
397
|
+
paddingX: TRANSCRIPT_OUTER_PADDING_X
|
|
398
|
+
},
|
|
399
|
+
...rows.map((row) => renderTranscriptLine(React, Box, Text, row)),
|
|
400
|
+
props.state.scroll.newContentPending ? React.createElement(Text, { color: TUI_COLORS.warning }, "\u6709\u65B0\u5185\u5BB9\uFF0C\u6309 End \u56DE\u5230\u5E95\u90E8") : null
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function renderTranscriptLine(React, Box, Text, row) {
|
|
405
|
+
if (row.kind === "spacer") {
|
|
406
|
+
return React.createElement(Box, { key: row.id, height: 1 });
|
|
407
|
+
}
|
|
408
|
+
return React.createElement(
|
|
409
|
+
Box,
|
|
410
|
+
{
|
|
411
|
+
key: row.id,
|
|
412
|
+
flexDirection: "row",
|
|
413
|
+
width: "100%",
|
|
414
|
+
height: 1,
|
|
415
|
+
backgroundColor: row.style.background,
|
|
416
|
+
marginLeft: row.frame.marginLeft,
|
|
417
|
+
paddingLeft: row.frame.paddingLeft,
|
|
418
|
+
paddingRight: row.frame.paddingRight
|
|
419
|
+
},
|
|
420
|
+
React.createElement(Text, { color: row.style.accent, wrap: "truncate-end" }, row.frame.gutter),
|
|
421
|
+
React.createElement(
|
|
422
|
+
Box,
|
|
423
|
+
{ width: row.frame.bodyWidth, marginLeft: row.frame.gap },
|
|
424
|
+
row.prefix ? React.createElement(
|
|
425
|
+
Text,
|
|
426
|
+
{ color: row.style.text, wrap: "truncate-end" },
|
|
427
|
+
React.createElement(Text, { color: TUI_COLORS.thought, italic: row.style.italicPrefix, wrap: "truncate-end" }, row.prefix),
|
|
428
|
+
row.text
|
|
429
|
+
) : React.createElement(Text, {
|
|
430
|
+
color: row.style.text,
|
|
431
|
+
bold: row.style.bold,
|
|
432
|
+
dimColor: row.style.dim,
|
|
433
|
+
wrap: "truncate-end"
|
|
434
|
+
}, row.text)
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/shell/tui/components/App.ts
|
|
440
|
+
function createTuiAppComponent(kit) {
|
|
441
|
+
const { React, Box, useInput, useStdout } = kit;
|
|
442
|
+
const Composer = createComposerComponent(kit);
|
|
443
|
+
const RuntimeDock = createRuntimeDockComponent(kit);
|
|
444
|
+
const Transcript = createTranscriptComponent(kit);
|
|
445
|
+
return function TuiApp(props) {
|
|
446
|
+
const state = useTuiState(React, props.controller);
|
|
447
|
+
const { stdout } = useStdout();
|
|
448
|
+
const width = Math.max(TUI_MIN_WIDTH, stdout.columns ?? 80);
|
|
449
|
+
const height = Math.max(TUI_MIN_HEIGHT, stdout.rows ?? 24);
|
|
450
|
+
const footerRows = measureTuiFooterRows(state.composer.visibleRows);
|
|
451
|
+
const transcriptViewport = React.useMemo(() => ({
|
|
452
|
+
width,
|
|
453
|
+
height: Math.max(1, height - footerRows)
|
|
454
|
+
}), [footerRows, height, width]);
|
|
455
|
+
const composerFrame = React.useMemo(() => ({
|
|
456
|
+
hasMeasured: false,
|
|
457
|
+
left: 0,
|
|
458
|
+
top: 0,
|
|
459
|
+
width: Math.max(1, width - TUI_FOOTER_PADDING_X * 2)
|
|
460
|
+
}), [transcriptViewport.height, width]);
|
|
461
|
+
React.useEffect(() => {
|
|
462
|
+
props.controller.setViewport(transcriptViewport);
|
|
463
|
+
}, [props.controller, transcriptViewport]);
|
|
464
|
+
React.useEffect(() => {
|
|
465
|
+
const disableMouse = props.enableMouseTracking();
|
|
466
|
+
return () => {
|
|
467
|
+
disableMouse();
|
|
468
|
+
};
|
|
469
|
+
}, [props.enableMouseTracking]);
|
|
470
|
+
useInput((input, key) => {
|
|
471
|
+
if (key.ctrl && input === "c") {
|
|
472
|
+
props.controller.interrupt();
|
|
473
|
+
} else if (key.pageUp) {
|
|
474
|
+
props.controller.pageUp();
|
|
475
|
+
} else if (key.pageDown) {
|
|
476
|
+
props.controller.pageDown();
|
|
477
|
+
} else if (key.home) {
|
|
478
|
+
props.controller.scrollTop();
|
|
479
|
+
} else if (key.end) {
|
|
480
|
+
props.controller.scrollBottom();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
return React.createElement(
|
|
484
|
+
Box,
|
|
485
|
+
{ flexDirection: "column", width, height },
|
|
486
|
+
React.createElement(Transcript, { state, viewport: transcriptViewport }),
|
|
487
|
+
React.createElement(
|
|
488
|
+
Box,
|
|
489
|
+
{
|
|
490
|
+
flexDirection: "column",
|
|
491
|
+
borderStyle: "single",
|
|
492
|
+
borderTop: true,
|
|
493
|
+
borderBottom: false,
|
|
494
|
+
borderLeft: false,
|
|
495
|
+
borderRight: false,
|
|
496
|
+
borderColor: TUI_COLORS.border,
|
|
497
|
+
backgroundColor: TUI_COLORS.panel,
|
|
498
|
+
paddingX: TUI_FOOTER_PADDING_X,
|
|
499
|
+
paddingBottom: TUI_FOOTER_PADDING_BOTTOM_ROWS,
|
|
500
|
+
width: "100%"
|
|
501
|
+
},
|
|
502
|
+
React.createElement(RuntimeDock, { dock: state.dock }),
|
|
503
|
+
React.createElement(Composer, {
|
|
504
|
+
controller: props.controller,
|
|
505
|
+
frame: composerFrame,
|
|
506
|
+
state
|
|
507
|
+
})
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function useTuiState(React, controller) {
|
|
513
|
+
return React.useSyncExternalStore(
|
|
514
|
+
(listener) => controller.subscribe(listener),
|
|
515
|
+
() => controller.getState(),
|
|
516
|
+
() => controller.getState()
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
export {
|
|
520
|
+
createTuiAppComponent
|
|
521
|
+
};
|