typeclaw 0.20.0 → 0.21.0

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/src/tui/index.ts CHANGED
@@ -11,19 +11,25 @@ export type TerminalFactory = () => Terminal
11
11
 
12
12
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000
13
13
 
14
- // Bare slash-command names (no leading `/`) the TUI intercepts client-side and
15
- // turns into a clean process exit. The hatching ritual tells the agent to point
16
- // users at `/quit` (see src/init/hatching.ts); without an intercept the literal
17
- // text would be shipped to the LLM as a chat message. Grammar (case-insensitive,
18
- // whitespace-tolerant, `//foo` escapes to a literal prompt) comes from
19
- // `parseCommand` in src/commands so channel and TUI slash commands stay
20
- // consistent. Arguments after the name disqualify the match: `/quit me a story`
21
- // is a real prompt, not a command.
14
+ // Bare slash-command names (no leading `/`) the TUI intercepts client-side.
15
+ // The hatching ritual tells the agent to point users at `/quit` (see
16
+ // src/init/hatching.ts); without an intercept the literal text would be shipped
17
+ // to the LLM as a chat message. Grammar (case-insensitive, whitespace-tolerant,
18
+ // `//foo` escapes to a literal prompt) comes from `parseCommand` in
19
+ // src/commands so channel and TUI slash commands stay consistent. Arguments
20
+ // after the name disqualify the match: `/quit me a story` is a real prompt, not
21
+ // a command.
22
22
  const QUIT_COMMAND_NAMES: ReadonlySet<string> = new Set(['quit', 'exit'])
23
+ const TUI_COMMAND_NAMES: ReadonlySet<TuiCommandName> = new Set(['quit', 'reload', 'restart'])
23
24
 
24
- function isQuitCommand(text: string): boolean {
25
+ type TuiCommandName = 'quit' | 'reload' | 'restart'
26
+
27
+ function parseBareTuiCommand(text: string): TuiCommandName | null {
25
28
  const parsed = parseCommand(text)
26
- return parsed !== null && parsed.args.length === 0 && QUIT_COMMAND_NAMES.has(parsed.name)
29
+ if (parsed === null || parsed.args.length > 0) return null
30
+ if (QUIT_COMMAND_NAMES.has(parsed.name)) return 'quit'
31
+ if (TUI_COMMAND_NAMES.has(parsed.name as TuiCommandName)) return parsed.name as TuiCommandName
32
+ return null
27
33
  }
28
34
 
29
35
  export type VersionMismatch = { expected: string; actual: string }
@@ -203,6 +209,25 @@ export function createTui({
203
209
  updateQueuePanel(msg.pending)
204
210
  break
205
211
  }
212
+ case 'reload_result': {
213
+ for (const result of msg.results) {
214
+ const text = result.ok
215
+ ? `${colors.green('●')} ${colors.bold(`[${result.scope}]`)} ${result.summary}`
216
+ : `${colors.red('●')} ${colors.bold(`[${result.scope}]`)} ${result.reason}`
217
+ appendHistory(new Text(text, 0, 0))
218
+ }
219
+ tui.requestRender()
220
+ break
221
+ }
222
+ case 'restart_result': {
223
+ const text =
224
+ msg.status === 'accepted'
225
+ ? colors.green(colors.dim(msg.message ?? 'restart scheduled; reconnecting when the new container is up'))
226
+ : colors.red(`restart failed: ${msg.error ?? 'unknown error'}`)
227
+ appendHistory(new Text(text, 0, 0))
228
+ tui.requestRender()
229
+ break
230
+ }
206
231
  }
207
232
  })
208
233
 
@@ -222,6 +247,25 @@ export function createTui({
222
247
  })
223
248
  }
224
249
 
250
+ function runTuiCommand(command: TuiCommandName): boolean {
251
+ if (command === 'quit') {
252
+ shutdown(0)
253
+ return true
254
+ }
255
+ if (command === 'reload') {
256
+ client.send({ type: 'reload' })
257
+ appendHistory(new Text(colors.dim('reloading...'), 0, 0))
258
+ tui.requestRender()
259
+ return true
260
+ }
261
+ client.send({ type: 'restart' })
262
+ appendHistory(
263
+ new Text(colors.yellow(colors.dim('restart requested... reconnecting when the new container is up')), 0, 0),
264
+ )
265
+ tui.requestRender()
266
+ return true
267
+ }
268
+
225
269
  // Esc aborts an in-flight reply. The Editor does not bind Esc, so a
226
270
  // top-level input listener can intercept it without fighting the editor.
227
271
  tui.addInputListener((data) => {
@@ -252,8 +296,13 @@ export function createTui({
252
296
 
253
297
  editor.onSubmit = (text) => {
254
298
  if (text.trim().length === 0) return
255
- if (isQuitCommand(text)) {
256
- shutdown(0)
299
+ const command = parseBareTuiCommand(text)
300
+ if (command !== null) {
301
+ if (command !== 'quit') {
302
+ editor.setText('')
303
+ editor.addToHistory(text)
304
+ }
305
+ runTuiCommand(command)
257
306
  return
258
307
  }
259
308
  editor.setText('')
@@ -275,13 +324,16 @@ export function createTui({
275
324
 
276
325
  if (initialPrompt) {
277
326
  // initialPrompt bypasses editor.onSubmit, so the quit intercept above
278
- // would never run. Guard the same way so `typeclaw tui /quit` exits
279
- // instead of leaking the command into the agent's chat context.
280
- if (isQuitCommand(initialPrompt)) {
281
- shutdown(0)
282
- return { lostConnection: false }
327
+ // would never run. Guard the same way so `typeclaw tui /quit` exits
328
+ // and `/reload` / `/restart` stay websocket control frames instead of
329
+ // leaking the command into the agent's chat context.
330
+ const command = parseBareTuiCommand(initialPrompt)
331
+ if (command !== null) {
332
+ runTuiCommand(command)
333
+ if (command === 'quit') return { lostConnection: false }
334
+ } else {
335
+ await send(initialPrompt)
283
336
  }
284
- await send(initialPrompt)
285
337
  }
286
338
 
287
339
  const lostConnection = await closed