deliver slash commands typed so the Claude TUI runs them
The composer routed everything through tmux paste-buffer -p (bracketed paste), and Claude's TUI deliberately doesn't parse a leading slash on pasted input — so /command and /skill arrived as literal text instead of running. Now a recognised command (single-line, leading slash, token in ClaudeConfig's slash list) is delivered via send-keys -l (typed) so the TUI fires it; everything else keeps the bracketed-paste path, which also leaves a stray leading slash (e.g. a /tmp path) as literal text rather than mis-parsing it. The slash list is warmed lazily when a Claude pane opens so custom commands are recognised. T-153. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'claude_banner.dart';
|
||||
import 'claude_composer.dart';
|
||||
import 'claude_config.dart';
|
||||
import 'claude_status.dart';
|
||||
import 'clipboard_paste.dart';
|
||||
import 'conversation_controller.dart';
|
||||
import 'conversation_view.dart';
|
||||
import 'session_naming.dart';
|
||||
import 'slash_commands.dart';
|
||||
import 'tmux_session.dart' as tmux;
|
||||
import 'transcript_publisher.dart';
|
||||
import 'transcript_reader.dart';
|
||||
@@ -90,6 +92,9 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
if (!_spawned) {
|
||||
_spawned = true;
|
||||
unawaited(_spawnWhenReady());
|
||||
// Warm the slash-command list in the background (lazy, idempotent) so
|
||||
// custom commands are recognised by the time the user types one (T-153).
|
||||
unawaited(activeClaudeConfig?.ensureProbe());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +303,16 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
void _send(String text) {
|
||||
if (_usingTmux) {
|
||||
final session = _sessionName;
|
||||
if (session != null) unawaited(tmux.sendMessage(session, text));
|
||||
if (session == null) return;
|
||||
// Recognised slash commands go typed (so the TUI fires them); anything
|
||||
// else is bracketed-pasted, keeping multi-line text and stray leading
|
||||
// slashes literal (T-153).
|
||||
final known = activeClaudeConfig?.slashCommands ?? kFallbackSlashCommands;
|
||||
if (isKnownSlashCommand(text, known)) {
|
||||
unawaited(tmux.sendCommand(session, text));
|
||||
} else {
|
||||
unawaited(tmux.sendMessage(session, text));
|
||||
}
|
||||
return;
|
||||
}
|
||||
final id = _paneId;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/// Pure helpers for recognising slash-command input in the Claude composer
|
||||
/// (T-153). Kept Flutter-free so they're cheap to unit-test.
|
||||
///
|
||||
/// Claude's TUI only treats a leading `/` as a command when the input is
|
||||
/// *typed*, not bracketed-pasted. clide delivers recognised commands typed
|
||||
/// (so they fire) and everything else bracketed-pasted (so multi-line text
|
||||
/// and stray leading slashes — e.g. a path like `/tmp/x.log` — arrive as
|
||||
/// literal text rather than being mis-parsed). Recognition is by the known
|
||||
/// command set, so unknown `/...` input stays literal; that's intentionally
|
||||
/// smarter than the CLI, which would reject it.
|
||||
library;
|
||||
|
||||
/// The command token of a single-line slash input — `"/model sonnet"` →
|
||||
/// `"model"` — or null when [text] isn't single-line leading-slash input.
|
||||
String? slashCommandToken(String text) {
|
||||
if (text.contains('\n') || !text.startsWith('/')) return null;
|
||||
final rest = text.substring(1);
|
||||
final ws = rest.indexOf(RegExp(r'\s'));
|
||||
final token = ws < 0 ? rest : rest.substring(0, ws);
|
||||
return token.isEmpty ? null : token;
|
||||
}
|
||||
|
||||
/// Whether [text] is a recognised slash command given the [known] set, so it
|
||||
/// should be delivered typed (firing the command) rather than bracketed-pasted.
|
||||
bool isKnownSlashCommand(String text, Iterable<String> known) {
|
||||
final token = slashCommandToken(text);
|
||||
return token != null && known.contains(token);
|
||||
}
|
||||
@@ -80,3 +80,13 @@ Future<void> sendMessage(String session, String text) async {
|
||||
await tmuxRunner([..._socket, 'paste-buffer', '-p', '-d', '-b', _composeBuffer, '-t', session]);
|
||||
await tmuxRunner([..._socket, 'send-keys', '-t', session, 'Enter']);
|
||||
}
|
||||
|
||||
/// Submit [text] to [session] as TYPED input (literal keystrokes, no bracketed
|
||||
/// paste), then Enter — so Claude's TUI parses a leading `/` as a slash
|
||||
/// command, exactly as if the user had typed it (T-153). For single-line
|
||||
/// slash-command input only; regular messages go through [sendMessage] so
|
||||
/// bracketed paste keeps multi-line content and stray slashes literal.
|
||||
Future<void> sendCommand(String session, String text) async {
|
||||
await tmuxRunner([..._socket, 'send-keys', '-t', session, '-l', '--', text]);
|
||||
await tmuxRunner([..._socket, 'send-keys', '-t', session, 'Enter']);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user