| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | tea "charm.land/bubbletea/v2" |
| 8 | |
| 9 | "reasonix/internal/control" |
| 10 | "reasonix/internal/i18n" |
| 11 | "reasonix/internal/skill" |
| 12 | ) |
| 13 | |
| 14 | // runUnrecognizedSlash resolves slash input that is not a built-in command, in |
| 15 | // the order a name must win: built-in docs pages, then a custom command, then |
| 16 | // a skill, then an extension action, and finally prose sent as a plain message. |
| 17 | // cmd is the canonicalized spelling used in the unknown-command notice. |
| 18 | func (m *chatTUI) runUnrecognizedSlash(input, typedCmd, cmd string) tea.Cmd { |
| 19 | if control.IsBuiltinDocsSlash(typedCmd, m.commands, m.skills) { |
| 20 | query := strings.TrimSpace(strings.TrimPrefix(input, typedCmd)) |
| 21 | if query != "" { |
| 22 | return m.startControllerTurn(input, input, func(ctrl control.SessionAPI) { ctrl.SubmitDisplay(input, input) }) |
| 23 | } |
| 24 | m.echoLocalCommand(input) |
| 25 | text, err := control.DocsCommandOverviewFor(typedCmd) |
| 26 | if err != nil { |
| 27 | m.notice("docs: " + err.Error()) |
| 28 | } else { |
| 29 | m.commitLine(text) |
| 30 | } |
| 31 | return nil |
| 32 | } |
| 33 | // A custom command wins over a skill of the same name; both resolve to a turn. |
| 34 | if sent, ok := m.ctrl.CustomCommand(input); ok { |
| 35 | return m.startTurn(sent, input, input) |
| 36 | } |
| 37 | if _, ok := m.ctrl.RunSkill(input); ok { |
| 38 | fields := strings.Fields(input) |
| 39 | name := strings.TrimPrefix(fields[0], "/") |
| 40 | for _, sk := range m.ctrl.Skills() { |
| 41 | if sk.Name == name && sk.RunAs == skill.RunSubagent && len(fields) == 1 { |
| 42 | m.echoLocalCommand(input) |
| 43 | m.notice("usage: /" + name + " <task>") |
| 44 | return nil |
| 45 | } |
| 46 | } |
| 47 | return m.startControllerTurn(input, input, func(ctrl control.SessionAPI) { ctrl.SubmitDisplay(input, input) }) |
| 48 | } |
| 49 | // An extension action (/<plugin>:<action>) resolves last, before the |
| 50 | // unknown-command fallback; the invocation is a sidecar round-trip, so it |
| 51 | // runs off the event loop and its result lands as a notice. |
| 52 | if action, ok := matchExtensionAction(m.ctrl, typedCmd); ok { |
| 53 | m.echoLocalCommand(input) |
| 54 | return m.runExtensionAction(action.Slash, parseExtensionActionArgs(strings.Fields(input)[1:])) |
| 55 | } |
| 56 | // Unknown slash input is prose more often than a typo — send it as a |
| 57 | // regular message (matching the controller's behavior for the other |
| 58 | // surfaces), with a notice so real typos stay visible (#5756). |
| 59 | m.notice(fmt.Sprintf("%s: %s — %s", i18n.M.SlashUnknown, cmd, i18n.M.SlashUnknownSentAsMessage)) |
| 60 | return m.startTurn(input, input, input) |
| 61 | } |
| 62 |