foliko 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli/src/ui/chat-ui.js +53 -223
  2. package/package.json +1 -1
@@ -1,75 +1,31 @@
1
1
  /**
2
2
  * 聊天界面组件
3
- * 处理用户输入、粘贴检测、输出渲染
3
+ * 使用 readline question() 实现多行输入
4
4
  */
5
5
 
6
6
  const readline = require('readline')
7
- const { EventEmitter } = require('events')
8
7
  const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi')
9
- const { render, renderLine } = require('../utils/markdown')
8
+ const { renderLine } = require('../utils/markdown')
10
9
 
11
- class ChatUI extends EventEmitter {
10
+ class ChatUI {
12
11
  constructor(agent) {
13
- super()
14
12
  this.agent = agent
15
13
  this.rl = null
16
- this.currentLine = ''
17
- this.lines = []
18
- this.pasteId = 0
19
- this.isFirstLine = true
20
-
21
- // 粘贴检测
22
- this.lastKeyTime = 0
23
- this.pasteBuffer = ''
24
- this.isPasting = false
25
-
26
- // 多行输入:记录上次 Enter 时间,检测连续两次空回车
27
- this.lastEnterTime = 0
28
- this.LINE_ENDING_INTERVAL = 800 // 毫秒内连续两次空回车结束输入
14
+ this.lines = [] // 多行输入的累积
29
15
  }
30
16
 
31
17
  /**
32
18
  * 启动聊天界面
33
19
  */
34
20
  start() {
35
- this.setupReadline()
36
- this.printWelcome()
37
- this.prompt()
38
- }
39
-
40
- /**
41
- * 设置 readline
42
- */
43
- setupReadline() {
44
21
  this.rl = readline.createInterface({
45
22
  input: process.stdin,
46
23
  output: process.stdout,
47
24
  crlfDelay: Infinity
48
25
  })
49
26
 
50
- // 使用 keypress 事件支持特殊按键
51
- readline.emitKeypressEvents(process.stdin)
52
-
53
- if (process.stdin.isTTY) {
54
- process.stdin.setRawMode(true)
55
- }
56
-
57
- // 监听按键
58
- process.stdin.on('keypress', this.handleKey.bind(this))
59
-
60
- // 退出时清理
61
- process.on('exit', () => {
62
- if (process.stdin.isTTY) {
63
- process.stdin.setRawMode(false)
64
- }
65
- })
66
-
67
- process.on('SIGINT', () => {
68
- if (process.stdin.isTTY) {
69
- process.stdin.setRawMode(false)
70
- }
71
- process.exit(0)
72
- })
27
+ this.printWelcome()
28
+ this.promptUser()
73
29
  }
74
30
 
75
31
  /**
@@ -77,174 +33,77 @@ class ChatUI extends EventEmitter {
77
33
  */
78
34
  printWelcome() {
79
35
  console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
80
- console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('Enter', DIM)} 换行 | ${colored('双Enter', DIM)} 发送\n`)
36
+ console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
81
37
  }
82
38
 
83
39
  /**
84
- * 显示提示符
40
+ * 获取多行输入
85
41
  */
86
- prompt() {
87
- const prefix = this.isFirstLine ? colored('> ', GREEN) : colored('- ', DIM)
88
- process.stdout.write(`\r${CLEAR_LINE}${prefix}${this.currentLine}`)
89
- }
90
-
91
- /**
92
- * 处理按键
93
- */
94
- handleKey(str, key) {
95
- // Ctrl+C 退出
96
- if (key.ctrl && key.name === 'c') {
97
- console.log('\n再见!')
98
- if (process.stdin.isTTY) {
99
- process.stdin.setRawMode(false)
100
- }
101
- process.exit(0)
102
- return
103
- }
104
-
105
- // Enter 发送
106
- if (key.name === 'return') {
107
- this.handleSubmit()
108
- return
109
- }
42
+ getMultilineInput() {
43
+ return new Promise((resolve) => {
44
+ const lines = []
45
+
46
+ const question = (isFirst) => {
47
+ const prompt = isFirst ? colored('> ', GREEN) : colored('- ', DIM)
48
+ this.rl.question(prompt, (input) => {
49
+ // 输入 !! 立即结束
50
+ if (input.trim() === '!!') {
51
+ const result = lines.join('\n').trim()
52
+ resolve(result)
53
+ return
54
+ }
110
55
 
111
- // Ctrl+Enter 换行
112
- if (key.ctrl && key.name === 'return') {
113
- this.lines.push(this.currentLine)
114
- this.currentLine = ''
115
- this.isFirstLine = false
116
- this.prompt()
117
- return
118
- }
56
+ // 空行:结束输入并发送(第一次空行就发送)
57
+ if (input.trim() === '') {
58
+ const result = lines.join('\n').trim()
59
+ resolve(result)
60
+ return
61
+ }
119
62
 
120
- // Backspace 退格
121
- if (key.name === 'backspace') {
122
- if (this.currentLine.length > 0) {
123
- this.currentLine = this.currentLine.slice(0, -1)
124
- this.prompt()
125
- } else if (this.lines.length > 0) {
126
- this.currentLine = this.lines.pop()
127
- this.isFirstLine = this.lines.length === 0
128
- this.prompt()
63
+ // 非空行:添加到 lines,继续输入
64
+ lines.push(input)
65
+ question(false)
66
+ })
129
67
  }
130
- return
131
- }
132
-
133
- // 普通字符
134
- if (str && !key.ctrl && !key.meta) {
135
- // 直接添加到当前行
136
- this.currentLine += str
137
- this.prompt()
138
- }
139
- }
140
-
141
- /**
142
- * 处理粘贴
143
- */
144
- handlePaste() {
145
- if (this.pasteBuffer.includes('\n')) {
146
- const allLines = this.pasteBuffer.split('\n')
147
- this.lines.push(...allLines.slice(0, -1))
148
- this.currentLine = allLines[allLines.length - 1]
149
- this.isFirstLine = false
150
- this.pasteId++
151
68
 
152
- console.log(`\n${CLEAR_LINE}${colored(`[Pasted text #${this.pasteId} +${this.lines.length + 1} lines]`, CYAN)}`)
153
- } else {
154
- this.currentLine = this.pasteBuffer
155
- this.lines = []
156
- }
157
- this.pasteBuffer = ''
69
+ question(true)
70
+ })
158
71
  }
159
72
 
160
73
  /**
161
- * 处理发送
74
+ * 提示用户输入
162
75
  */
163
- async handleSubmit() {
164
- const now = Date.now()
165
- const content = this.lines.length > 0
166
- ? [...this.lines, this.currentLine].join('\n')
167
- : this.currentLine
168
-
169
- const trimmed = content.trim()
170
-
171
- // 检测连续两次空回车(多行输入结束)
172
- if (this.currentLine.trim() === '' && this.lines.length > 0) {
173
- if (now - this.lastEnterTime < this.LINE_ENDING_INTERVAL) {
174
- // 第二次空回车,结束输入
175
- const finalContent = this.lines.join('\n').trim()
176
- this.resetInput()
177
- console.log()
178
-
179
- if (!finalContent) {
180
- this.prompt()
181
- return
182
- }
76
+ async promptUser() {
77
+ try {
78
+ const input = await this.getMultilineInput()
183
79
 
184
- await this.sendMessage(finalContent)
80
+ if (!input) {
81
+ await this.promptUser()
185
82
  return
186
83
  }
187
- }
188
- this.lastEnterTime = now
189
-
190
- // 输入 !! 立即结束多行输入
191
- if (this.currentLine.trim() === '!!') {
192
- const finalContent = this.lines.join('\n').trim()
193
- this.resetInput()
194
- console.log()
195
84
 
196
- if (finalContent) {
197
- await this.sendMessage(finalContent)
198
- } else {
199
- this.prompt()
85
+ // 退出命令
86
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
87
+ console.log('再见!')
88
+ this.rl.close()
89
+ return
200
90
  }
201
- return
202
- }
203
91
 
204
- // 显示空行
205
- console.log()
92
+ // 发送消息
93
+ await this.sendMessage(input)
206
94
 
207
- // 退出命令
208
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
209
- console.log('再见!')
210
- if (process.stdin.isTTY) {
211
- process.stdin.setRawMode(false)
212
- }
213
- process.exit(0)
214
- return
215
- }
216
-
217
- if (!trimmed) {
218
- this.resetInput()
219
- this.prompt()
220
- return
95
+ // 继续等待下一条消息
96
+ await this.promptUser()
97
+ } catch (err) {
98
+ console.error(`\n${colored('[错误]', RED)} ${err.message}`)
99
+ await this.promptUser()
221
100
  }
222
-
223
- // 重置输入
224
- this.resetInput()
225
-
226
- // 发送消息
227
- await this.sendMessage(trimmed)
228
- }
229
-
230
- /**
231
- * 重置输入状态
232
- */
233
- resetInput() {
234
- this.currentLine = ''
235
- this.lines = []
236
- this.isFirstLine = true
237
- this.pasteId = 0
238
- this.isPasting = false
239
- this.pasteBuffer = ''
240
- this.lastEnterTime = 0
241
101
  }
242
102
 
243
103
  /**
244
104
  * 发送消息并显示响应
245
105
  */
246
106
  async sendMessage(message) {
247
- console.log(colored('Agent:', GREEN))
248
107
  console.log()
249
108
 
250
109
  // 用于打断的标志
@@ -263,43 +122,18 @@ class ChatUI extends EventEmitter {
263
122
 
264
123
  try {
265
124
  let lineBuffer = ''
266
- // 渲染状态追踪
267
125
  const renderState = { inThink: false, inCodeBlock: false }
268
126
 
269
- // 检查是否还有孤立的代理对
270
- const hasOrphanedSurrogate = (str) => {
271
- if (!str || str.length === 0) return false
272
- let i = 0
273
- while (i < str.length) {
274
- const code = str.charCodeAt(i)
275
- if (code >= 0xD800 && code <= 0xDBFF) {
276
- const nextCode = str.charCodeAt(i + 1)
277
- if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
278
- i += 2
279
- continue
280
- }
281
- return true // 孤立的高代理
282
- }
283
- i++
284
- }
285
- return false
286
- }
127
+ console.log(colored('Agent:', GREEN))
128
+ console.log()
287
129
 
288
130
  for await (const chunk of this.agent.chatStream(message)) {
289
- // 检查是否被打断
290
131
  if (interrupted) break
291
132
 
292
133
  if (chunk.type === 'text') {
293
134
  lineBuffer += chunk.text
294
135
 
295
- // 如果有孤立代理对,等待下一个chunk补充
296
- if (hasOrphanedSurrogate(lineBuffer)) {
297
- continue
298
- }
299
-
300
- // 当有一行完整内容时,渲染并输出
301
136
  while (lineBuffer.includes('\n')) {
302
- // 检查是否被打断
303
137
  if (interrupted) break
304
138
 
305
139
  const nlIndex = lineBuffer.indexOf('\n')
@@ -316,7 +150,6 @@ class ChatUI extends EventEmitter {
316
150
  }
317
151
  }
318
152
 
319
- // 输出剩余内容
320
153
  if (lineBuffer.trim() && !interrupted) {
321
154
  console.log(renderLine(lineBuffer, renderState))
322
155
  }
@@ -325,11 +158,8 @@ class ChatUI extends EventEmitter {
325
158
  console.error(`\n${colored('[错误]', RED)} ${err.message}\n`)
326
159
  }
327
160
  } finally {
328
- // 移除监听器
329
161
  process.removeListener('SIGINT', interruptHandler)
330
162
  }
331
-
332
- this.prompt()
333
163
  }
334
164
  }
335
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "bin": {