kimaki 0.4.33 → 0.4.35
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/dist/cli.js +10 -1
- package/dist/commands/session.js +55 -0
- package/dist/logger.js +13 -6
- package/dist/opencode.js +1 -2
- package/dist/session-handler.js +53 -15
- package/dist/system-message.js +13 -0
- package/dist/unnest-code-blocks.js +10 -3
- package/dist/unnest-code-blocks.test.js +231 -12
- package/package.json +5 -6
- package/src/cli.ts +11 -1
- package/src/commands/session.ts +68 -0
- package/src/logger.ts +20 -12
- package/src/opencode.ts +1 -2
- package/src/session-handler.ts +61 -19
- package/src/system-message.ts +13 -0
- package/src/unnest-code-blocks.test.ts +242 -12
- package/src/unnest-code-blocks.ts +10 -3
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.35",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "tsx --env-file .env src/cli.ts",
|
|
8
8
|
"prepublishOnly": "pnpm tsc",
|
|
@@ -30,30 +30,29 @@
|
|
|
30
30
|
"tsx": "^4.20.5"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@ai-sdk/google": "^2.0.47",
|
|
34
33
|
"@clack/prompts": "^0.11.0",
|
|
35
|
-
"@discordjs/opus": "^0.10.0",
|
|
36
34
|
"@discordjs/voice": "^0.19.0",
|
|
37
35
|
"@google/genai": "^1.34.0",
|
|
38
36
|
"@opencode-ai/sdk": "^1.1.12",
|
|
39
37
|
"@purinton/resampler": "^1.0.4",
|
|
40
|
-
"@snazzah/davey": "^0.1.6",
|
|
41
38
|
"ai": "^5.0.114",
|
|
42
39
|
"better-sqlite3": "^12.3.0",
|
|
43
40
|
"cac": "^6.7.14",
|
|
44
41
|
"discord.js": "^14.16.3",
|
|
45
42
|
"domhandler": "^5.0.3",
|
|
46
43
|
"glob": "^13.0.0",
|
|
47
|
-
"go-try": "^3.0.2",
|
|
48
44
|
"htmlparser2": "^10.0.0",
|
|
49
45
|
"js-yaml": "^4.1.0",
|
|
50
46
|
"marked": "^16.3.0",
|
|
51
47
|
"picocolors": "^1.1.1",
|
|
52
48
|
"pretty-ms": "^9.3.0",
|
|
53
|
-
"prism-media": "^1.3.5",
|
|
54
49
|
"ripgrep-js": "^3.0.0",
|
|
55
50
|
"string-dedent": "^3.0.2",
|
|
56
51
|
"undici": "^7.16.0",
|
|
57
52
|
"zod": "^4.2.1"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"@discordjs/opus": "^0.10.0",
|
|
56
|
+
"prism-media": "^1.3.5"
|
|
58
57
|
}
|
|
59
58
|
}
|
package/src/cli.ts
CHANGED
|
@@ -197,6 +197,14 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
197
197
|
|
|
198
198
|
return option
|
|
199
199
|
})
|
|
200
|
+
.addStringOption((option) => {
|
|
201
|
+
option
|
|
202
|
+
.setName('agent')
|
|
203
|
+
.setDescription('Agent to use for this session')
|
|
204
|
+
.setAutocomplete(true)
|
|
205
|
+
|
|
206
|
+
return option
|
|
207
|
+
})
|
|
200
208
|
.toJSON(),
|
|
201
209
|
new SlashCommandBuilder()
|
|
202
210
|
.setName('add-project')
|
|
@@ -279,7 +287,9 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
279
287
|
continue
|
|
280
288
|
}
|
|
281
289
|
|
|
282
|
-
|
|
290
|
+
// Sanitize command name: oh-my-opencode uses MCP commands with colons, which Discord doesn't allow
|
|
291
|
+
const sanitizedName = cmd.name.replace(/:/g, '-')
|
|
292
|
+
const commandName = `${sanitizedName}-cmd`
|
|
283
293
|
const description = cmd.description || `Run /${cmd.name} command`
|
|
284
294
|
|
|
285
295
|
commands.push(
|
package/src/commands/session.ts
CHANGED
|
@@ -21,6 +21,7 @@ export async function handleSessionCommand({
|
|
|
21
21
|
|
|
22
22
|
const prompt = command.options.getString('prompt', true)
|
|
23
23
|
const filesString = command.options.getString('files') || ''
|
|
24
|
+
const agent = command.options.getString('agent') || undefined
|
|
24
25
|
const channel = command.channel
|
|
25
26
|
|
|
26
27
|
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
@@ -91,6 +92,7 @@ export async function handleSessionCommand({
|
|
|
91
92
|
thread,
|
|
92
93
|
projectDirectory,
|
|
93
94
|
channelId: textChannel.id,
|
|
95
|
+
agent,
|
|
94
96
|
})
|
|
95
97
|
} catch (error) {
|
|
96
98
|
logger.error('[SESSION] Error:', error)
|
|
@@ -100,12 +102,78 @@ export async function handleSessionCommand({
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
async function handleAgentAutocomplete({
|
|
106
|
+
interaction,
|
|
107
|
+
appId,
|
|
108
|
+
}: AutocompleteContext): Promise<void> {
|
|
109
|
+
const focusedValue = interaction.options.getFocused()
|
|
110
|
+
|
|
111
|
+
let projectDirectory: string | undefined
|
|
112
|
+
|
|
113
|
+
if (interaction.channel) {
|
|
114
|
+
const channel = interaction.channel
|
|
115
|
+
if (channel.type === ChannelType.GuildText) {
|
|
116
|
+
const textChannel = channel as TextChannel
|
|
117
|
+
if (textChannel.topic) {
|
|
118
|
+
const extracted = extractTagsArrays({
|
|
119
|
+
xml: textChannel.topic,
|
|
120
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
121
|
+
})
|
|
122
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
123
|
+
if (channelAppId && channelAppId !== appId) {
|
|
124
|
+
await interaction.respond([])
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!projectDirectory) {
|
|
133
|
+
await interaction.respond([])
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
139
|
+
|
|
140
|
+
const agentsResponse = await getClient().app.agents({
|
|
141
|
+
query: { directory: projectDirectory },
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
145
|
+
await interaction.respond([])
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const agents = agentsResponse.data
|
|
150
|
+
.filter((a) => a.mode === 'primary' || a.mode === 'all')
|
|
151
|
+
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
152
|
+
.slice(0, 25)
|
|
153
|
+
|
|
154
|
+
const choices = agents.map((agent) => ({
|
|
155
|
+
name: agent.name.slice(0, 100),
|
|
156
|
+
value: agent.name,
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
await interaction.respond(choices)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('[AUTOCOMPLETE] Error fetching agents:', error)
|
|
162
|
+
await interaction.respond([])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
103
166
|
export async function handleSessionAutocomplete({
|
|
104
167
|
interaction,
|
|
105
168
|
appId,
|
|
106
169
|
}: AutocompleteContext): Promise<void> {
|
|
107
170
|
const focusedOption = interaction.options.getFocused(true)
|
|
108
171
|
|
|
172
|
+
if (focusedOption.name === 'agent') {
|
|
173
|
+
await handleAgentAutocomplete({ interaction, appId })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
109
177
|
if (focusedOption.name !== 'files') {
|
|
110
178
|
return
|
|
111
179
|
}
|
package/src/logger.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { log } from '@clack/prompts'
|
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path, { dirname } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import util from 'node:util'
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
11
12
|
const __dirname = dirname(__filename)
|
|
@@ -22,36 +23,43 @@ if (isDev) {
|
|
|
22
23
|
fs.writeFileSync(logFilePath, `--- kimaki log started at ${new Date().toISOString()} ---\n`)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
+
function formatArg(arg: unknown): string {
|
|
27
|
+
if (typeof arg === 'string') {
|
|
28
|
+
return arg
|
|
29
|
+
}
|
|
30
|
+
return util.inspect(arg, { colors: true, depth: 4 })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeToFile(level: string, prefix: string, args: unknown[]) {
|
|
26
34
|
if (!isDev) {
|
|
27
35
|
return
|
|
28
36
|
}
|
|
29
37
|
const timestamp = new Date().toISOString()
|
|
30
|
-
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(
|
|
38
|
+
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`
|
|
31
39
|
fs.appendFileSync(logFilePath, message)
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export function createLogger(prefix: string) {
|
|
35
43
|
return {
|
|
36
|
-
log: (...args:
|
|
44
|
+
log: (...args: unknown[]) => {
|
|
37
45
|
writeToFile('INFO', prefix, args)
|
|
38
|
-
log.info([`[${prefix}]`, ...args.map(
|
|
46
|
+
log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
|
|
39
47
|
},
|
|
40
|
-
error: (...args:
|
|
48
|
+
error: (...args: unknown[]) => {
|
|
41
49
|
writeToFile('ERROR', prefix, args)
|
|
42
|
-
log.error([`[${prefix}]`, ...args.map(
|
|
50
|
+
log.error([`[${prefix}]`, ...args.map(formatArg)].join(' '))
|
|
43
51
|
},
|
|
44
|
-
warn: (...args:
|
|
52
|
+
warn: (...args: unknown[]) => {
|
|
45
53
|
writeToFile('WARN', prefix, args)
|
|
46
|
-
log.warn([`[${prefix}]`, ...args.map(
|
|
54
|
+
log.warn([`[${prefix}]`, ...args.map(formatArg)].join(' '))
|
|
47
55
|
},
|
|
48
|
-
info: (...args:
|
|
56
|
+
info: (...args: unknown[]) => {
|
|
49
57
|
writeToFile('INFO', prefix, args)
|
|
50
|
-
log.info([`[${prefix}]`, ...args.map(
|
|
58
|
+
log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
|
|
51
59
|
},
|
|
52
|
-
debug: (...args:
|
|
60
|
+
debug: (...args: unknown[]) => {
|
|
53
61
|
writeToFile('DEBUG', prefix, args)
|
|
54
|
-
log.info([`[${prefix}]`, ...args.map(
|
|
62
|
+
log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
|
|
55
63
|
},
|
|
56
64
|
}
|
|
57
65
|
}
|
package/src/opencode.ts
CHANGED
|
@@ -115,8 +115,7 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
115
115
|
|
|
116
116
|
const port = await getOpenPort()
|
|
117
117
|
|
|
118
|
-
const
|
|
119
|
-
const opencodeCommand = process.env.OPENCODE_PATH || `${opencodeBinDir}/opencode`
|
|
118
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
120
119
|
|
|
121
120
|
const serverProcess = spawn(
|
|
122
121
|
opencodeCommand,
|
package/src/session-handler.ts
CHANGED
|
@@ -6,14 +6,14 @@ import type { Part, PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
|
6
6
|
import type { FilePartInput } from '@opencode-ai/sdk'
|
|
7
7
|
import type { Message, ThreadChannel } from 'discord.js'
|
|
8
8
|
import prettyMilliseconds from 'pretty-ms'
|
|
9
|
-
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent } from './database.js'
|
|
9
|
+
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent, setSessionAgent } from './database.js'
|
|
10
10
|
import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js'
|
|
11
11
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
12
12
|
import { formatPart } from './message-formatting.js'
|
|
13
13
|
import { getOpencodeSystemMessage } from './system-message.js'
|
|
14
14
|
import { createLogger } from './logger.js'
|
|
15
15
|
import { isAbortError } from './utils.js'
|
|
16
|
-
import { showAskUserQuestionDropdowns, cancelPendingQuestion } from './commands/ask-question.js'
|
|
16
|
+
import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts } from './commands/ask-question.js'
|
|
17
17
|
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
18
18
|
|
|
19
19
|
const sessionLogger = createLogger('SESSION')
|
|
@@ -141,6 +141,7 @@ export async function handleOpencodeSession({
|
|
|
141
141
|
images = [],
|
|
142
142
|
channelId,
|
|
143
143
|
command,
|
|
144
|
+
agent,
|
|
144
145
|
}: {
|
|
145
146
|
prompt: string
|
|
146
147
|
thread: ThreadChannel
|
|
@@ -150,6 +151,8 @@ export async function handleOpencodeSession({
|
|
|
150
151
|
channelId?: string
|
|
151
152
|
/** If set, uses session.command API instead of session.prompt */
|
|
152
153
|
command?: { name: string; arguments: string }
|
|
154
|
+
/** Agent to use for this session */
|
|
155
|
+
agent?: string
|
|
153
156
|
}): Promise<{ sessionID: string; result: any; port?: number } | undefined> {
|
|
154
157
|
voiceLogger.log(
|
|
155
158
|
`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
|
|
@@ -209,6 +212,12 @@ export async function handleOpencodeSession({
|
|
|
209
212
|
.run(thread.id, session.id)
|
|
210
213
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`)
|
|
211
214
|
|
|
215
|
+
// Store agent preference if provided
|
|
216
|
+
if (agent) {
|
|
217
|
+
setSessionAgent(session.id, agent)
|
|
218
|
+
sessionLogger.log(`Set agent preference for session ${session.id}: ${agent}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
const existingController = abortControllers.get(session.id)
|
|
213
222
|
if (existingController) {
|
|
214
223
|
voiceLogger.log(
|
|
@@ -239,11 +248,10 @@ export async function handleOpencodeSession({
|
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
|
|
242
|
-
// Cancel any pending question tool if user sends a new message
|
|
251
|
+
// Cancel any pending question tool if user sends a new message (silently, no thread message)
|
|
243
252
|
const questionCancelled = await cancelPendingQuestion(thread.id)
|
|
244
253
|
if (questionCancelled) {
|
|
245
254
|
sessionLogger.log(`[QUESTION] Cancelled pending question due to new message`)
|
|
246
|
-
await sendThreadMessage(thread, `⚠️ Previous question cancelled - processing your new message`)
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
const abortController = new AbortController()
|
|
@@ -433,7 +441,14 @@ export async function handleOpencodeSession({
|
|
|
433
441
|
}
|
|
434
442
|
|
|
435
443
|
if (part.type === 'step-start') {
|
|
436
|
-
|
|
444
|
+
// Don't start typing if user needs to respond to a question or permission
|
|
445
|
+
const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
|
|
446
|
+
(ctx) => ctx.thread.id === thread.id,
|
|
447
|
+
)
|
|
448
|
+
const hasPendingPermission = pendingPermissions.has(thread.id)
|
|
449
|
+
if (!hasPendingQuestion && !hasPendingPermission) {
|
|
450
|
+
stopTyping = startTyping()
|
|
451
|
+
}
|
|
437
452
|
}
|
|
438
453
|
|
|
439
454
|
if (part.type === 'tool' && part.state.status === 'running') {
|
|
@@ -475,6 +490,12 @@ export async function handleOpencodeSession({
|
|
|
475
490
|
await sendPartMessage(part)
|
|
476
491
|
}
|
|
477
492
|
|
|
493
|
+
// Send text parts when complete (time.end is set)
|
|
494
|
+
// Text parts stream incrementally; only send when finished to avoid partial text
|
|
495
|
+
if (part.type === 'text' && part.time?.end) {
|
|
496
|
+
await sendPartMessage(part)
|
|
497
|
+
}
|
|
498
|
+
|
|
478
499
|
if (part.type === 'step-finish') {
|
|
479
500
|
for (const p of currentParts) {
|
|
480
501
|
if (p.type !== 'step-start' && p.type !== 'step-finish') {
|
|
@@ -483,6 +504,12 @@ export async function handleOpencodeSession({
|
|
|
483
504
|
}
|
|
484
505
|
setTimeout(() => {
|
|
485
506
|
if (abortController.signal.aborted) return
|
|
507
|
+
// Don't restart typing if user needs to respond to a question or permission
|
|
508
|
+
const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
|
|
509
|
+
(ctx) => ctx.thread.id === thread.id,
|
|
510
|
+
)
|
|
511
|
+
const hasPendingPermission = pendingPermissions.has(thread.id)
|
|
512
|
+
if (hasPendingQuestion || hasPendingPermission) return
|
|
486
513
|
stopTyping = startTyping()
|
|
487
514
|
}, 300)
|
|
488
515
|
}
|
|
@@ -527,6 +554,12 @@ export async function handleOpencodeSession({
|
|
|
527
554
|
`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
|
|
528
555
|
)
|
|
529
556
|
|
|
557
|
+
// Stop typing - user needs to respond now, not the bot
|
|
558
|
+
if (stopTyping) {
|
|
559
|
+
stopTyping()
|
|
560
|
+
stopTyping = null
|
|
561
|
+
}
|
|
562
|
+
|
|
530
563
|
// Show dropdown instead of text message
|
|
531
564
|
const { messageId, contextHash } = await showPermissionDropdown({
|
|
532
565
|
thread,
|
|
@@ -569,6 +602,12 @@ export async function handleOpencodeSession({
|
|
|
569
602
|
`Question requested: id=${questionRequest.id}, questions=${questionRequest.questions.length}`,
|
|
570
603
|
)
|
|
571
604
|
|
|
605
|
+
// Stop typing - user needs to respond now, not the bot
|
|
606
|
+
if (stopTyping) {
|
|
607
|
+
stopTyping()
|
|
608
|
+
stopTyping = null
|
|
609
|
+
}
|
|
610
|
+
|
|
572
611
|
// Flush any pending text/reasoning parts before showing the dropdown
|
|
573
612
|
// This ensures text the LLM generated before the question tool is shown first
|
|
574
613
|
for (const p of currentParts) {
|
|
@@ -584,6 +623,12 @@ export async function handleOpencodeSession({
|
|
|
584
623
|
requestId: questionRequest.id,
|
|
585
624
|
input: { questions: questionRequest.questions },
|
|
586
625
|
})
|
|
626
|
+
} else if (event.type === 'session.idle') {
|
|
627
|
+
// Session is done processing - abort to signal completion
|
|
628
|
+
if (event.properties.sessionID === session.id) {
|
|
629
|
+
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
|
|
630
|
+
abortController.abort('finished')
|
|
631
|
+
}
|
|
587
632
|
}
|
|
588
633
|
}
|
|
589
634
|
} catch (e) {
|
|
@@ -789,20 +834,17 @@ export async function handleOpencodeSession({
|
|
|
789
834
|
discordLogger.log(`Could not update reaction:`, e)
|
|
790
835
|
}
|
|
791
836
|
}
|
|
792
|
-
const
|
|
793
|
-
error
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
typeof error
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
await sendThreadMessage(
|
|
803
|
-
thread,
|
|
804
|
-
`✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`,
|
|
805
|
-
)
|
|
837
|
+
const errorDisplay = (() => {
|
|
838
|
+
if (error instanceof Error) {
|
|
839
|
+
const name = error.constructor.name || 'Error'
|
|
840
|
+
return `[${name}]\n${error.stack || error.message}`
|
|
841
|
+
}
|
|
842
|
+
if (typeof error === 'string') {
|
|
843
|
+
return error
|
|
844
|
+
}
|
|
845
|
+
return String(error)
|
|
846
|
+
})()
|
|
847
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: ${errorDisplay}`)
|
|
806
848
|
}
|
|
807
849
|
}
|
|
808
850
|
}
|
package/src/system-message.ts
CHANGED
|
@@ -66,5 +66,18 @@ headings are discouraged anyway. instead try to use bold text for titles which r
|
|
|
66
66
|
## diagrams
|
|
67
67
|
|
|
68
68
|
you can create diagrams wrapping them in code blocks.
|
|
69
|
+
|
|
70
|
+
## ending conversations with options
|
|
71
|
+
|
|
72
|
+
IMPORTANT: At the end of each response, especially after completing a task or presenting a plan, use the question tool to offer the user clear options for what to do next.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
- After showing a plan: offer "Start implementing?" with Yes/No options
|
|
76
|
+
- After completing edits: offer "Commit changes?" with Yes/No options
|
|
77
|
+
- After debugging: offer "How to proceed?" with options like "Apply fix", "Investigate further", "Try different approach"
|
|
78
|
+
|
|
79
|
+
The user can always select "Other" to type a custom response if the provided options don't fit their needs, or if the plan needs updating.
|
|
80
|
+
|
|
81
|
+
This makes the interaction more guided and reduces friction for the user.
|
|
69
82
|
`
|
|
70
83
|
}
|