typeclaw 0.25.0 → 0.27.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/package.json +1 -1
- package/src/agent/session-origin.ts +36 -5
- package/src/agent/subagent-completion-reminder.ts +16 -1
- package/src/agent/tools/channel-react.ts +11 -4
- package/src/bundled-plugins/reviewer/skills/code-review.ts +3 -1
- package/src/channels/adapters/discord-bot-classify.ts +3 -0
- package/src/channels/adapters/discord-bot-reactions.ts +164 -0
- package/src/channels/adapters/discord-bot.ts +23 -0
- package/src/channels/adapters/github/inbound.ts +60 -13
- package/src/channels/adapters/github/review-thread-resolver.ts +28 -3
- package/src/channels/adapters/slack-bot-classify.ts +2 -0
- package/src/channels/adapters/slack-bot-reactions.ts +167 -0
- package/src/channels/adapters/slack-bot.ts +24 -0
- package/src/channels/router.ts +191 -7
- package/src/channels/schema.ts +41 -0
- package/src/cli/inspect.ts +216 -36
- package/src/cli/logs.ts +15 -0
- package/src/cli/tui.ts +33 -39
- package/src/compose/logs.ts +1 -1
- package/src/config/config.ts +43 -2
- package/src/container/logs.ts +70 -22
- package/src/init/index.ts +3 -3
- package/src/inspect/index.ts +128 -42
- package/src/inspect/item-list.ts +44 -0
- package/src/inspect/item.ts +17 -0
- package/src/inspect/label.ts +1 -1
- package/src/inspect/logs-item.ts +79 -0
- package/src/inspect/loop.ts +74 -3
- package/src/inspect/open-item.ts +100 -0
- package/src/inspect/preview.ts +106 -0
- package/src/inspect/session-list.ts +15 -3
- package/src/inspect/transcript-view.ts +182 -0
- package/src/inspect/tui-item.ts +97 -0
- package/src/skills/typeclaw-channel-github/SKILL.md +4 -2
- package/src/tui/index.ts +72 -32
- package/typeclaw.schema.json +1 -0
package/src/tui/index.ts
CHANGED
|
@@ -50,13 +50,21 @@ export type TuiOptions = {
|
|
|
50
50
|
onVersionMismatch?: (info: VersionMismatch) => void
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Outcome of a single `run()` cycle.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
53
|
+
// Outcome of a single `run()` cycle.
|
|
54
|
+
// - 'detach': idle Esc — return to the session-viewer list. Closing the WS
|
|
55
|
+
// ends the server-side AgentSession (accepted; the list re-shows it as a
|
|
56
|
+
// read-only transcript).
|
|
57
|
+
// - 'exit': deliberate /quit or Ctrl+C — terminate the client.
|
|
58
|
+
// - 'lostConnection': WS closed AFTER the handshake without a deliberate
|
|
59
|
+
// quit/detach — exactly the self-restart case, and the only one where a
|
|
60
|
+
// fresh connect can recover the session.
|
|
61
|
+
// - 'connectFailed': pre-handshake connect/handshake error.
|
|
62
|
+
// The CLI reconnect loop spins only on 'lostConnection'.
|
|
63
|
+
export type TuiRunResult =
|
|
64
|
+
| { reason: 'detach' }
|
|
65
|
+
| { reason: 'exit'; exitCode: number }
|
|
66
|
+
| { reason: 'lostConnection' }
|
|
67
|
+
| { reason: 'connectFailed' }
|
|
60
68
|
|
|
61
69
|
export function createTui({
|
|
62
70
|
url,
|
|
@@ -68,7 +76,7 @@ export function createTui({
|
|
|
68
76
|
expectedVersion,
|
|
69
77
|
onVersionMismatch,
|
|
70
78
|
}: TuiOptions) {
|
|
71
|
-
async function run(): Promise<
|
|
79
|
+
async function run(): Promise<TuiRunResult> {
|
|
72
80
|
const terminal = createTerminal()
|
|
73
81
|
const tui = new TUI(terminal)
|
|
74
82
|
const displayUrl = redactUrl(url)
|
|
@@ -78,13 +86,19 @@ export function createTui({
|
|
|
78
86
|
tui.start()
|
|
79
87
|
tui.requestRender()
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
// Pre-handshake failures resolve 'connectFailed' (not throw): the standalone
|
|
90
|
+
// CLI injects exit=process.exit so exit(1) ends the process and the return is
|
|
91
|
+
// moot; the viewer injects a no-op exit so run() resolves cleanly and the
|
|
92
|
+
// caller maps connectFailed into an error result instead of an uncaught reject.
|
|
93
|
+
const maybeClient = await createClient(url).catch((err) => {
|
|
82
94
|
status.setText(colors.red(`connection error: ${err instanceof Error ? err.message : String(err)}`))
|
|
83
95
|
tui.requestRender()
|
|
84
96
|
tui.stop()
|
|
85
97
|
exit(1)
|
|
86
|
-
|
|
98
|
+
return null
|
|
87
99
|
})
|
|
100
|
+
if (maybeClient === null) return { reason: 'connectFailed' }
|
|
101
|
+
const client = maybeClient
|
|
88
102
|
|
|
89
103
|
const handshake = await waitForConnected(client, displayUrl, handshakeTimeoutMs).catch((err) => {
|
|
90
104
|
status.setText(colors.red(`connection error: ${err instanceof Error ? err.message : String(err)}`))
|
|
@@ -92,10 +106,10 @@ export function createTui({
|
|
|
92
106
|
client.close()
|
|
93
107
|
tui.stop()
|
|
94
108
|
exit(1)
|
|
95
|
-
|
|
109
|
+
return null
|
|
96
110
|
})
|
|
111
|
+
if (handshake === null) return { reason: 'connectFailed' }
|
|
97
112
|
|
|
98
|
-
let userInitiatedShutdown = false
|
|
99
113
|
const { sessionId, serverVersion } = handshake
|
|
100
114
|
status.setText(colors.dim(`session: ${sessionId}`))
|
|
101
115
|
tui.requestRender()
|
|
@@ -231,12 +245,23 @@ export function createTui({
|
|
|
231
245
|
}
|
|
232
246
|
})
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
let settleOutcome: ((result: TuiRunResult) => void) | null = null
|
|
249
|
+
const outcome = new Promise<TuiRunResult>((resolve) => {
|
|
250
|
+
settleOutcome = resolve
|
|
251
|
+
})
|
|
252
|
+
const settle = (result: TuiRunResult): void => {
|
|
253
|
+
if (settleOutcome === null) return
|
|
254
|
+
const fn = settleOutcome
|
|
255
|
+
settleOutcome = null
|
|
256
|
+
fn(result)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
client.onClose(() => {
|
|
260
|
+
appendHistory(new Text(colors.dim('disconnected'), 0, 0))
|
|
261
|
+
tui.requestRender()
|
|
262
|
+
// A user-initiated detach/exit already closed the WS deliberately and
|
|
263
|
+
// settled the outcome; onClose then fires but must not override it.
|
|
264
|
+
settle({ reason: 'lostConnection' })
|
|
240
265
|
})
|
|
241
266
|
|
|
242
267
|
function send(text: string): Promise<void> {
|
|
@@ -249,7 +274,7 @@ export function createTui({
|
|
|
249
274
|
|
|
250
275
|
function runTuiCommand(command: TuiCommandName): boolean {
|
|
251
276
|
if (command === 'quit') {
|
|
252
|
-
|
|
277
|
+
exitWith(0)
|
|
253
278
|
return true
|
|
254
279
|
}
|
|
255
280
|
if (command === 'reload') {
|
|
@@ -266,29 +291,44 @@ export function createTui({
|
|
|
266
291
|
return true
|
|
267
292
|
}
|
|
268
293
|
|
|
269
|
-
// Esc
|
|
270
|
-
//
|
|
294
|
+
// Esc means "abort the in-flight reply" while a turn is generating, and
|
|
295
|
+
// "detach back to the session list" when idle. The Editor does not bind
|
|
296
|
+
// Esc, so a top-level listener intercepts it without fighting the editor.
|
|
271
297
|
tui.addInputListener((data) => {
|
|
272
|
-
if (matchesKey(data, Key.escape)
|
|
298
|
+
if (!matchesKey(data, Key.escape)) return undefined
|
|
299
|
+
if (replyInFlight) {
|
|
273
300
|
client.send({ type: 'abort' })
|
|
274
301
|
return { consume: true }
|
|
275
302
|
}
|
|
276
|
-
|
|
303
|
+
detach()
|
|
304
|
+
return { consume: true }
|
|
277
305
|
})
|
|
278
306
|
|
|
279
|
-
|
|
280
|
-
|
|
307
|
+
// Settle BEFORE closing the client: client.close() fires onClose, which
|
|
308
|
+
// settles 'lostConnection'. settle() is idempotent, so the first call wins —
|
|
309
|
+
// settling the deliberate outcome first keeps the later onClose a no-op.
|
|
310
|
+
const teardown = (): void => {
|
|
281
311
|
tui.stop()
|
|
282
312
|
client.close()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const exitWith = (code: number): void => {
|
|
316
|
+
settle({ reason: 'exit', exitCode: code })
|
|
317
|
+
teardown()
|
|
283
318
|
exit(code)
|
|
284
319
|
}
|
|
285
320
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
321
|
+
const detach = (): void => {
|
|
322
|
+
settle({ reason: 'detach' })
|
|
323
|
+
teardown()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Ctrl+C exits the client. In raw mode the kernel does NOT generate SIGINT,
|
|
327
|
+
// so we intercept the \x03 byte ourselves; the Editor would otherwise
|
|
328
|
+
// swallow it. teardown() restores raw-mode/cursor/echo before we settle.
|
|
289
329
|
tui.addInputListener((data) => {
|
|
290
330
|
if (matchesKey(data, Key.ctrl('c'))) {
|
|
291
|
-
|
|
331
|
+
exitWith(0)
|
|
292
332
|
return { consume: true }
|
|
293
333
|
}
|
|
294
334
|
return undefined
|
|
@@ -330,15 +370,15 @@ export function createTui({
|
|
|
330
370
|
const command = parseBareTuiCommand(initialPrompt)
|
|
331
371
|
if (command !== null) {
|
|
332
372
|
runTuiCommand(command)
|
|
333
|
-
if (command === 'quit') return {
|
|
373
|
+
if (command === 'quit') return { reason: 'exit', exitCode: 0 }
|
|
334
374
|
} else {
|
|
335
375
|
await send(initialPrompt)
|
|
336
376
|
}
|
|
337
377
|
}
|
|
338
378
|
|
|
339
|
-
const
|
|
379
|
+
const result = await outcome
|
|
340
380
|
tui.stop()
|
|
341
|
-
return
|
|
381
|
+
return result
|
|
342
382
|
}
|
|
343
383
|
|
|
344
384
|
return { run }
|
package/typeclaw.schema.json
CHANGED