kimaki 1.3.0 → 1.5.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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +13 -6
- package/dist/bot.js +17 -15
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +72 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +33 -18
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +5 -6
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.d.ts +1 -0
- package/dist/coordinator-agents.e2e.test.js +113 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.d.ts +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.d.ts +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +78 -3
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +2 -3
- package/dist/event-loop.js +74 -44
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +47 -12
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +83 -31
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +6 -3
- package/dist/main.js +14 -4
- package/dist/main.js.map +1 -1
- package/dist/mainmast-idle.e2e.test.d.ts +1 -0
- package/dist/mainmast-idle.e2e.test.js +99 -0
- package/dist/mainmast-idle.e2e.test.js.map +1 -0
- package/dist/mainmast-queue.e2e.test.d.ts +1 -0
- package/dist/mainmast-queue.e2e.test.js +244 -0
- package/dist/mainmast-queue.e2e.test.js.map +1 -0
- package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
- package/dist/mainmast-warmup.e2e.test.js +130 -0
- package/dist/mainmast-warmup.e2e.test.js.map +1 -0
- package/dist/mainmast.d.ts +6 -0
- package/dist/mainmast.e2e.test.d.ts +1 -0
- package/dist/mainmast.e2e.test.js +428 -0
- package/dist/mainmast.e2e.test.js.map +1 -0
- package/dist/mainmast.js +156 -0
- package/dist/mainmast.js.map +1 -0
- package/dist/markdown/markdown.test.js +14 -4
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +9 -6
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +93 -61
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/mainmast.d.ts +4 -0
- package/dist/plugin/mainmast.js +379 -0
- package/dist/plugin/mainmast.js.map +1 -0
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/prompt.d.ts +22 -9
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +3 -3
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +152 -6
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +18 -6
- package/dist/session-events.js +175 -27
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +63 -12
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +44 -21
- package/dist/sessions.js +177 -65
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +6 -4
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +2 -1
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +5 -15
- package/dist/system-prompt.js +180 -131
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.d.ts +3 -0
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -2
- package/dist/test/harness.js +51 -11
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -8
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -6
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +109 -50
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +190 -48
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +72 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +11 -5
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +36 -19
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +73 -1
- package/src/builtin-skills.ts +33 -0
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +30 -19
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +145 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -45
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +44 -11
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +26 -10
- package/src/markdown/markdown.test.ts +15 -4
- package/src/markdown/render-markdown.ts +9 -6
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +86 -59
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +168 -25
- package/src/session-ops.e2e.test.ts +64 -11
- package/src/sessions.ts +217 -67
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +6 -4
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +190 -134
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -17
- package/src/test/replay.ts +12 -7
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +199 -49
- package/src/thread-reducer.ts +135 -66
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +93 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/src/message-tool.e2e.test.ts +0 -45
package/src/thread-reducer.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// root text, tool lines, banner, footer, retries, errors
|
|
14
14
|
// child foreground: tool lines labelled "┣ general ⋅ glob ..."
|
|
15
|
-
// background: no tool lines, one "
|
|
15
|
+
// background: no tool lines, one "general finished: ..." line
|
|
16
16
|
// never text, banner or footer
|
|
17
17
|
//
|
|
18
18
|
// busy = root execution running OR any child running (29.2 #3). Typing
|
|
@@ -36,9 +36,12 @@ import {
|
|
|
36
36
|
executeCalls,
|
|
37
37
|
formatExecuteFailures,
|
|
38
38
|
buttonRow,
|
|
39
|
+
formatAgentSwitched,
|
|
39
40
|
formatBanner,
|
|
40
41
|
formatCacheMiss,
|
|
42
|
+
formatCompacted,
|
|
41
43
|
formatError,
|
|
44
|
+
formatModelSwitched,
|
|
42
45
|
formatRetry,
|
|
43
46
|
formatShellEnded,
|
|
44
47
|
formatShellFinished,
|
|
@@ -52,10 +55,10 @@ import {
|
|
|
52
55
|
type ToolInput,
|
|
53
56
|
type UiMessage,
|
|
54
57
|
} from './format-parts.ts'
|
|
58
|
+
import { IDLE_REPLY } from './plugin/coordinator.ts'
|
|
55
59
|
import { closePermission, hydratePermissions, showPermission, STATUS, type PendingPermission } from './permissions.ts'
|
|
56
60
|
import { closeForm, formatAnswer, hydrateForms, showForm, withAnswer, type PendingForm } from './questions.ts'
|
|
57
61
|
import { cancelQueued, deliverQueued, enqueueInput, hydrateQueue, promoteQueued, queuedItems, type PendingInput } from './queue.ts'
|
|
58
|
-
import { MESSAGE_TOOL } from './system-prompt.ts'
|
|
59
62
|
|
|
60
63
|
export type Turn = {
|
|
61
64
|
startedAt: number
|
|
@@ -65,14 +68,10 @@ export type Turn = {
|
|
|
65
68
|
tokens: number
|
|
66
69
|
// `kimaki sleep` ran in this turn: the session wakes later, so the footer is silent.
|
|
67
70
|
slept: boolean
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function newTurn(at: number): Turn {
|
|
75
|
-
return { startedAt: at, model: null, agent: null, tokens: 0, slept: false, messaged: false, unsent: null }
|
|
71
|
+
// Discord message the next answer text replies to (a coordinator prompt, queue.ts). Taken by the first text.
|
|
72
|
+
replyTo: string | null
|
|
73
|
+
// The current answer posted model text. A coordinator answer without it gets no footer.
|
|
74
|
+
texted: boolean
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
export type Child = {
|
|
@@ -104,6 +103,8 @@ export type ThreadView = {
|
|
|
104
103
|
sessionId: string
|
|
105
104
|
// Project channel of the thread: its verbosity applies.
|
|
106
105
|
channelId: string
|
|
106
|
+
// The view is a coordinator channel itself, not a thread.
|
|
107
|
+
coordinator: boolean
|
|
107
108
|
// Current working directory of the root session (session.moved changes it).
|
|
108
109
|
directory: string
|
|
109
110
|
// True only for sessions this bot created: the first step shows a banner.
|
|
@@ -116,16 +117,19 @@ export type ThreadView = {
|
|
|
116
117
|
children: Readonly<Record<string, Child>>
|
|
117
118
|
// Background shells started by tool calls: shell ID -> label, until their job ends.
|
|
118
119
|
shells: Readonly<Record<string, string>>
|
|
119
|
-
// Blank line between text and tool blocks.
|
|
120
|
+
// Blank line between text and tool blocks. null: the current answer posted nothing yet (no footer).
|
|
120
121
|
lastKind: 'text' | 'tool' | null
|
|
121
122
|
lastRetryAt: number | null
|
|
122
123
|
// Prompt tokens the provider holds in cache after the last root step, and when that step ended.
|
|
123
124
|
cached: { tokens: number; at: number } | null
|
|
125
|
+
// The root context was compacted: the next root step (or the execution end) posts the new size.
|
|
126
|
+
compacted: boolean
|
|
124
127
|
// Root inbox: user items not delivered yet, in order (queuedItems() for the queue).
|
|
125
128
|
inbox: readonly PendingInput[]
|
|
126
129
|
// Kimaki runs an OpenCode command (kimaki.command): the next root user item
|
|
127
130
|
// without Discord metadata is its expanded prompt, shown as the typed command.
|
|
128
|
-
|
|
131
|
+
// `echo` is set when no Discord message shows the command (CLI, routines).
|
|
132
|
+
commandPending: { text: string; username: string; messageId: string | null; echo: string | null } | null
|
|
129
133
|
// Questions and permission requests waiting for the user (root and children).
|
|
130
134
|
forms: Readonly<Record<string, PendingForm>>
|
|
131
135
|
permissions: Readonly<Record<string, PendingPermission>>
|
|
@@ -136,19 +140,22 @@ export type UploadFile = { path: string; name: string; size: number }
|
|
|
136
140
|
export type Effect =
|
|
137
141
|
// Bot-formatted Discord content (tool lines, banner, errors).
|
|
138
142
|
// Effects are silent unless `notify` is set (format-parts.ts message flags).
|
|
139
|
-
|
|
143
|
+
// `echo`: user text (a prompt echo), posted with mentions disabled.
|
|
144
|
+
| { type: 'send'; text: string; notify?: boolean; echo?: boolean }
|
|
140
145
|
// Model markdown, rendered and split by the executor.
|
|
141
|
-
|
|
146
|
+
// `replyTo`: the first message replies to this Discord message.
|
|
147
|
+
| { type: 'markdown'; text: string; blankLineBefore: boolean; replyTo: string | null }
|
|
142
148
|
| { type: 'typing'; on: boolean }
|
|
143
149
|
// Files from `kimaki upload-to-discord` (never from the reducer). `settle`
|
|
144
|
-
// answers the waiting CLI call:
|
|
145
|
-
| { type: 'attachments'; files: readonly UploadFile[]; settle: (
|
|
146
|
-
// The executor adds
|
|
147
|
-
| { type: 'footer'; directory: string; durationMs: number; contextPercent: number | null; model: ModelRef; agent: string | null; notify: boolean }
|
|
148
|
-
// Posts the messages in order; the first replies to `replyTo` when set.
|
|
149
|
-
| { type: 'show'; key: string; messages: readonly UiMessage[]; replyTo: string | null; notify: boolean }
|
|
150
|
-
// Edits the messages `show` posted under `key` (by index; the last one covers the rest)
|
|
151
|
-
|
|
150
|
+
// answers the waiting CLI call: the Discord URL of each file once posted, or why not.
|
|
151
|
+
| { type: 'attachments'; files: readonly UploadFile[]; settle: (result: Error | string[]) => void }
|
|
152
|
+
// The executor adds the git branch of `directory` and the defaults of `channelId` when it posts.
|
|
153
|
+
| { type: 'footer'; channelId: string; directory: string; durationMs: number; contextPercent: number | null; model: ModelRef; agent: string | null; notify: boolean }
|
|
154
|
+
// Posts the messages in order; the first replies to `replyTo` when set. `echo`: holds user text, no mentions.
|
|
155
|
+
| { type: 'show'; key: string; messages: readonly UiMessage[]; replyTo: string | null; notify: boolean; echo?: boolean }
|
|
156
|
+
// Edits the messages `show` posted under `key` (by index; the last one covers the rest),
|
|
157
|
+
// then posts `reply` as a reply to `replyTo`, or to the first of them when null.
|
|
158
|
+
| { type: 'edit'; key: string; messages: readonly UiMessage[]; reply?: string; replyTo?: string | null }
|
|
152
159
|
// Marks the root execution that went idle at `idle` as read in OpenCode, once
|
|
153
160
|
// the lines before it are posted. Bots cannot see Discord read state.
|
|
154
161
|
| { type: 'viewed'; sessionId: string; idle: number }
|
|
@@ -174,7 +181,7 @@ export type KimakiEvent =
|
|
|
174
181
|
// Kimaki is about to run an OpenCode command (prompt.ts). Prompts without
|
|
175
182
|
// Discord metadata are echoed as sent from elsewhere; this one must not be.
|
|
176
183
|
// TODO: pass metadata.discord to session.command once its payload accepts metadata, then drop this event.
|
|
177
|
-
| { type: 'kimaki.command'; text: string; username: string }
|
|
184
|
+
| { type: 'kimaki.command'; text: string; username: string; messageId: string | null; echo: string | null }
|
|
178
185
|
// session.command failed, so no prompt follows.
|
|
179
186
|
| { type: 'kimaki.command-failed' }
|
|
180
187
|
// A session found by walking parentID after a bot restart.
|
|
@@ -193,14 +200,13 @@ export function isKimakiEvent(event: ThreadEvent): event is KimakiEvent {
|
|
|
193
200
|
|
|
194
201
|
export type Prefs = {
|
|
195
202
|
verbosity: Verbosity
|
|
196
|
-
// "providerID/modelID" -> context window size in tokens.
|
|
197
|
-
|
|
198
|
-
// `kimaki --message-tool`: Discord hides root plain text (kept for the TUI), only send_message shows.
|
|
199
|
-
messageTool: boolean
|
|
203
|
+
// "providerID/modelID" -> display name and context window size in tokens.
|
|
204
|
+
models: Readonly<Record<string, { name: string; context: number }>>
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
const RETRY_NOTICE_INTERVAL_MS = 10_000
|
|
203
208
|
const CACHE_MISS_SLACK = 1_024
|
|
209
|
+
const CACHE_MISS_MIN_WRITE_SHARE = 0.5
|
|
204
210
|
const REPLAY_LIMIT = 30
|
|
205
211
|
|
|
206
212
|
export function emptyView({
|
|
@@ -208,15 +214,18 @@ export function emptyView({
|
|
|
208
214
|
channelId,
|
|
209
215
|
directory,
|
|
210
216
|
isNew,
|
|
217
|
+
coordinator,
|
|
211
218
|
}: {
|
|
212
219
|
sessionId: string
|
|
213
220
|
channelId: string
|
|
214
221
|
directory: string
|
|
215
222
|
isNew: boolean
|
|
223
|
+
coordinator: boolean
|
|
216
224
|
}): ThreadView {
|
|
217
225
|
return {
|
|
218
226
|
sessionId,
|
|
219
227
|
channelId,
|
|
228
|
+
coordinator,
|
|
220
229
|
directory,
|
|
221
230
|
bannerPending: isNew,
|
|
222
231
|
turn: null,
|
|
@@ -227,6 +236,7 @@ export function emptyView({
|
|
|
227
236
|
lastKind: null,
|
|
228
237
|
lastRetryAt: null,
|
|
229
238
|
cached: null,
|
|
239
|
+
compacted: false,
|
|
230
240
|
inbox: [],
|
|
231
241
|
commandPending: null,
|
|
232
242
|
forms: {},
|
|
@@ -234,13 +244,37 @@ export function emptyView({
|
|
|
234
244
|
}
|
|
235
245
|
}
|
|
236
246
|
|
|
247
|
+
function newTurn(startedAt: number): Turn {
|
|
248
|
+
return { startedAt, model: null, agent: null, tokens: 0, slept: false, replyTo: null, texted: false }
|
|
249
|
+
}
|
|
250
|
+
|
|
237
251
|
export function isBusy(view: ThreadView): boolean {
|
|
238
252
|
return view.turn !== null || Object.values(view.children).some((child) => child.running)
|
|
239
253
|
}
|
|
240
254
|
|
|
255
|
+
function waitsForUser(view: ThreadView): boolean {
|
|
256
|
+
return Object.keys(view.forms).length > 0 || Object.keys(view.permissions).length > 0
|
|
257
|
+
}
|
|
258
|
+
|
|
241
259
|
// Typing stops while the agent waits for the user (spec 6.6).
|
|
242
260
|
function isTyping(view: ThreadView): boolean {
|
|
243
|
-
return isBusy(view) &&
|
|
261
|
+
return isBusy(view) && !waitsForUser(view)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Typing of one conversation across all views: a coordinator (a view whose channel
|
|
265
|
+
// is its own conversation) also types while a thread of its channel does, so
|
|
266
|
+
// background workers show there. Other conversations: isTyping of their view.
|
|
267
|
+
export function conversationTyping({ threads, threadId }: { threads: Readonly<Record<string, ThreadView>>; threadId: string }): boolean {
|
|
268
|
+
const view = threads[threadId]
|
|
269
|
+
if (!view || waitsForUser(view)) return false
|
|
270
|
+
if (isBusy(view)) return true
|
|
271
|
+
if (!view.coordinator) return false
|
|
272
|
+
return workersTyping({ threads, channelId: threadId })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// A thread of the channel runs and does not wait for the user.
|
|
276
|
+
export function workersTyping({ threads, channelId }: { threads: Readonly<Record<string, ThreadView>>; channelId: string }): boolean {
|
|
277
|
+
return Object.entries(threads).some(([id, thread]) => id !== channelId && thread.channelId === channelId && isTyping(thread))
|
|
244
278
|
}
|
|
245
279
|
|
|
246
280
|
export function eventSessionId(event: V2Event): string | null {
|
|
@@ -283,27 +317,41 @@ function toolLine({ draft, emit }: Context, text: string) {
|
|
|
283
317
|
draft.lastKind = 'tool'
|
|
284
318
|
}
|
|
285
319
|
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
320
|
+
function showCompacted(context: Context, size: { tokens: number | null; percent: number | null }) {
|
|
321
|
+
context.draft.compacted = false
|
|
322
|
+
toolLine(context, formatCompacted(size))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// A compaction in an execution with no model step (`/compact`, the idle coordinator compaction) stays silent:
|
|
326
|
+
// the user did not ask for anything, so the line is only noise. Auto compaction in a turn shows.
|
|
327
|
+
function endCompaction(context: Context) {
|
|
328
|
+
if (context.draft.compacted && context.draft.turn?.model) showCompacted(context, { tokens: null, percent: null })
|
|
329
|
+
context.draft.compacted = false
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function contextPercent({ tokens, model, prefs }: { tokens: number; model: ModelRef | null; prefs: Prefs }): number | null {
|
|
333
|
+
if (!model || tokens <= 0) return null
|
|
334
|
+
const limit = prefs.models[`${model.providerID}/${model.id}`]?.context
|
|
289
335
|
if (!limit || limit <= 0) return null
|
|
290
|
-
return Math.round((
|
|
336
|
+
return Math.round((tokens / limit) * 100)
|
|
291
337
|
}
|
|
292
338
|
|
|
293
339
|
// The footer closing the answer of `turn` at `at`.
|
|
294
340
|
function emitFooter({ draft, prefs, emit, turn, at, notify }: Context & { turn: Draft<Turn>; at: number; notify: boolean }) {
|
|
295
|
-
// Message mode: a model that never called send_message would leave the user in silence.
|
|
296
|
-
if (!turn.messaged && turn.unsent) emit({ type: 'markdown', text: turn.unsent, blankLineBefore: draft.lastKind === 'tool' })
|
|
297
|
-
turn.unsent = null
|
|
298
341
|
// No model step ran (a compaction-only execution): nothing to summarize.
|
|
299
342
|
if (!turn.model) return
|
|
343
|
+
// The answer posted no text or tool line (e.g. an IDLE_REPLY, only hidden tools).
|
|
344
|
+
if (draft.lastKind === null) return
|
|
345
|
+
// A coordinator answer with only tool lines (it started a worker, then answered IDLE): its thread shows the work.
|
|
346
|
+
if (draft.coordinator && !turn.texted) return
|
|
300
347
|
// A child still runs: the answer comes later.
|
|
301
348
|
if (Object.values(draft.children).some((child) => child.running)) return
|
|
302
349
|
emit({
|
|
303
350
|
type: 'footer',
|
|
351
|
+
channelId: draft.channelId,
|
|
304
352
|
directory: draft.directory,
|
|
305
353
|
durationMs: at - turn.startedAt,
|
|
306
|
-
contextPercent: contextPercent({ turn, prefs }),
|
|
354
|
+
contextPercent: contextPercent({ tokens: turn.tokens, model: turn.model, prefs }),
|
|
307
355
|
model: { providerID: turn.model.providerID, id: turn.model.id },
|
|
308
356
|
agent: turn.agent,
|
|
309
357
|
notify,
|
|
@@ -334,7 +382,8 @@ function applyOpencode({ draft, event, prefs, emit }: Context & { event: V2Event
|
|
|
334
382
|
// Questions and permissions of subagents show in this thread too.
|
|
335
383
|
const label = child?.agent ?? null
|
|
336
384
|
if (event.type === 'session.inbox.enqueued' && event.data.item.type === 'synthetic' && !child?.background) {
|
|
337
|
-
|
|
385
|
+
const metadata = event.data.item.payload.metadata ?? {}
|
|
386
|
+
return showShellFinished({ draft, prefs, emit, metadata, label })
|
|
338
387
|
}
|
|
339
388
|
switch (event.type) {
|
|
340
389
|
case 'form.created':
|
|
@@ -362,12 +411,26 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
362
411
|
case 'session.execution.started':
|
|
363
412
|
draft.turn ??= newTurn(event.created)
|
|
364
413
|
return
|
|
414
|
+
// A new session's banner already shows its first model and agent.
|
|
415
|
+
case 'session.model.selected': {
|
|
416
|
+
const { model, previous } = event.data
|
|
417
|
+
const same = previous && previous.providerID === model.providerID && previous.id === model.id && previous.variant === model.variant
|
|
418
|
+
if (draft.bannerPending || same) return
|
|
419
|
+
emit({ type: 'send', text: formatModelSwitched({ name: prefs.models[`${model.providerID}/${model.id}`]?.name ?? `${model.providerID}/${model.id}`, variant: model.variant }) })
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
case 'session.agent.selected': {
|
|
423
|
+
const { agent, previous } = event.data
|
|
424
|
+
if (draft.bannerPending || agent === previous) return
|
|
425
|
+
emit({ type: 'send', text: formatAgentSwitched({ agent, previous }) })
|
|
426
|
+
return
|
|
427
|
+
}
|
|
365
428
|
case 'session.step.started': {
|
|
366
429
|
const model = { providerID: event.data.model.providerID, id: event.data.model.id }
|
|
367
430
|
draft.turn ??= newTurn(event.created)
|
|
368
431
|
draft.turn.model = model
|
|
369
432
|
draft.turn.agent = event.data.agent
|
|
370
|
-
if (draft.bannerPending) emit({ type: 'send', text: formatBanner({ model: event.data.model, agent: event.data.agent }) })
|
|
433
|
+
if (draft.bannerPending) emit({ type: 'send', text: formatBanner({ model: event.data.model, agent: event.data.agent, coordinator: draft.coordinator }) })
|
|
371
434
|
draft.bannerPending = false
|
|
372
435
|
return
|
|
373
436
|
}
|
|
@@ -375,10 +438,17 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
375
438
|
if (!draft.turn) return
|
|
376
439
|
const { tokens } = event.data
|
|
377
440
|
draft.turn.tokens = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
|
|
441
|
+
if (draft.compacted) {
|
|
442
|
+
const prompt = tokens.input + tokens.cache.read + tokens.cache.write
|
|
443
|
+
showCompacted(context, { tokens: prompt, percent: contextPercent({ tokens: prompt, model: draft.turn.model, prefs }) })
|
|
444
|
+
}
|
|
378
445
|
// The prompt only grows, so the cached prefix should too. A drop means the provider
|
|
379
446
|
// re-read it: changed prefix, other model, or cache expired (TTL). The slack absorbs cache block rounding.
|
|
447
|
+
// Only a step that rewrote most of the prompt is a full loss; a smaller rewrite is a partial miss, not worth a line.
|
|
380
448
|
const previous = draft.cached
|
|
381
|
-
|
|
449
|
+
const promptTokens = tokens.input + tokens.cache.read + tokens.cache.write
|
|
450
|
+
const rewroteMost = tokens.cache.write >= promptTokens * CACHE_MISS_MIN_WRITE_SHARE
|
|
451
|
+
if (previous && rewroteMost && tokens.cache.read + CACHE_MISS_SLACK < previous.tokens) {
|
|
382
452
|
toolLine(context, formatCacheMiss({ read: tokens.cache.read, expected: previous.tokens, idleMs: event.created - previous.at }))
|
|
383
453
|
}
|
|
384
454
|
draft.cached = { tokens: tokens.cache.read + tokens.cache.write, at: event.created }
|
|
@@ -386,12 +456,15 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
386
456
|
}
|
|
387
457
|
case 'session.text.ended': {
|
|
388
458
|
const text = event.data.text.trim()
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
459
|
+
// A coordinator turn with nothing for the user (a note, a started worker) answers IDLE_REPLY: Discord shows nothing.
|
|
460
|
+
if (!text || text === IDLE_REPLY) return
|
|
461
|
+
const replyTo = draft.turn?.replyTo ?? null
|
|
462
|
+
// The reply header already sets the text apart from the tool lines above.
|
|
463
|
+
emit({ type: 'markdown', text, blankLineBefore: draft.lastKind === 'tool' && !replyTo, replyTo })
|
|
464
|
+
if (draft.turn) {
|
|
465
|
+
draft.turn.replyTo = null
|
|
466
|
+
draft.turn.texted = true
|
|
393
467
|
}
|
|
394
|
-
emit({ type: 'markdown', text, blankLineBefore: draft.lastKind === 'tool' })
|
|
395
468
|
draft.lastKind = 'text'
|
|
396
469
|
return
|
|
397
470
|
}
|
|
@@ -419,24 +492,28 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
419
492
|
case 'session.compaction.ended':
|
|
420
493
|
// A compacted history has a new prefix: its first miss is expected.
|
|
421
494
|
draft.cached = null
|
|
422
|
-
|
|
495
|
+
// Its size is only known when the next step reads it.
|
|
496
|
+
draft.compacted = true
|
|
497
|
+
return
|
|
423
498
|
case 'session.compaction.failed':
|
|
424
499
|
return toolLine(context, formatError(`compaction failed: ${event.data.error.message}`))
|
|
425
500
|
case 'session.execution.succeeded': {
|
|
426
|
-
|
|
501
|
+
endCompaction(context)
|
|
502
|
+
if (draft.turn) emitFooter({ ...context, turn: draft.turn, at: event.created, notify: !draft.turn.slept })
|
|
427
503
|
draft.turn = null
|
|
428
504
|
draft.lastKind = null
|
|
429
|
-
if (turn) emitFooter({ ...context, turn, at: event.created, notify: !turn.slept })
|
|
430
505
|
emit({ type: 'viewed', sessionId: draft.sessionId, idle: event.created })
|
|
431
506
|
return
|
|
432
507
|
}
|
|
433
508
|
case 'session.execution.failed':
|
|
509
|
+
endCompaction(context)
|
|
434
510
|
draft.turn = null
|
|
435
511
|
draft.lastKind = null
|
|
436
512
|
emit({ type: 'send', text: formatError(event.data.error.message), notify: true })
|
|
437
513
|
emit({ type: 'viewed', sessionId: draft.sessionId, idle: event.created })
|
|
438
514
|
return
|
|
439
515
|
case 'session.execution.interrupted':
|
|
516
|
+
endCompaction(context)
|
|
440
517
|
draft.turn = null
|
|
441
518
|
draft.lastKind = null
|
|
442
519
|
// A shutdown does not make the session idle in OpenCode.
|
|
@@ -447,12 +524,16 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
447
524
|
case 'session.inbox.delivered': {
|
|
448
525
|
// OpenCode runs queued items in the same execution: close the answer
|
|
449
526
|
// before each one with a silent footer, like V1 did per turn.
|
|
450
|
-
const
|
|
527
|
+
const item = draft.inbox.find((candidate) => candidate.inboxID === event.data.inboxID)
|
|
528
|
+
const queued = item?.delivery === 'queue'
|
|
529
|
+
// Each user prompt the run takes gets its own reply; synthetic items keep the current one.
|
|
530
|
+
// An acked item's "Queued message sent" reply already marks where its answer starts.
|
|
531
|
+
if (item && draft.turn) draft.turn.replyTo = item.acked ? null : item.replyTo
|
|
451
532
|
if (queued && draft.turn?.model) {
|
|
452
533
|
emitFooter({ ...context, turn: draft.turn, at: event.created, notify: false })
|
|
453
534
|
draft.turn.startedAt = event.created
|
|
454
535
|
draft.turn.slept = false
|
|
455
|
-
draft.turn.
|
|
536
|
+
draft.turn.texted = false
|
|
456
537
|
draft.lastKind = null
|
|
457
538
|
}
|
|
458
539
|
return deliverQueued({ draft, emit, inboxID: event.data.inboxID })
|
|
@@ -513,15 +594,6 @@ function applyTool(context: Context & { event: V2Event; label: string | undefine
|
|
|
513
594
|
if (reused) Object.assign(reused, { ...call, agent: call.agent || reused.agent })
|
|
514
595
|
}
|
|
515
596
|
draft.tools[key] = tool
|
|
516
|
-
// The plugin offers send_message to root sessions only (MESSAGE_TOOL).
|
|
517
|
-
if (name === MESSAGE_TOOL && label === undefined) {
|
|
518
|
-
const text = stringInput(input, 'text').trim()
|
|
519
|
-
if (draft.turn) draft.turn.messaged = true
|
|
520
|
-
if (!text) return
|
|
521
|
-
context.emit({ type: 'markdown', text, blankLineBefore: draft.lastKind === 'tool' })
|
|
522
|
-
draft.lastKind = 'text'
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
597
|
if (render && isToolVisible({ name, input }, prefs.verbosity)) toolLine(context, formatToolLine({ name, input }, { label }))
|
|
526
598
|
return
|
|
527
599
|
}
|
|
@@ -585,6 +657,7 @@ function showShellFinished({
|
|
|
585
657
|
...context
|
|
586
658
|
}: Context & { metadata: Readonly<Record<string, unknown>>; label: string | null }) {
|
|
587
659
|
const { shellID, jobID, state, exit } = metadata
|
|
660
|
+
// A <session-finished> note (coordinator.ts) shows nothing: the coordinator reports the outcome.
|
|
588
661
|
if (metadata['source'] !== 'shell' || typeof shellID !== 'string' || typeof jobID !== 'string') return
|
|
589
662
|
const description = context.draft.shells[shellID] ?? null
|
|
590
663
|
delete context.draft.shells[shellID]
|
|
@@ -609,7 +682,7 @@ function showAgentPrompt({ draft, emit, prompt }: Pick<Context, 'draft' | 'emit'
|
|
|
609
682
|
function dismissAgentPrompts({ draft, emit, ids, result = 'dismissed' }: Pick<Context, 'draft' | 'emit'> & { ids: readonly string[]; result?: string }) {
|
|
610
683
|
if (ids.length === 0) return
|
|
611
684
|
draft.agentUi = draft.agentUi.filter((prompt) => !ids.includes(prompt.id))
|
|
612
|
-
for (const id of ids) emit({ type: 'edit', key: `agent:${id}`, messages: [textOnly(asSubtext(
|
|
685
|
+
for (const id of ids) emit({ type: 'edit', key: `agent:${id}`, messages: [textOnly(asSubtext(result))] })
|
|
613
686
|
}
|
|
614
687
|
|
|
615
688
|
function applyKimaki({ draft, event, prefs, emit }: Context & { event: KimakiEvent }) {
|
|
@@ -622,7 +695,7 @@ function applyKimaki({ draft, event, prefs, emit }: Context & { event: KimakiEve
|
|
|
622
695
|
if (draft.turn) draft.turn.slept = true
|
|
623
696
|
return
|
|
624
697
|
case 'kimaki.command':
|
|
625
|
-
draft.commandPending = { text: event.text, username: event.username }
|
|
698
|
+
draft.commandPending = { text: event.text, username: event.username, messageId: event.messageId, echo: event.echo }
|
|
626
699
|
return
|
|
627
700
|
case 'kimaki.command-failed':
|
|
628
701
|
draft.commandPending = null
|
|
@@ -673,13 +746,9 @@ export function replayEffects({
|
|
|
673
746
|
const blocks = messages.flatMap((message): Block[] => {
|
|
674
747
|
if (message.type !== 'assistant') return []
|
|
675
748
|
return message.content.flatMap((part): Block[] => {
|
|
676
|
-
if (part.type === 'text') return part.text.trim()
|
|
749
|
+
if (part.type === 'text') return part.text.trim() ? [{ kind: 'text', text: part.text.trim() }] : []
|
|
677
750
|
if (part.type !== 'tool' || part.state.status === 'streaming') return []
|
|
678
751
|
const { input, metadata } = part.state
|
|
679
|
-
if (part.name === MESSAGE_TOOL) {
|
|
680
|
-
const text = stringInput(input, 'text').trim()
|
|
681
|
-
return text ? [{ kind: 'text', text }] : []
|
|
682
|
-
}
|
|
683
752
|
const isExecute = part.name === 'execute'
|
|
684
753
|
const lines = [{ name: part.name, input }, ...(isExecute ? executeCalls(metadata) : [])]
|
|
685
754
|
.filter((call) => isToolVisible(call, prefs.verbosity))
|
|
@@ -692,7 +761,7 @@ export function replayEffects({
|
|
|
692
761
|
const skipped = blocks.length - kept.length
|
|
693
762
|
const effects = kept.map((block, index): Effect => {
|
|
694
763
|
const previous = kept[index - 1]?.kind ?? null
|
|
695
|
-
if (block.kind === 'text') return { type: 'markdown', text: block.text, blankLineBefore: previous === 'tool' }
|
|
764
|
+
if (block.kind === 'text') return { type: 'markdown', text: block.text, blankLineBefore: previous === 'tool', replyTo: null }
|
|
696
765
|
return { type: 'send', text: previous === 'text' ? `\n${block.text}` : block.text }
|
|
697
766
|
})
|
|
698
767
|
return [
|
package/src/tunnel.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createServer } from 'vite'
|
|
1
6
|
import { expect, test } from 'vitest'
|
|
2
|
-
import { detectPortFromText, routeName } from './tunnel.ts'
|
|
7
|
+
import { detectPortFromText, parseNetstatListeningPids, routeName, startHostProxy } from './tunnel.ts'
|
|
3
8
|
|
|
4
9
|
test('detects the dev server port and skips noise lines', () => {
|
|
5
10
|
const vite = '\u001b[33mPort 5173 is in use, trying another one...\u001b[39m\n ➜ Local: \u001b[36mhttp://localhost:\u001b[1m5174\u001b[22m/\u001b[39m\n'
|
|
@@ -25,15 +30,86 @@ test('detects the dev server port and skips noise lines', () => {
|
|
|
25
30
|
test('route name: explicit, then tuistory session, then random', () => {
|
|
26
31
|
expect({
|
|
27
32
|
explicit: routeName({ name: 'docs', tuistorySessionId: 'abc123' }),
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
normalized: routeName({ name: '_My_App.dev_' }),
|
|
34
|
+
invalid: String(routeName({ name: '___' })),
|
|
35
|
+
tuistory: routeName({ tuistorySessionId: 'Holocron_Dev' }),
|
|
36
|
+
tuistoryInvalid: String(routeName({ tuistorySessionId: '___' })).length,
|
|
30
37
|
random: String(routeName({})).length,
|
|
31
38
|
}).toMatchInlineSnapshot(`
|
|
32
39
|
{
|
|
33
40
|
"explicit": "docs",
|
|
34
|
-
"invalid": "Error: Invalid tunnel ID "
|
|
41
|
+
"invalid": "Error: Invalid tunnel ID "___": use letters, digits and dashes",
|
|
42
|
+
"normalized": "my-app-dev",
|
|
35
43
|
"random": 16,
|
|
36
|
-
"tuistory": "
|
|
44
|
+
"tuistory": "holocron-dev",
|
|
45
|
+
"tuistoryInvalid": 16,
|
|
37
46
|
}
|
|
38
47
|
`)
|
|
39
48
|
})
|
|
49
|
+
|
|
50
|
+
test('netstat listening pids on Windows', () => {
|
|
51
|
+
const stdout = [
|
|
52
|
+
'Active Connections',
|
|
53
|
+
' Proto Local Address Foreign Address State PID',
|
|
54
|
+
' TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 1234',
|
|
55
|
+
' TCP [::]:5173 [::]:0 LISTENING 1234',
|
|
56
|
+
' TCP 127.0.0.1:51730 0.0.0.0:0 LISTENING 999',
|
|
57
|
+
' TCP 127.0.0.1:5173 127.0.0.1:60000 ESTABLISHED 777',
|
|
58
|
+
' UDP 0.0.0.0:5173 *:* 555',
|
|
59
|
+
].join('\r\n')
|
|
60
|
+
expect(parseNetstatListeningPids({ stdout, port: 5173 })).toMatchInlineSnapshot(`
|
|
61
|
+
[
|
|
62
|
+
1234,
|
|
63
|
+
]
|
|
64
|
+
`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function request({ port, headers }: { port: number; headers: http.OutgoingHttpHeaders }): Promise<string> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const req = http.request({ host: '127.0.0.1', port, path: '/', headers })
|
|
70
|
+
req.on('response', (res) => {
|
|
71
|
+
res.resume()
|
|
72
|
+
resolve(String(res.statusCode))
|
|
73
|
+
req.destroy()
|
|
74
|
+
})
|
|
75
|
+
req.on('upgrade', (res, socket) => {
|
|
76
|
+
resolve(`${res.statusCode} ${res.headers['sec-websocket-protocol']}`)
|
|
77
|
+
socket.destroy()
|
|
78
|
+
})
|
|
79
|
+
req.on('error', reject)
|
|
80
|
+
req.end()
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test('host proxy lets the public tunnel host through Vite host check, HMR included', async () => {
|
|
85
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-tunnel-'))
|
|
86
|
+
fs.writeFileSync(path.join(root, 'index.html'), '<p>hi</p>')
|
|
87
|
+
const vite = await createServer({ root, configFile: false, logLevel: 'silent', server: { host: '127.0.0.1', port: 0 } })
|
|
88
|
+
await vite.listen()
|
|
89
|
+
const address = vite.httpServer!.address()
|
|
90
|
+
if (!address || typeof address === 'string') throw new Error('no vite address')
|
|
91
|
+
const proxy = await startHostProxy({ host: '127.0.0.1', port: address.port })
|
|
92
|
+
if (proxy instanceof Error) throw proxy
|
|
93
|
+
const proxyAddress = proxy.address()
|
|
94
|
+
if (!proxyAddress || typeof proxyAddress === 'string') throw new Error('no proxy address')
|
|
95
|
+
|
|
96
|
+
const publicHost = { host: 'docs.abc.opentunnel.xyz' }
|
|
97
|
+
const hmr = { ...publicHost, connection: 'Upgrade', upgrade: 'websocket', 'sec-websocket-version': '13', 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==', 'sec-websocket-protocol': 'vite-hmr' }
|
|
98
|
+
try {
|
|
99
|
+
expect({
|
|
100
|
+
direct: await request({ port: address.port, headers: publicHost }),
|
|
101
|
+
proxied: await request({ port: proxyAddress.port, headers: publicHost }),
|
|
102
|
+
proxiedHmr: await request({ port: proxyAddress.port, headers: hmr }),
|
|
103
|
+
}).toMatchInlineSnapshot(`
|
|
104
|
+
{
|
|
105
|
+
"direct": "403",
|
|
106
|
+
"proxied": "200",
|
|
107
|
+
"proxiedHmr": "101 vite-hmr",
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
} finally {
|
|
111
|
+
proxy.close()
|
|
112
|
+
await vite.close()
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
})
|