kimaki 0.4.82 → 0.4.84
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/LICENSE +21 -0
- package/dist/anthropic-auth-plugin.js +7 -0
- package/dist/cli.js +51 -7
- package/dist/commands/abort.js +5 -16
- package/dist/commands/action-buttons.js +3 -3
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/ask-question.js +3 -3
- package/dist/commands/context-usage.js +1 -1
- package/dist/commands/create-new-project.js +1 -1
- package/dist/commands/fork.js +11 -8
- package/dist/commands/merge-worktree.js +1 -1
- package/dist/commands/new-worktree.js +63 -44
- package/dist/commands/remove-project.js +1 -1
- package/dist/commands/resume.js +11 -8
- package/dist/commands/screenshare.js +14 -6
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session.js +1 -1
- package/dist/commands/undo-redo.js +91 -7
- package/dist/commands/user-command.js +1 -1
- package/dist/config.js +16 -1
- package/dist/database.js +53 -2
- package/dist/db.js +6 -0
- package/dist/discord-bot.js +48 -85
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-opencode-sync.js +515 -0
- package/dist/external-opencode-sync.test.js +151 -0
- package/dist/gateway-proxy.e2e.test.js +8 -5
- package/dist/genai.js +1 -1
- package/dist/generated/enums.js +4 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.js +1 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
- package/dist/generated/models/external_session_pending_prompts.js +1 -0
- package/dist/hrana-server.js +14 -285
- package/dist/hrana-server.test.js +4 -2
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kitty-graphics-parser.js +3 -0
- package/dist/kitty-graphics-parser.test.js +276 -0
- package/dist/kitty-graphics-plugin.js +3 -0
- package/dist/markdown.js +4 -4
- package/dist/markdown.test.js +1 -1
- package/dist/message-formatting.js +54 -15
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/openai-realtime.js +9 -13
- package/dist/opencode.js +28 -5
- package/dist/queue-advanced-e2e-setup.js +89 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
- package/dist/queue-advanced-typing.e2e.test.js +9 -22
- package/dist/queue-question-select-drain.e2e.test.js +117 -0
- package/dist/session-handler/event-stream-state.js +101 -7
- package/dist/session-handler/event-stream-state.test.js +7 -3
- package/dist/session-handler/thread-session-runtime.js +120 -9
- package/dist/store.js +1 -0
- package/dist/system-message.js +22 -4
- package/dist/system-message.test.js +19 -0
- package/dist/task-runner.js +1 -1
- package/dist/thread-message-queue.e2e.test.js +8 -14
- package/dist/tools.js +1 -1
- package/dist/undo-redo.e2e.test.js +20 -25
- package/package.json +10 -6
- package/schema.prisma +6 -0
- package/skills/errore/SKILL.md +40 -13
- package/skills/goke/SKILL.md +12 -0
- package/skills/lintcn/SKILL.md +868 -0
- package/skills/npm-package/SKILL.md +1 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/spiceflow/SKILL.md +1 -1
- package/skills/usecomputer/SKILL.md +339 -0
- package/src/ai-tool-to-genai.ts +1 -0
- package/src/anthropic-auth-plugin.ts +7 -0
- package/src/cli.ts +59 -6
- package/src/commands/abort.ts +6 -16
- package/src/commands/action-buttons.ts +5 -1
- package/src/commands/add-project.ts +1 -1
- package/src/commands/ask-question.ts +5 -2
- package/src/commands/context-usage.ts +1 -1
- package/src/commands/create-new-project.ts +1 -1
- package/src/commands/fork.ts +12 -11
- package/src/commands/merge-worktree.ts +1 -1
- package/src/commands/new-worktree.ts +74 -55
- package/src/commands/remove-project.ts +1 -1
- package/src/commands/resume.ts +12 -10
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +18 -6
- package/src/commands/session.ts +1 -1
- package/src/commands/undo-redo.ts +108 -10
- package/src/commands/user-command.ts +1 -1
- package/src/config.ts +19 -1
- package/src/database.ts +72 -3
- package/src/db.ts +8 -0
- package/src/discord-bot.ts +58 -93
- package/src/discord-command-registration.ts +1 -1
- package/src/external-opencode-sync.ts +729 -0
- package/src/gateway-proxy.e2e.test.ts +9 -5
- package/src/genai.ts +3 -3
- package/src/generated/commonInputTypes.ts +34 -0
- package/src/generated/enums.ts +8 -0
- package/src/generated/internal/class.ts +4 -4
- package/src/generated/internal/prismaNamespace.ts +8 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
- package/src/generated/models/thread_sessions.ts +53 -1
- package/src/hrana-server.test.ts +8 -2
- package/src/hrana-server.ts +18 -390
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/markdown.test.ts +1 -1
- package/src/markdown.ts +4 -4
- package/src/message-formatting.ts +66 -17
- package/src/onboarding-tutorial.ts +1 -1
- package/src/openai-realtime.ts +6 -10
- package/src/opencode.ts +31 -7
- package/src/queue-advanced-e2e-setup.ts +92 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
- package/src/queue-advanced-typing.e2e.test.ts +9 -22
- package/src/queue-question-select-drain.e2e.test.ts +149 -0
- package/src/schema.sql +1 -0
- package/src/session-handler/event-stream-state.test.ts +7 -2
- package/src/session-handler/event-stream-state.ts +128 -7
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +153 -11
- package/src/store.ts +8 -0
- package/src/system-message.ts +27 -4
- package/src/task-runner.ts +1 -1
- package/src/thread-message-queue.e2e.test.ts +8 -14
- package/src/tools.ts +1 -1
- package/src/undo-redo.e2e.test.ts +28 -26
- package/skills/jitter/node_modules/.bin/esbuild +0 -21
- package/skills/jitter/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
- package/skills/jitter/node_modules/typescript/README.md +0 -50
- package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
- package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
- package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
- package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
- package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
- package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
- package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
- package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
- package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
- package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
- package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
- package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
- package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
- package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
- package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
- package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/package.json +0 -120
package/dist/markdown.js
CHANGED
|
@@ -263,8 +263,8 @@ export function getCompactSessionContext({ client, sessionId, includeSystemPromp
|
|
|
263
263
|
for (const msg of recentMessages) {
|
|
264
264
|
if (msg.info.role === 'user') {
|
|
265
265
|
const textParts = (msg.parts || [])
|
|
266
|
-
.filter((p) => p.type === 'text'
|
|
267
|
-
.map((p) => ('text'
|
|
266
|
+
.filter((p) => p.type === 'text')
|
|
267
|
+
.map((p) => (p.type === 'text' ? extractNonXmlContent(p.text || '') : ''))
|
|
268
268
|
.filter(Boolean);
|
|
269
269
|
if (textParts.length > 0) {
|
|
270
270
|
lines.push(`[User]: ${textParts.join(' ').slice(0, 1000)}`);
|
|
@@ -274,8 +274,8 @@ export function getCompactSessionContext({ client, sessionId, includeSystemPromp
|
|
|
274
274
|
else if (msg.info.role === 'assistant') {
|
|
275
275
|
// Get assistant text parts (non-synthetic, non-empty)
|
|
276
276
|
const textParts = (msg.parts || [])
|
|
277
|
-
.filter((p) => p.type === 'text' &&
|
|
278
|
-
.map((p) => ('text'
|
|
277
|
+
.filter((p) => p.type === 'text' && !p.synthetic && p.text)
|
|
278
|
+
.map((p) => (p.type === 'text' ? p.text : ''))
|
|
279
279
|
.filter(Boolean);
|
|
280
280
|
if (textParts.length > 0) {
|
|
281
281
|
lines.push(`[Assistant]: ${textParts.join(' ').slice(0, 1000)}`);
|
package/dist/markdown.test.js
CHANGED
|
@@ -100,7 +100,7 @@ beforeAll(async () => {
|
|
|
100
100
|
const msgs = await client.session.messages({ sessionID });
|
|
101
101
|
const assistantMsg = msgs.data?.find((m) => m.info.role === 'assistant');
|
|
102
102
|
const hasTextParts = assistantMsg?.parts?.some((p) => {
|
|
103
|
-
return p.type === 'text' &&
|
|
103
|
+
return p.type === 'text' && p.text && !p.synthetic;
|
|
104
104
|
});
|
|
105
105
|
if (hasTextParts) {
|
|
106
106
|
// Extra wait for step-start and other parts to be flushed
|
|
@@ -45,26 +45,65 @@ function normalizeWhitespace(text) {
|
|
|
45
45
|
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Collect renderable assistant parts from session messages as SessionChunks.
|
|
49
|
+
* Each non-empty formatted part becomes one chunk. Caller can batch them
|
|
50
|
+
* with batchChunksForDiscord() before sending.
|
|
51
|
+
*
|
|
52
|
+
* - skipPartIds: parts already synced (external sync). Skipped parts are
|
|
53
|
+
* not included in the result.
|
|
54
|
+
* - limit: max parts to include (from the end). Older parts are counted
|
|
55
|
+
* in skippedCount.
|
|
50
56
|
*/
|
|
51
|
-
export function
|
|
52
|
-
const
|
|
57
|
+
export function collectSessionChunks({ messages, skipPartIds, limit, }) {
|
|
58
|
+
const allChunks = [];
|
|
53
59
|
for (const message of messages) {
|
|
54
|
-
if (message.info.role
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
if (message.info.role !== 'assistant') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const part of message.parts) {
|
|
64
|
+
if (skipPartIds?.has(part.id)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const content = formatPart(part);
|
|
68
|
+
if (!content.trim()) {
|
|
69
|
+
continue;
|
|
60
70
|
}
|
|
71
|
+
allChunks.push({ partIds: [part.id], content: content.trimEnd() });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (limit !== undefined && allChunks.length > limit) {
|
|
75
|
+
return {
|
|
76
|
+
chunks: allChunks.slice(-limit),
|
|
77
|
+
skippedCount: allChunks.length - limit,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { chunks: allChunks, skippedCount: 0 };
|
|
81
|
+
}
|
|
82
|
+
// Merge consecutive SessionChunks into as few Discord messages as possible,
|
|
83
|
+
// respecting the 2000 char limit.
|
|
84
|
+
const DISCORD_BATCH_MAX_LENGTH = 2000;
|
|
85
|
+
export function batchChunksForDiscord(chunks) {
|
|
86
|
+
if (chunks.length === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const batched = [];
|
|
90
|
+
let current = { partIds: [...chunks[0].partIds], content: chunks[0].content };
|
|
91
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
92
|
+
const next = chunks[i];
|
|
93
|
+
const merged = current.content + '\n' + next.content;
|
|
94
|
+
if (merged.length <= DISCORD_BATCH_MAX_LENGTH) {
|
|
95
|
+
current = {
|
|
96
|
+
partIds: [...current.partIds, ...next.partIds],
|
|
97
|
+
content: merged,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
batched.push(current);
|
|
102
|
+
current = { partIds: [...next.partIds], content: next.content };
|
|
61
103
|
}
|
|
62
104
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const content = partsToRender.map((p) => p.content).join('\n');
|
|
66
|
-
const skippedCount = allAssistantParts.length - partsToRender.length;
|
|
67
|
-
return { partIds, content, skippedCount };
|
|
105
|
+
batched.push(current);
|
|
106
|
+
return batched;
|
|
68
107
|
}
|
|
69
108
|
export const TEXT_MIME_TYPES = [
|
|
70
109
|
'text/',
|
|
@@ -142,7 +142,7 @@ ${backticks}bash
|
|
|
142
142
|
PORT=$((RANDOM % 6000 + 3000))
|
|
143
143
|
tmux kill-session -t game-dev 2>/dev/null
|
|
144
144
|
tmux new-session -d -s game-dev -c "$PWD"
|
|
145
|
-
tmux send-keys -t game-dev "PORT=$PORT kimaki tunnel -p $PORT -- bun run server.ts" Enter
|
|
145
|
+
tmux send-keys -t game-dev "PORT=$PORT kimaki tunnel --kill -p $PORT -- bun run server.ts" Enter
|
|
146
146
|
${backticks}
|
|
147
147
|
|
|
148
148
|
Wait a moment, then get the tunnel URL:
|
package/dist/openai-realtime.js
CHANGED
|
@@ -140,13 +140,11 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
140
140
|
}
|
|
141
141
|
// Set up event handlers
|
|
142
142
|
client.on('conversation.item.created', ({ item }) => {
|
|
143
|
-
if (
|
|
144
|
-
item.role === 'assistant' &&
|
|
143
|
+
if (item.role === 'assistant' &&
|
|
145
144
|
item.type === 'message') {
|
|
146
145
|
// Check if this is the first audio content
|
|
147
|
-
const hasAudio =
|
|
148
|
-
|
|
149
|
-
item.content.some((c) => 'type' in c && c.type === 'audio');
|
|
146
|
+
const hasAudio = Array.isArray(item.content) &&
|
|
147
|
+
item.content.some((c) => c.type === 'audio');
|
|
150
148
|
if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
151
149
|
isAssistantSpeaking = true;
|
|
152
150
|
onAssistantStartSpeaking();
|
|
@@ -155,7 +153,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
155
153
|
});
|
|
156
154
|
client.on('conversation.updated', ({ item, delta, }) => {
|
|
157
155
|
// Handle audio chunks
|
|
158
|
-
if (delta?.audio &&
|
|
156
|
+
if (delta?.audio && item.role === 'assistant') {
|
|
159
157
|
if (!isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
160
158
|
isAssistantSpeaking = true;
|
|
161
159
|
onAssistantStartSpeaking();
|
|
@@ -177,13 +175,11 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
177
175
|
}
|
|
178
176
|
// Handle transcriptions
|
|
179
177
|
if (delta?.transcript) {
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
openaiLogger.log('Assistant transcription:', delta.transcript);
|
|
186
|
-
}
|
|
178
|
+
if (item.role === 'user') {
|
|
179
|
+
openaiLogger.log('User transcription:', delta.transcript);
|
|
180
|
+
}
|
|
181
|
+
else if (item.role === 'assistant') {
|
|
182
|
+
openaiLogger.log('Assistant transcription:', delta.transcript);
|
|
187
183
|
}
|
|
188
184
|
}
|
|
189
185
|
});
|
package/dist/opencode.js
CHANGED
|
@@ -29,6 +29,9 @@ import { notifyError } from './sentry.js';
|
|
|
29
29
|
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
|
|
30
30
|
import { ensureKimakiCommandShim, getPathEnvKey, getSpawnCommandAndArgs, prependPathEntry, selectResolvedCommand, } from './opencode-command.js';
|
|
31
31
|
const opencodeLogger = createLogger(LogPrefix.OPENCODE);
|
|
32
|
+
// Tracks directories that have been initialized, to avoid repeated log spam
|
|
33
|
+
// from the external sync polling loop.
|
|
34
|
+
const initializedDirectories = new Set();
|
|
32
35
|
const STARTUP_STDERR_TAIL_LIMIT = 30;
|
|
33
36
|
const STARTUP_STDERR_LINE_MAX_LENGTH = 120;
|
|
34
37
|
const STARTUP_ERROR_REASON_MAX_LENGTH = 1500;
|
|
@@ -361,6 +364,20 @@ async function startSingleServer() {
|
|
|
361
364
|
opencodeLogger.warn(kimakiShimDirectory.message);
|
|
362
365
|
}
|
|
363
366
|
const gatewayToken = store.getState().gatewayToken;
|
|
367
|
+
const vitestOpencodeEnv = (() => {
|
|
368
|
+
if (process.env.KIMAKI_VITEST !== '1') {
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
const root = path.join(getDataDir(), 'opencode-vitest-home');
|
|
372
|
+
return {
|
|
373
|
+
OPENCODE_TEST_HOME: root,
|
|
374
|
+
OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
|
|
375
|
+
XDG_CONFIG_HOME: path.join(root, '.config'),
|
|
376
|
+
XDG_DATA_HOME: path.join(root, '.local', 'share'),
|
|
377
|
+
XDG_CACHE_HOME: path.join(root, '.cache'),
|
|
378
|
+
XDG_STATE_HOME: path.join(root, '.local', 'state'),
|
|
379
|
+
};
|
|
380
|
+
})();
|
|
364
381
|
const serverProcess = spawn(spawnCommand, spawnArgs, {
|
|
365
382
|
stdio: 'pipe',
|
|
366
383
|
detached: false,
|
|
@@ -417,6 +434,7 @@ async function startSingleServer() {
|
|
|
417
434
|
...(process.env.KIMAKI_SENTRY_DSN && {
|
|
418
435
|
KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
|
|
419
436
|
}),
|
|
437
|
+
...vitestOpencodeEnv,
|
|
420
438
|
...(pathEnv && { [pathEnvKey]: pathEnv }),
|
|
421
439
|
},
|
|
422
440
|
});
|
|
@@ -475,10 +493,12 @@ async function startSingleServer() {
|
|
|
475
493
|
singleServer = null;
|
|
476
494
|
clientCache.clear();
|
|
477
495
|
notifyServerLifecycle({ type: 'stopped' });
|
|
478
|
-
// Intentional kills
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
496
|
+
// Intentional kills should not trigger auto-restart:
|
|
497
|
+
// - SIGTERM from our cleanup/restart code
|
|
498
|
+
// - SIGINT propagated from Ctrl+C (parent process group signal)
|
|
499
|
+
// - any exit during bot shutdown (shuttingDown flag)
|
|
500
|
+
// Only unexpected crashes (non-zero exit without signal) get retried.
|
|
501
|
+
if (signal === 'SIGTERM' || signal === 'SIGINT' || global.shuttingDown) {
|
|
482
502
|
serverRetryCount = 0;
|
|
483
503
|
return;
|
|
484
504
|
}
|
|
@@ -586,7 +606,10 @@ export async function initializeOpencodeForDirectory(directory, _options) {
|
|
|
586
606
|
if (server instanceof Error) {
|
|
587
607
|
return server;
|
|
588
608
|
}
|
|
589
|
-
|
|
609
|
+
if (!initializedDirectories.has(directory)) {
|
|
610
|
+
initializedDirectories.add(directory);
|
|
611
|
+
opencodeLogger.log(`Using shared server on port ${server.port} for directory: ${directory}`);
|
|
612
|
+
}
|
|
590
613
|
return () => {
|
|
591
614
|
if (!singleServer) {
|
|
592
615
|
throw new ServerNotReadyError({ directory });
|
|
@@ -299,6 +299,41 @@ export function createDeterministicMatchers() {
|
|
|
299
299
|
],
|
|
300
300
|
},
|
|
301
301
|
};
|
|
302
|
+
// Question tool for select+queue drain test: model asks a question via dropdown,
|
|
303
|
+
// user answers via select menu while a message is queued.
|
|
304
|
+
const questionSelectQueueMatcher = {
|
|
305
|
+
id: 'question-select-queue-marker',
|
|
306
|
+
priority: 107,
|
|
307
|
+
when: {
|
|
308
|
+
lastMessageRole: 'user',
|
|
309
|
+
latestUserTextIncludes: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
310
|
+
},
|
|
311
|
+
then: {
|
|
312
|
+
parts: [
|
|
313
|
+
{ type: 'stream-start', warnings: [] },
|
|
314
|
+
{
|
|
315
|
+
type: 'tool-call',
|
|
316
|
+
toolCallId: 'question-select-queue-call',
|
|
317
|
+
toolName: 'question',
|
|
318
|
+
input: JSON.stringify({
|
|
319
|
+
questions: [{
|
|
320
|
+
question: 'How to proceed?',
|
|
321
|
+
header: 'Select action',
|
|
322
|
+
options: [
|
|
323
|
+
{ label: 'Alpha', description: 'Alpha option' },
|
|
324
|
+
{ label: 'Beta', description: 'Beta option' },
|
|
325
|
+
],
|
|
326
|
+
}],
|
|
327
|
+
}),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
type: 'finish',
|
|
331
|
+
finishReason: 'tool-calls',
|
|
332
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
};
|
|
302
337
|
// Model responds with text + tool call, then after tool result the
|
|
303
338
|
// follow-up matcher responds with text. This creates two assistant messages:
|
|
304
339
|
// first with finish="tool-calls" + completed, second with finish="stop".
|
|
@@ -356,6 +391,57 @@ export function createDeterministicMatchers() {
|
|
|
356
391
|
],
|
|
357
392
|
},
|
|
358
393
|
};
|
|
394
|
+
const undoFileMatcher = {
|
|
395
|
+
id: 'undo-file-marker',
|
|
396
|
+
priority: 111,
|
|
397
|
+
when: {
|
|
398
|
+
lastMessageRole: 'user',
|
|
399
|
+
latestUserTextIncludes: 'UNDO_FILE_MARKER',
|
|
400
|
+
},
|
|
401
|
+
then: {
|
|
402
|
+
parts: [
|
|
403
|
+
{ type: 'stream-start', warnings: [] },
|
|
404
|
+
{ type: 'text-start', id: 'undo-file-text' },
|
|
405
|
+
{ type: 'text-delta', id: 'undo-file-text', delta: 'creating undo file' },
|
|
406
|
+
{ type: 'text-end', id: 'undo-file-text' },
|
|
407
|
+
{
|
|
408
|
+
type: 'tool-call',
|
|
409
|
+
toolCallId: 'undo-file-bash',
|
|
410
|
+
toolName: 'bash',
|
|
411
|
+
input: JSON.stringify({
|
|
412
|
+
command: 'mkdir -p tmp && printf created > tmp/undo-marker.txt',
|
|
413
|
+
description: 'Create undo marker file',
|
|
414
|
+
}),
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: 'finish',
|
|
418
|
+
finishReason: 'tool-calls',
|
|
419
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
const undoFileFollowupMatcher = {
|
|
425
|
+
id: 'undo-file-followup',
|
|
426
|
+
priority: 112,
|
|
427
|
+
when: {
|
|
428
|
+
latestUserTextIncludes: 'UNDO_FILE_MARKER',
|
|
429
|
+
rawPromptIncludes: 'creating undo file',
|
|
430
|
+
},
|
|
431
|
+
then: {
|
|
432
|
+
parts: [
|
|
433
|
+
{ type: 'stream-start', warnings: [] },
|
|
434
|
+
{ type: 'text-start', id: 'undo-file-followup' },
|
|
435
|
+
{ type: 'text-delta', id: 'undo-file-followup', delta: 'undo file created' },
|
|
436
|
+
{ type: 'text-end', id: 'undo-file-followup' },
|
|
437
|
+
{
|
|
438
|
+
type: 'finish',
|
|
439
|
+
finishReason: 'stop',
|
|
440
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
};
|
|
359
445
|
// Multi-step tool chain: model emits text + 3 parallel tool calls in one
|
|
360
446
|
// response (finish="tool-calls"). All tools complete, then the follow-up
|
|
361
447
|
// matcher responds with final text (finish="stop"). This creates 2 assistant
|
|
@@ -562,10 +648,13 @@ export function createDeterministicMatchers() {
|
|
|
562
648
|
pluginTimeoutSleepMatcher,
|
|
563
649
|
actionButtonClickFollowupMatcher,
|
|
564
650
|
questionToolMatcher,
|
|
651
|
+
questionSelectQueueMatcher,
|
|
565
652
|
permissionTypingMatcher,
|
|
566
653
|
permissionTypingFollowupMatcher,
|
|
567
654
|
multiToolMatcher,
|
|
568
655
|
multiToolFollowupMatcher,
|
|
656
|
+
undoFileMatcher,
|
|
657
|
+
undoFileFollowupMatcher,
|
|
569
658
|
multiStepChainInitMatcher,
|
|
570
659
|
multiStepChainStep2Matcher,
|
|
571
660
|
multiStepChainStep3Matcher,
|
|
@@ -39,7 +39,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
const th = ctx.discord.thread(thread.id);
|
|
42
|
-
await th.waitForTypingEvent({ timeout:
|
|
42
|
+
await th.waitForTypingEvent({ timeout: 4_000 });
|
|
43
43
|
const pending = await waitForPendingPermission({
|
|
44
44
|
threadId: thread.id,
|
|
45
45
|
timeoutMs: 4_000,
|
|
@@ -133,14 +133,14 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
133
133
|
discord: ctx.discord,
|
|
134
134
|
threadId: thread.id,
|
|
135
135
|
text: 'Permission dismissed - user sent a new message.',
|
|
136
|
-
timeout:
|
|
136
|
+
timeout: 8_000,
|
|
137
137
|
});
|
|
138
138
|
await waitForBotReplyAfterUserMessage({
|
|
139
139
|
discord: ctx.discord,
|
|
140
140
|
threadId: thread.id,
|
|
141
141
|
userId: TEST_USER_ID,
|
|
142
142
|
userMessageIncludes: 'post-permission-user-message',
|
|
143
|
-
timeout:
|
|
143
|
+
timeout: 8_000,
|
|
144
144
|
});
|
|
145
145
|
await waitForBotMessageContaining({
|
|
146
146
|
discord: ctx.discord,
|
|
@@ -148,12 +148,12 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
148
148
|
userId: TEST_USER_ID,
|
|
149
149
|
text: 'ok',
|
|
150
150
|
afterUserMessageIncludes: 'post-permission-user-message',
|
|
151
|
-
timeout:
|
|
151
|
+
timeout: 8_000,
|
|
152
152
|
});
|
|
153
153
|
await waitForFooterMessage({
|
|
154
154
|
discord: ctx.discord,
|
|
155
155
|
threadId: thread.id,
|
|
156
|
-
timeout:
|
|
156
|
+
timeout: 8_000,
|
|
157
157
|
afterMessageIncludes: 'ok',
|
|
158
158
|
afterAuthorId: ctx.discord.botUserId,
|
|
159
159
|
});
|
|
@@ -52,14 +52,11 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
52
52
|
&& message.content.includes('⋅');
|
|
53
53
|
});
|
|
54
54
|
const timeline = await th.text({ showTyping: true });
|
|
55
|
-
expect(timeline).
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
[bot typing]
|
|
59
|
-
|
|
60
|
-
⬥ ok
|
|
61
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
62
|
-
`);
|
|
55
|
+
expect(timeline).toContain('Reply with exactly: typing-stop-normal');
|
|
56
|
+
expect(timeline).toContain('⬥ ok');
|
|
57
|
+
expect(timeline).toContain('*project ⋅ main ⋅');
|
|
58
|
+
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
|
|
59
|
+
expect(typingCount).toBeGreaterThanOrEqual(1);
|
|
63
60
|
expect(replyIndex).toBeGreaterThanOrEqual(0);
|
|
64
61
|
expect(footerIndex).toBeGreaterThan(replyIndex);
|
|
65
62
|
expect(messages[footerIndex]).toBeDefined();
|
|
@@ -134,20 +131,10 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
134
131
|
afterAuthorId: TEST_USER_ID,
|
|
135
132
|
});
|
|
136
133
|
const timeline = await th.text({ showTyping: true });
|
|
137
|
-
expect(timeline).
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
⬥ ok
|
|
142
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
143
|
-
--- from: user (queue-advanced-tester)
|
|
144
|
-
TYPING_REPULSE_MARKER
|
|
145
|
-
[bot typing]
|
|
146
|
-
--- from: assistant (TestBot)
|
|
147
|
-
⬥ repulse-first
|
|
148
|
-
[bot typing]
|
|
149
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
150
|
-
`);
|
|
134
|
+
expect(timeline).toContain('TYPING_REPULSE_MARKER');
|
|
135
|
+
expect(timeline).toContain('⬥ repulse-first');
|
|
136
|
+
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
|
|
137
|
+
expect(typingCount).toBeGreaterThanOrEqual(2);
|
|
151
138
|
const followupUserIndex = messages.findIndex((message) => {
|
|
152
139
|
return message.author.id === TEST_USER_ID
|
|
153
140
|
&& message.content.includes('TYPING_REPULSE_MARKER');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// E2e test: queued message must drain after the user answers a pending question
|
|
2
|
+
// via the Discord dropdown select menu. Reproduces a bug where answering via
|
|
3
|
+
// select (not text) leaves queued messages stuck because the session continues
|
|
4
|
+
// processing after the answer and may enter another blocking state.
|
|
5
|
+
import { describe, test, expect } from 'vitest';
|
|
6
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
7
|
+
import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
8
|
+
import { pendingQuestionContexts } from './commands/ask-question.js';
|
|
9
|
+
const TEXT_CHANNEL_ID = '200000000000001030';
|
|
10
|
+
async function waitForPendingQuestion({ threadId, timeoutMs, }) {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
while (Date.now() - start < timeoutMs) {
|
|
13
|
+
const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
|
|
14
|
+
return context.thread.id === threadId;
|
|
15
|
+
});
|
|
16
|
+
if (entry) {
|
|
17
|
+
return { contextHash: entry[0] };
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, 100);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Timed out waiting for pending question context');
|
|
24
|
+
}
|
|
25
|
+
describe('queue drain after question select answer', () => {
|
|
26
|
+
const ctx = setupQueueAdvancedSuite({
|
|
27
|
+
channelId: TEXT_CHANNEL_ID,
|
|
28
|
+
channelName: 'qa-question-select-drain',
|
|
29
|
+
dirName: 'qa-question-select-drain',
|
|
30
|
+
username: 'question-select-tester',
|
|
31
|
+
});
|
|
32
|
+
test('queued message drains after answering question via dropdown select', async () => {
|
|
33
|
+
// 1. Send a message that triggers the question tool
|
|
34
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
35
|
+
content: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
36
|
+
});
|
|
37
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
38
|
+
timeout: 4_000,
|
|
39
|
+
predicate: (t) => {
|
|
40
|
+
return t.name === 'QUESTION_SELECT_QUEUE_MARKER';
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const th = ctx.discord.thread(thread.id);
|
|
44
|
+
// 2. Wait for the question dropdown to appear
|
|
45
|
+
const pending = await waitForPendingQuestion({
|
|
46
|
+
threadId: thread.id,
|
|
47
|
+
timeoutMs: 4_000,
|
|
48
|
+
});
|
|
49
|
+
expect(pending.contextHash).toBeTruthy();
|
|
50
|
+
// Verify dropdown message appeared
|
|
51
|
+
const questionMessages = await waitForBotMessageContaining({
|
|
52
|
+
discord: ctx.discord,
|
|
53
|
+
threadId: thread.id,
|
|
54
|
+
text: 'How to proceed?',
|
|
55
|
+
timeout: 4_000,
|
|
56
|
+
});
|
|
57
|
+
const questionMsg = questionMessages.find((m) => {
|
|
58
|
+
return m.content.includes('How to proceed?');
|
|
59
|
+
});
|
|
60
|
+
expect(questionMsg).toBeTruthy();
|
|
61
|
+
// 3. Queue a message while question is pending
|
|
62
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
63
|
+
.runSlashCommand({
|
|
64
|
+
name: 'queue',
|
|
65
|
+
options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
|
|
66
|
+
});
|
|
67
|
+
const queueAck = await th.waitForInteractionAck({
|
|
68
|
+
interactionId: queueInteractionId,
|
|
69
|
+
timeout: 4_000,
|
|
70
|
+
});
|
|
71
|
+
if (!queueAck.messageId) {
|
|
72
|
+
throw new Error('Expected /queue response message id');
|
|
73
|
+
}
|
|
74
|
+
// 4. Answer the question via dropdown select (pick first option "Alpha")
|
|
75
|
+
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
76
|
+
messageId: questionMsg.id,
|
|
77
|
+
customId: `ask_question:${pending.contextHash}:0`,
|
|
78
|
+
values: ['0'],
|
|
79
|
+
});
|
|
80
|
+
await th.waitForInteractionAck({
|
|
81
|
+
interactionId: interaction.id,
|
|
82
|
+
timeout: 4_000,
|
|
83
|
+
});
|
|
84
|
+
// 5. Queued message should be handed off to OpenCode's own prompt queue
|
|
85
|
+
// after the question reply, so the dispatch indicator appears without
|
|
86
|
+
// waiting for a later natural idle.
|
|
87
|
+
await waitForBotMessageContaining({
|
|
88
|
+
discord: ctx.discord,
|
|
89
|
+
threadId: thread.id,
|
|
90
|
+
text: '» **question-select-tester:** Reply with exactly: post-question-drain',
|
|
91
|
+
timeout: 4_000,
|
|
92
|
+
});
|
|
93
|
+
// 6. Wait for footer from the drained queued message
|
|
94
|
+
await waitForFooterMessage({
|
|
95
|
+
discord: ctx.discord,
|
|
96
|
+
threadId: thread.id,
|
|
97
|
+
timeout: 4_000,
|
|
98
|
+
afterMessageIncludes: '» **question-select-tester:**',
|
|
99
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
100
|
+
});
|
|
101
|
+
const timeline = await th.text({ showInteractions: true });
|
|
102
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
103
|
+
"--- from: user (question-select-tester)
|
|
104
|
+
QUESTION_SELECT_QUEUE_MARKER
|
|
105
|
+
--- from: assistant (TestBot)
|
|
106
|
+
**Select action**
|
|
107
|
+
How to proceed?
|
|
108
|
+
✓ _Alpha_
|
|
109
|
+
[user interaction]
|
|
110
|
+
Queued message (position 1)
|
|
111
|
+
[user selects dropdown: 0]
|
|
112
|
+
» **question-select-tester:** Reply with exactly: post-question-drain
|
|
113
|
+
⬥ ok
|
|
114
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
115
|
+
`);
|
|
116
|
+
}, 20_000);
|
|
117
|
+
});
|