Skip to content

Commit dc730f4

Browse files
committed
feat(cli): add command factory pattern with graceful arg handling
- Add defineCommand and defineCommandWithArgs factory functions - defineCommand: gracefully ignores args (no error shown) - defineCommandWithArgs: passes args to handler for processing - Add acceptsArgs property to CommandDefinition type - Update feedback command to pre-fill form when args provided - Update new command to send args as first message after clearing - Update mode commands to send args as message in new mode - Update publish command to pre-select agents from args - Fix Tab to complete word (not execute) when single match - Fix click to execute slash command immediately - Commands that accept args: feedback, bash, referral, image, publish, new, mode:* - Commands that ignore args: login, logout, exit, init, usage - Add comprehensive tests for factory pattern and arg handling
1 parent 2871a8c commit dc730f4

File tree

11 files changed

+414
-220
lines changed

11 files changed

+414
-220
lines changed

cli/src/chat.tsx

Lines changed: 112 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ export const Chat = ({
700700
})
701701
})
702702

703-
// handleSlashItemClick is defined later after openPublishMode is available
703+
// handleSlashItemClick is defined later after feedback/publish stores are available
704704

705705
const handleMentionItemClick = useCallback(
706706
(index: number) => {
@@ -779,24 +779,52 @@ export const Chat = ({
779779

780780
const publishMutation = usePublishMutation()
781781

782-
// Click handler for slash menu items (defined here after openPublishMode is available)
782+
// Click handler for slash menu items - executes command immediately
783783
const handleSlashItemClick = useCallback(
784784
async (index: number) => {
785785
const selected = slashMatches[index]
786786
if (!selected) return
787+
787788
// Execute the selected slash command immediately
788789
const commandString = `/${selected.id}`
789790
setSlashSelectedIndex(0)
791+
792+
ensureQueueActiveBeforeSubmit()
790793
const result = await onSubmitPrompt(commandString, agentMode)
794+
791795
if (result?.openFeedbackMode) {
796+
// Save the feedback text that was set by the command handler before opening feedback mode
797+
const prefilledText = useFeedbackStore.getState().feedbackText
798+
const prefilledCursor = useFeedbackStore.getState().feedbackCursor
792799
saveCurrentInput('', 0)
793800
openFeedbackForMessage(null)
801+
// Restore the prefilled text after openFeedbackForMessage resets it
802+
if (prefilledText) {
803+
useFeedbackStore.getState().setFeedbackText(prefilledText)
804+
useFeedbackStore.getState().setFeedbackCursor(prefilledCursor)
805+
}
794806
}
795807
if (result?.openPublishMode) {
796-
openPublishMode()
808+
if (result.preSelectAgents && result.preSelectAgents.length > 0) {
809+
// preSelectAgents already sets publishMode: true, so don't call openPublishMode
810+
// which would reset the selectedAgentIds
811+
preSelectAgents(result.preSelectAgents)
812+
} else {
813+
openPublishMode()
814+
}
797815
}
798816
},
799-
[slashMatches, setSlashSelectedIndex, onSubmitPrompt, agentMode, saveCurrentInput, openFeedbackForMessage, openPublishMode],
817+
[
818+
slashMatches,
819+
setSlashSelectedIndex,
820+
ensureQueueActiveBeforeSubmit,
821+
onSubmitPrompt,
822+
agentMode,
823+
saveCurrentInput,
824+
openFeedbackForMessage,
825+
openPublishMode,
826+
preSelectAgents,
827+
],
800828
)
801829

802830
const inputValueRef = useRef(inputValue)
@@ -902,16 +930,24 @@ export const Chat = ({
902930
})
903931

904932
if (result?.openFeedbackMode) {
933+
// Save the feedback text that was set by the command handler before opening feedback mode
934+
const prefilledText = useFeedbackStore.getState().feedbackText
935+
const prefilledCursor = useFeedbackStore.getState().feedbackCursor
905936
saveCurrentInput('', 0)
906937
openFeedbackForMessage(null)
938+
// Restore the prefilled text after openFeedbackForMessage resets it
939+
if (prefilledText) {
940+
useFeedbackStore.getState().setFeedbackText(prefilledText)
941+
useFeedbackStore.getState().setFeedbackCursor(prefilledCursor)
942+
}
907943
}
908944

909945
if (result?.openPublishMode) {
910946
if (result.preSelectAgents && result.preSelectAgents.length > 0) {
911-
// Pre-select agents and skip to confirmation
947+
// preSelectAgents already sets publishMode: true, so don't call openPublishMode
948+
// which would reset the selectedAgentIds
912949
preSelectAgents(result.preSelectAgents)
913950
} else {
914-
// Open selection UI
915951
openPublishMode()
916952
}
917953
}
@@ -1028,28 +1064,9 @@ export const Chat = ({
10281064
},
10291065
onSlashMenuDown: () => setSlashSelectedIndex((prev) => prev + 1),
10301066
onSlashMenuUp: () => setSlashSelectedIndex((prev) => prev - 1),
1031-
onSlashMenuTab: async () => {
1032-
// If only one match, execute it immediately (unless the command opts out)
1033-
if (slashMatches.length === 1) {
1034-
const selected = slashMatches[0]
1035-
if (!selected) return
1036-
// Don't auto-execute commands that opt out (e.g. /exit)
1037-
if (selected.noTabAutoExecute) {
1038-
return
1039-
}
1040-
const commandString = `/${selected.id}`
1041-
setSlashSelectedIndex(0)
1042-
const result = await onSubmitPrompt(commandString, agentMode)
1043-
if (result?.openFeedbackMode) {
1044-
saveCurrentInput('', 0)
1045-
openFeedbackForMessage(null)
1046-
}
1047-
if (result?.openPublishMode) {
1048-
openPublishMode()
1049-
}
1050-
return
1051-
}
1052-
// Otherwise cycle through options
1067+
onSlashMenuTab: () => {
1068+
// Do nothing if there's only one match - user needs to press Enter to select
1069+
if (slashMatches.length <= 1) return
10531070
setSlashSelectedIndex((prev) => (prev + 1) % slashMatches.length)
10541071
},
10551072
onSlashMenuShiftTab: () =>
@@ -1059,18 +1076,52 @@ export const Chat = ({
10591076
onSlashMenuSelect: async () => {
10601077
const selected = slashMatches[slashSelectedIndex] || slashMatches[0]
10611078
if (!selected) return
1079+
10621080
// Execute the selected slash command immediately
10631081
const commandString = `/${selected.id}`
10641082
setSlashSelectedIndex(0)
1083+
1084+
ensureQueueActiveBeforeSubmit()
10651085
const result = await onSubmitPrompt(commandString, agentMode)
1086+
10661087
if (result?.openFeedbackMode) {
1088+
// Save the feedback text that was set by the command handler before opening feedback mode
1089+
const prefilledText = useFeedbackStore.getState().feedbackText
1090+
const prefilledCursor = useFeedbackStore.getState().feedbackCursor
10671091
saveCurrentInput('', 0)
10681092
openFeedbackForMessage(null)
1093+
// Restore the prefilled text after openFeedbackForMessage resets it
1094+
if (prefilledText) {
1095+
useFeedbackStore.getState().setFeedbackText(prefilledText)
1096+
useFeedbackStore.getState().setFeedbackCursor(prefilledCursor)
1097+
}
10691098
}
10701099
if (result?.openPublishMode) {
1071-
openPublishMode()
1100+
if (result.preSelectAgents && result.preSelectAgents.length > 0) {
1101+
// preSelectAgents already sets publishMode: true, so don't call openPublishMode
1102+
// which would reset the selectedAgentIds
1103+
preSelectAgents(result.preSelectAgents)
1104+
} else {
1105+
openPublishMode()
1106+
}
10721107
}
10731108
},
1109+
onSlashMenuComplete: () => {
1110+
// Complete the word without executing - same as clicking on the item
1111+
const selected = slashMatches[slashSelectedIndex] || slashMatches[0]
1112+
if (!selected || slashContext.startIndex < 0) return
1113+
const before = inputValue.slice(0, slashContext.startIndex)
1114+
const after = inputValue.slice(
1115+
slashContext.startIndex + 1 + slashContext.query.length,
1116+
)
1117+
const replacement = `/${selected.id} `
1118+
setInputValue({
1119+
text: before + replacement + after,
1120+
cursorPosition: before.length + replacement.length,
1121+
lastEditDueToNav: false,
1122+
})
1123+
setSlashSelectedIndex(0)
1124+
},
10741125
onMentionMenuDown: () => setAgentSelectedIndex((prev) => prev + 1),
10751126
onMentionMenuUp: () => setAgentSelectedIndex((prev) => prev - 1),
10761127
onMentionMenuTab: () => {
@@ -1114,6 +1165,33 @@ export const Chat = ({
11141165
// Try current selection, fall back to first item
11151166
trySelectAtIndex(agentSelectedIndex) || trySelectAtIndex(0)
11161167
},
1168+
onMentionMenuComplete: () => {
1169+
// Complete the word without executing - same as select for mentions
1170+
if (mentionContext.startIndex < 0) return
1171+
1172+
let replacement: string
1173+
const index = agentSelectedIndex
1174+
if (index < agentMatches.length) {
1175+
const selected = agentMatches[index] || agentMatches[0]
1176+
if (!selected) return
1177+
replacement = `@${selected.displayName} `
1178+
} else {
1179+
const fileIndex = index - agentMatches.length
1180+
const selectedFile = fileMatches[fileIndex] || fileMatches[0]
1181+
if (!selectedFile) return
1182+
replacement = `@${selectedFile.filePath} `
1183+
}
1184+
const before = inputValue.slice(0, mentionContext.startIndex)
1185+
const after = inputValue.slice(
1186+
mentionContext.startIndex + 1 + mentionContext.query.length,
1187+
)
1188+
setInputValue({
1189+
text: before + replacement + after,
1190+
cursorPosition: before.length + replacement.length,
1191+
lastEditDueToNav: false,
1192+
})
1193+
setAgentSelectedIndex(0)
1194+
},
11171195
onOpenFileMenuWithTab: () => {
11181196
const safeCursor = Math.max(
11191197
0,
@@ -1187,8 +1265,13 @@ export const Chat = ({
11871265
setSlashSelectedIndex,
11881266
slashMatches,
11891267
slashSelectedIndex,
1268+
ensureQueueActiveBeforeSubmit,
11901269
onSubmitPrompt,
11911270
agentMode,
1271+
saveCurrentInput,
1272+
openFeedbackForMessage,
1273+
openPublishMode,
1274+
preSelectAgents,
11921275
setAgentSelectedIndex,
11931276
agentMatches,
11941277
fileMatches,

0 commit comments

Comments
 (0)