diff --git a/TODO.md b/TODO.md
index 318dc46..72e22d7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -269,6 +269,38 @@ TinyTUI 现在已经具备最小可运行的 C# TUI 框架骨架:终端输入
参考:`tmp/tui/src/autocomplete.ts`、`tmp/tui/src/components/editor.ts`
+本次增量已完成:
+
+- 新增 `TinyTUI.Autocomplete` 模块,定义 `IAutocompleteProvider`、`AutocompleteRequest`、`AutocompleteSuggestions`、`AutocompleteApplyContext` 和 `AutocompleteApplyResult`,先把“查询候选”和“应用候选”从 Editor 中拆成可复用契约
+- 新增 `AutocompleteProviderBase`,提供默认的前缀替换实现,后续文件路径、变量、命令参数等 provider 可以只覆盖查询或特殊应用逻辑
+- 新增 `SlashCommandAutocompleteProvider` 和 `SlashCommand`,支持行首 `/` 命令名候选,确认后自动写回 `/{command} ` 并把光标放到参数位置
+- `Editor` 增加 `AutocompleteProvider`、`AutocompleteList`、`IsAutocompleteActive`、`StartAutocomplete`、`ConfirmAutocomplete`、`CancelAutocomplete` 和 `RenderAutocomplete`,补全列表复用现有 `SelectList` 的选择、滚动和渲染能力
+- 新增 `tui.autocomplete.trigger`、`tui.autocomplete.confirm`、`tui.autocomplete.cancel` 默认动作,Tab 触发或确认补全,Enter 在补全激活时确认,Escape 取消补全
+- 扩展 `TinyTUI.ComponentChecks` 覆盖 slash command 候选渲染、方向键切换候选、Tab 确认应用和 Escape 取消
+
+这样设计的原因:
+
+- provider 契约返回 `items + prefix`,与 `tmp/tui/src/autocomplete.ts` 的核心模型一致,Editor 不需要知道候选来自命令、路径还是特殊前缀
+- 应用候选独立成 `ApplyCompletion`,是为了保留 slash command 自动补空格、路径目录不补空格、引号去重等后续差异,不把这些规则硬塞进 Editor
+- Editor 内部持有 `SelectList`,先把补全选择和确认流程跑通,后续 overlay 只需要挂载 `RenderAutocomplete` 或直接挂载同一个列表组件
+- 补全确认通过现有 `Edit` 包装写回文本,所以撤销栈、redo 清理和 `OnChanged` 行为与普通编辑保持一致
+
+怎么看懂当前代码:
+
+- 先看 `Autocomplete/IAutocompleteProvider.cs`,它定义自动补全框架的两个关键动作:查询候选和应用候选
+- 再看 `Autocomplete/SlashCommandAutocompleteProvider.cs`,它展示了一个最小 provider 如何根据当前行和光标返回候选,以及如何把候选写回文本
+- 最后看 `Components/Editor/Editor.Autocomplete.cs`,这里是 Editor 的补全状态机:启动查询、用 `SelectList` 展示候选、确认应用、取消清理
+- `Components/Editor/Editor.cs` 的 `HandleInput` 入口可以看到补全输入优先级:活动补全先消费导航和确认,普通 Tab 强制触发补全,普通文本输入后尝试自然补全
+
+对比 `tmp/tui` 后续仍需补齐:
+
+- C# 侧 provider 契约已用 `ValueTask` 支持异步形态,但 Editor 当前 `HandleInput` 仍是同步接口,会阻塞等待 provider;后续需要 Runtime 级 invalidate 和取消令牌来实现真正非阻塞异步补全
+- 还没有移植文件路径 provider、`@` 附件前缀、引号路径、`~/` 展开、目录优先排序和 fd fuzzy 搜索
+- 还没有实现 slash command 参数补全,参考实现里的 `getArgumentCompletions` 需要在 C# 侧扩展到 `SlashCommand`
+- 还没有把补全列表通过 `OverlayManager` 自动挂到光标附近,目前只提供 `RenderAutocomplete` 和内部 `SelectList` 供后续 overlay 接线
+- 还没有做补全预览文本、候选变化 debounce、过期请求丢弃和 AbortController 等请求竞争处理
+- 当前默认前缀替换基类按字符串长度处理 prefix,后续支持宽字符或跨行补全时需要把 provider 光标协议统一成 Rune 列或专门的文本范围类型
+
### 8. 图像和特殊终端内容
目标:为 Kitty / iTerm2 图像和特殊终端序列预留完整框架入口
diff --git a/src/TinyTUI/Autocomplete/AutocompleteApplyContext.cs b/src/TinyTUI/Autocomplete/AutocompleteApplyContext.cs
new file mode 100644
index 0000000..65bf02b
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteApplyContext.cs
@@ -0,0 +1,11 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 描述应用一个补全候选时的编辑器上下文
+///
+public sealed record AutocompleteApplyContext(
+ IReadOnlyList Lines,
+ int CursorLine,
+ int CursorColumn,
+ AutocompleteItem Item,
+ string Prefix);
diff --git a/src/TinyTUI/Autocomplete/AutocompleteApplyResult.cs b/src/TinyTUI/Autocomplete/AutocompleteApplyResult.cs
new file mode 100644
index 0000000..f0db0b9
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteApplyResult.cs
@@ -0,0 +1,6 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 表示补全候选应用后的编辑器文本和光标位置
+///
+public sealed record AutocompleteApplyResult(IReadOnlyList Lines, int CursorLine, int CursorColumn);
diff --git a/src/TinyTUI/Autocomplete/AutocompleteItem.cs b/src/TinyTUI/Autocomplete/AutocompleteItem.cs
new file mode 100644
index 0000000..a2f49c8
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteItem.cs
@@ -0,0 +1,12 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 表示自动补全列表中的一个候选项
+///
+public sealed record AutocompleteItem(string Value, string Label, string? Description = null)
+{
+ ///
+ /// 使用同一段文本创建值和标签一致的候选项
+ ///
+ public static AutocompleteItem FromText(string text) => new(text, text);
+}
diff --git a/src/TinyTUI/Autocomplete/AutocompleteProviderBase.cs b/src/TinyTUI/Autocomplete/AutocompleteProviderBase.cs
new file mode 100644
index 0000000..5fdbf99
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteProviderBase.cs
@@ -0,0 +1,28 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 提供通用前缀替换逻辑的自动补全 provider 基类
+///
+public abstract class AutocompleteProviderBase : IAutocompleteProvider
+{
+ ///
+ public abstract ValueTask GetSuggestionsAsync(AutocompleteRequest request);
+
+ ///
+ public virtual AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context)
+ {
+ var lines = context.Lines.ToArray();
+ if (context.CursorLine < 0 || context.CursorLine >= lines.Length)
+ return new AutocompleteApplyResult(lines, context.CursorLine, context.CursorColumn);
+
+ var line = lines[context.CursorLine];
+ var prefixStart = Math.Clamp(context.CursorColumn - context.Prefix.Length, 0, line.Length);
+ var cursor = Math.Clamp(context.CursorColumn, 0, line.Length);
+ lines[context.CursorLine] = line[..prefixStart] + context.Item.Value + line[cursor..];
+
+ return new AutocompleteApplyResult(
+ lines,
+ context.CursorLine,
+ prefixStart + context.Item.Value.Length);
+ }
+}
diff --git a/src/TinyTUI/Autocomplete/AutocompleteRequest.cs b/src/TinyTUI/Autocomplete/AutocompleteRequest.cs
new file mode 100644
index 0000000..ba87112
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteRequest.cs
@@ -0,0 +1,11 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 描述一次自动补全查询所需的编辑器上下文
+///
+public sealed record AutocompleteRequest(
+ IReadOnlyList Lines,
+ int CursorLine,
+ int CursorColumn,
+ bool Force,
+ CancellationToken CancellationToken = default);
diff --git a/src/TinyTUI/Autocomplete/AutocompleteSuggestions.cs b/src/TinyTUI/Autocomplete/AutocompleteSuggestions.cs
new file mode 100644
index 0000000..084eb69
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/AutocompleteSuggestions.cs
@@ -0,0 +1,6 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 表示 provider 返回的自动补全候选和需要替换的前缀
+///
+public sealed record AutocompleteSuggestions(IReadOnlyList Items, string Prefix);
diff --git a/src/TinyTUI/Autocomplete/IAutocompleteProvider.cs b/src/TinyTUI/Autocomplete/IAutocompleteProvider.cs
new file mode 100644
index 0000000..4e2f22d
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/IAutocompleteProvider.cs
@@ -0,0 +1,17 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 为编辑器或命令输入提供自动补全候选
+///
+public interface IAutocompleteProvider
+{
+ ///
+ /// 获取当前光标位置可用的候选项
+ ///
+ ValueTask GetSuggestionsAsync(AutocompleteRequest request);
+
+ ///
+ /// 将用户选中的候选项应用到文本模型
+ ///
+ AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context);
+}
diff --git a/src/TinyTUI/Autocomplete/SlashCommand.cs b/src/TinyTUI/Autocomplete/SlashCommand.cs
new file mode 100644
index 0000000..e82f540
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/SlashCommand.cs
@@ -0,0 +1,6 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 描述一个可由斜杠命令补全的命令
+///
+public sealed record SlashCommand(string Name, string? Description = null, string? ArgumentHint = null);
diff --git a/src/TinyTUI/Autocomplete/SlashCommandAutocompleteProvider.cs b/src/TinyTUI/Autocomplete/SlashCommandAutocompleteProvider.cs
new file mode 100644
index 0000000..278b5b3
--- /dev/null
+++ b/src/TinyTUI/Autocomplete/SlashCommandAutocompleteProvider.cs
@@ -0,0 +1,66 @@
+namespace TinyTUI.Autocomplete;
+
+///
+/// 为行首斜杠命令提供候选项
+///
+public sealed class SlashCommandAutocompleteProvider(IEnumerable commands) : AutocompleteProviderBase
+{
+ private readonly IReadOnlyList _commands = [.. commands];
+
+ ///
+ public override ValueTask GetSuggestionsAsync(AutocompleteRequest request)
+ {
+ if (request.CursorLine < 0 || request.CursorLine >= request.Lines.Count)
+ return ValueTask.FromResult(null);
+
+ var line = request.Lines[request.CursorLine];
+ var cursor = Math.Clamp(request.CursorColumn, 0, line.Length);
+ var beforeCursor = line[..cursor];
+ if (!beforeCursor.StartsWith('/'))
+ return ValueTask.FromResult(null);
+
+ // 当前增量只处理命令名补全 命令参数补全后续由组合 provider 扩展
+ if (beforeCursor.Contains(' ', StringComparison.Ordinal))
+ return ValueTask.FromResult(null);
+
+ var query = beforeCursor[1..];
+ var items = _commands
+ .Where(command => command.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(command => command.Name.StartsWith(query, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
+ .ThenBy(command => command.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(command => new AutocompleteItem(command.Name, command.Name, BuildDescription(command)))
+ .ToArray();
+
+ return ValueTask.FromResult(
+ items.Length == 0 ? null : new AutocompleteSuggestions(items, beforeCursor));
+ }
+
+ ///
+ public override AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context)
+ {
+ var lines = context.Lines.ToArray();
+ if (context.CursorLine < 0 || context.CursorLine >= lines.Length)
+ return new AutocompleteApplyResult(lines, context.CursorLine, context.CursorColumn);
+
+ var line = lines[context.CursorLine];
+ var prefixStart = Math.Clamp(context.CursorColumn - context.Prefix.Length, 0, line.Length);
+ var cursor = Math.Clamp(context.CursorColumn, 0, line.Length);
+ var replacement = "/" + context.Item.Value + " ";
+ lines[context.CursorLine] = line[..prefixStart] + replacement + line[cursor..];
+
+ return new AutocompleteApplyResult(lines, context.CursorLine, prefixStart + replacement.Length);
+ }
+
+ ///
+ /// 合并参数提示和描述用于 SelectList 的说明列
+ ///
+ private static string? BuildDescription(SlashCommand command)
+ {
+ if (command.ArgumentHint is null)
+ return command.Description;
+
+ return command.Description is null
+ ? command.ArgumentHint
+ : $"{command.ArgumentHint} - {command.Description}";
+ }
+}
diff --git a/src/TinyTUI/Components/Editor/Editor.Autocomplete.cs b/src/TinyTUI/Components/Editor/Editor.Autocomplete.cs
new file mode 100644
index 0000000..dc4a4d8
--- /dev/null
+++ b/src/TinyTUI/Components/Editor/Editor.Autocomplete.cs
@@ -0,0 +1,125 @@
+using TinyTUI.Autocomplete;
+using TinyTUI.Input;
+
+namespace TinyTUI.Components;
+
+public sealed partial class Editor
+{
+ ///
+ /// 获取或设置当前编辑器使用的自动补全 provider
+ ///
+ public IAutocompleteProvider? AutocompleteProvider { get; set; }
+
+ ///
+ /// 获取当前是否正在显示补全候选
+ ///
+ public bool IsAutocompleteActive => _autocompleteSuggestions is not null;
+
+ private AutocompleteSuggestions? _autocompleteSuggestions;
+
+ ///
+ /// 渲染当前补全候选列表供 overlay 或外部布局挂载
+ ///
+ public IReadOnlyList RenderAutocomplete(int width)
+ => IsAutocompleteActive ? AutocompleteList.Render(width) : [];
+
+ ///
+ /// 触发补全查询并刷新内部 SelectList
+ ///
+ public bool StartAutocomplete(bool force = false)
+ {
+ if (AutocompleteProvider is not { } provider)
+ return false;
+
+ var request = new AutocompleteRequest([.. _lines], _cursorRow, _cursorColumn, force);
+ var suggestions = provider.GetSuggestionsAsync(request).AsTask().GetAwaiter().GetResult();
+ if (suggestions is null || suggestions.Items.Count == 0)
+ {
+ CancelAutocomplete();
+ return false;
+ }
+
+ _autocompleteSuggestions = suggestions;
+ AutocompleteList.SetItems(suggestions.Items.Select(static item => new SelectItem(item.Value, item.Label, item.Description)));
+ AutocompleteList.SetFilter(string.Empty);
+ AutocompleteList.SetSelectedIndex(0);
+ return true;
+ }
+
+ ///
+ /// 取消当前补全状态
+ ///
+ public void CancelAutocomplete()
+ {
+ _autocompleteSuggestions = null;
+ AutocompleteList.SetItems(Array.Empty());
+ }
+
+ ///
+ /// 将当前选中的补全候选应用到编辑器文本
+ ///
+ public bool ConfirmAutocomplete()
+ {
+ if (_autocompleteSuggestions is not { } suggestions ||
+ AutocompleteProvider is not { } provider ||
+ AutocompleteList.SelectedItemModel is not { } selected)
+ {
+ return false;
+ }
+
+ var item = new AutocompleteItem(selected.Value, selected.Label, selected.Description);
+ var context = new AutocompleteApplyContext([.. _lines], _cursorRow, _cursorColumn, item, suggestions.Prefix);
+
+ Edit(() =>
+ {
+ var result = provider.ApplyCompletion(context);
+ ApplyAutocompleteResult(result);
+ });
+
+ CancelAutocomplete();
+ return true;
+ }
+
+ ///
+ /// 把补全结果写回编辑器内部文本模型并约束光标位置
+ ///
+ private void ApplyAutocompleteResult(AutocompleteApplyResult result)
+ {
+ _lines.Clear();
+ _lines.AddRange(result.Lines.Count == 0 ? [string.Empty] : result.Lines);
+
+ _cursorRow = Math.Clamp(result.CursorLine, 0, _lines.Count - 1);
+ _cursorColumn = Math.Clamp(result.CursorColumn, 0, GetRuneCount(_lines[_cursorRow]));
+ _preferredVisualColumn = null;
+ }
+
+ ///
+ /// 活动补全优先消费选择和确认相关快捷键
+ ///
+ private bool TryHandleAutocompleteInput(TuiInputEvent input)
+ {
+ if (!IsAutocompleteActive)
+ return false;
+
+ if (_keybindings.Matches(input, TuiKeybindings.AutocompleteCancel))
+ {
+ CancelAutocomplete();
+ return true;
+ }
+
+ if (_keybindings.Matches(input, TuiKeybindings.AutocompleteConfirm))
+ return ConfirmAutocomplete();
+
+ // 候选列表继续复用 SelectList 的导航逻辑 保持滚动和选择事件一致
+ if (_keybindings.Matches(input, TuiKeybindings.SelectUp) ||
+ _keybindings.Matches(input, TuiKeybindings.SelectDown) ||
+ _keybindings.Matches(input, TuiKeybindings.SelectHome) ||
+ _keybindings.Matches(input, TuiKeybindings.SelectEnd))
+ {
+ AutocompleteList.HandleInput(input);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/TinyTUI/Components/Editor/Editor.cs b/src/TinyTUI/Components/Editor/Editor.cs
index 0c481bc..b206520 100644
--- a/src/TinyTUI/Components/Editor/Editor.cs
+++ b/src/TinyTUI/Components/Editor/Editor.cs
@@ -6,10 +6,10 @@ namespace TinyTUI.Components;
///
/// 支持多行文本编辑的基础编辑器组件
///
-public sealed partial class Editor(ITextMeasurer? textMeasurer = null, KeybindingRegistry? keybindings = null) : IInputComponent, IFocusableComponent
+public sealed partial class Editor : IInputComponent, IFocusableComponent
{
- private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
- private readonly KeybindingRegistry _keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
+ private readonly ITextMeasurer _textMeasurer;
+ private readonly KeybindingRegistry _keybindings;
private readonly List _lines = [string.Empty];
private readonly Stack _undoStack = [];
private readonly Stack _redoStack = [];
@@ -23,6 +23,20 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
private int _scrollOffset;
private int? _preferredVisualColumn;
+ ///
+ /// 创建多行编辑器组件
+ ///
+ public Editor(ITextMeasurer? textMeasurer = null, KeybindingRegistry? keybindings = null)
+ {
+ _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
+ _keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
+ AutocompleteList = new SelectList(_textMeasurer, _keybindings)
+ {
+ Height = 5,
+ Focused = true,
+ };
+ }
+
///
/// 获取或设置编辑器高度
///
@@ -41,6 +55,11 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
///
public bool Focused { get; set; }
+ ///
+ /// 获取编辑器内部复用的补全列表组件
+ ///
+ public SelectList AutocompleteList { get; }
+
///
/// 获取当前编辑器文本
///
@@ -68,15 +87,26 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
///
public void HandleInput(TuiInputEvent input)
{
+ if (TryHandleAutocompleteInput(input))
+ return;
+
switch (input)
{
case { Kind: TuiInputEventKind.Text }:
Edit(() => InsertText(input.Value));
+ StartAutocomplete(force: false);
break;
case { Kind: TuiInputEventKind.Paste }:
Edit(() => InsertText(input.Value.Replace("\r\n", "\n")));
+ CancelAutocomplete();
break;
case { Kind: TuiInputEventKind.Key }:
+ if (_keybindings.Matches(input, TuiKeybindings.AutocompleteTrigger))
+ {
+ StartAutocomplete(force: true);
+ return;
+ }
+
HandleKey(input);
break;
}
diff --git a/src/TinyTUI/Input/TuiKeybindings.cs b/src/TinyTUI/Input/TuiKeybindings.cs
index fa0b8df..4314390 100644
--- a/src/TinyTUI/Input/TuiKeybindings.cs
+++ b/src/TinyTUI/Input/TuiKeybindings.cs
@@ -22,6 +22,9 @@ public static class TuiKeybindings
public const string EditorClear = "tui.editor.clear";
public const string EditorNewLine = "tui.editor.newLine";
public const string EditorSubmit = "tui.editor.submit";
+ public const string AutocompleteTrigger = "tui.autocomplete.trigger";
+ public const string AutocompleteConfirm = "tui.autocomplete.confirm";
+ public const string AutocompleteCancel = "tui.autocomplete.cancel";
public const string InputNewLine = "tui.input.newLine";
public const string InputSubmit = "tui.input.submit";
public const string InputCancel = "tui.input.cancel";
@@ -54,6 +57,9 @@ public static class TuiKeybindings
Define(EditorClear, [KeyNames.Ctrl("c")], "Clear current input"),
Define(EditorNewLine, [KeyNames.Enter], "Insert editor newline"),
Define(EditorSubmit, [KeyNames.Ctrl(KeyNames.Enter), KeyNames.Ctrl("s")], "Submit editor input"),
+ Define(AutocompleteTrigger, [KeyNames.Tab], "Trigger autocomplete"),
+ Define(AutocompleteConfirm, [KeyNames.Tab, KeyNames.Enter], "Confirm autocomplete"),
+ Define(AutocompleteCancel, [KeyNames.Escape], "Cancel autocomplete"),
Define(InputNewLine, [KeyNames.Shift(KeyNames.Enter)], "Insert newline"),
Define(InputSubmit, [KeyNames.Enter], "Submit input"),
Define(InputCancel, [KeyNames.Escape], "Cancel input"),
diff --git a/test/TinyTUI.ComponentChecks/Program.cs b/test/TinyTUI.ComponentChecks/Program.cs
index 41987d6..e2e48da 100644
--- a/test/TinyTUI.ComponentChecks/Program.cs
+++ b/test/TinyTUI.ComponentChecks/Program.cs
@@ -1,4 +1,5 @@
using TinyTUI.Components;
+using TinyTUI.Autocomplete;
using TinyTUI.Input;
using TinyTUI.Rendering;
using TinyTUI.Text;
@@ -41,6 +42,29 @@ loader.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
AssertTrue(loader.IsCanceled, "cancellable loader token");
AssertTrue(canceled, "cancellable loader callback");
+var autocompleteEditor = new Editor(measurer)
+{
+ AutocompleteProvider = new SlashCommandAutocompleteProvider(
+ [
+ new SlashCommand("help", "show help"),
+ new SlashCommand("history", "show history"),
+ ]),
+};
+autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Text, "/h"));
+AssertTrue(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete active");
+AssertTrue(autocompleteEditor.RenderAutocomplete(40).Any(line => line.Contains("help", StringComparison.Ordinal)), "slash command autocomplete render");
+autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Down));
+autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
+AssertEqual("/history ", autocompleteEditor.Value, "slash command autocomplete confirm");
+AssertFalse(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete closes after confirm");
+
+autocompleteEditor.Value = "/h";
+autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
+AssertTrue(autocompleteEditor.IsAutocompleteActive, "forced autocomplete active");
+autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
+AssertFalse(autocompleteEditor.IsAutocompleteActive, "autocomplete cancel");
+AssertEqual("/h", autocompleteEditor.Value, "autocomplete cancel keeps text");
+
Console.WriteLine("TinyTUI component checks passed");
static void AssertEqual(T expected, T actual, string name)