🗑️ docs: 移除内部设计/实现文档(不随 PR 合入上游)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kunghim
2026-08-01 02:12:06 +08:00
parent f38fdf5732
commit 30822042dd
2 changed files with 0 additions and 569 deletions

View File

@@ -1,476 +0,0 @@
# 查询编辑器「接受 AI 补全」可配置快捷键 — 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 新增可配置快捷键 `acceptSqlAiCompletion`(默认 Right,可改为 Tab 等任意键),在查询编辑器 AI 幽灵补全可见时按下该键即接受补全;幽灵不可见/过期时不拦截按键。
**Architecture:** 快捷键注册进现有 `shortcuts.ts` 数据驱动清单(快捷键管理 UI 自动生效);QueryEditor 用 editor.onKeyDown 单机制处理接受,以 `acceptAiInlineGhost()` 返回 true 为前提才 preventDefault/stopPropagation,取代硬编码 `addCommand(RightArrow, contextKey)` 与 MonacoEditor fallback 补丁。
**Tech Stack:** TypeScript / React / Monaco Editor / Vitest
## Global Constraints
- 分支:`feat/query-editor-accept-ai-completion`(基座 dev `772d8391`,spec 已提交 `a457a5d2`)
- 前端测试命令:`cd frontend && npx vitest run <文件>`(测试在 `frontend/` 下运行)
- 提交信息:emoji 前缀 + 中文描述,结尾 `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`
- 不修改 `queryEditor/QueryEditorAiAssist.ts`(纯逻辑层)与 `triggerSqlAiCompletion`(Alt+\ 触发)行为
- i18n key 必须 6 个语言文件全加:`shared/i18n/{zh-CN,en-US,zh-TW,ja-JP,de-DE,ru-RU}.json`
- 新 action 元数据:`scope: 'queryEditor'``allowInEditable: true``allowWithoutModifier: true``disallowShift: true`、无 `requiredKey`;默认 `Right`(win/mac 相同)
---
### Task 1: 注册 acceptSqlAiCompletion 快捷键
**Files:**
- Modify: `frontend/src/utils/shortcuts.ts`(ShortcutAction 联合类型 ~L5-27、SHORTCUT_ACTION_ORDER ~L107、META_DEFINITIONS ~L152、DEFAULT_SHORTCUT_OPTIONS ~L280)
- Test: `frontend/src/utils/shortcuts.test.ts`(文件末尾追加 describe)
**Interfaces:**
- Produces: `ShortcutAction` 新增 `'acceptSqlAiCompletion'`;`DEFAULT_SHORTCUT_OPTIONS.acceptSqlAiCompletion = { mac: { combo: 'Right', enabled: true }, windows: { combo: 'Right', enabled: true } }`;`SHORTCUT_ACTION_META.acceptSqlAiCompletion` 含 scope/allowInEditable/allowWithoutModifier/disallowShift 元数据;i18n keys `app.shortcuts.action.acceptSqlAiCompletion.label|description`
- [ ] **Step 1: 写失败测试**
`frontend/src/utils/shortcuts.test.ts` 末尾追加:
```ts
describe('acceptSqlAiCompletion', () => {
it('默认绑定为 Right 且启用(win/mac 一致)', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.acceptSqlAiCompletion.mac).toEqual({ combo: 'Right', enabled: true });
expect(DEFAULT_SHORTCUT_OPTIONS.acceptSqlAiCompletion.windows).toEqual({ combo: 'Right', enabled: true });
});
it('允许录制无修饰键的 Tab(裸键场景)', () => {
expect(canRecordShortcutForAction('acceptSqlAiCompletion', 'Tab')).toBe(true);
});
it('拒绝 Shift+Tab(disallowShift)', () => {
expect(canRecordShortcutForAction('acceptSqlAiCompletion', 'Shift+Tab')).toBe(false);
});
it('允许带修饰键组合(如 Ctrl+Right)', () => {
expect(canRecordShortcutForAction('acceptSqlAiCompletion', 'Ctrl+Right')).toBe(true);
});
it('resolveShortcutBinding 对缺失配置回退默认 Right', () => {
const binding = resolveShortcutBinding({}, 'acceptSqlAiCompletion', 'windows');
expect(binding).toEqual({ combo: 'Right', enabled: true });
});
});
```
同时把 `canRecordShortcutForAction``DEFAULT_SHORTCUT_OPTIONS``resolveShortcutBinding` 加入文件顶部已有的 import 列表(检查现有 import,缺则补)。
- [ ] **Step 2: 运行确认失败**
Run: `cd frontend && npx vitest run src/utils/shortcuts.test.ts`
Expected: FAIL — `acceptSqlAiCompletion is undefined` / TS 类型错误。
- [ ] **Step 3: 实现注册**
`frontend/src/utils/shortcuts.ts` 三处修改:
1. `ShortcutAction` 联合类型,在 `'triggerSqlAiCompletion'` 后加 `| 'acceptSqlAiCompletion'`;
2. `SHORTCUT_ACTION_ORDER``'triggerSqlAiCompletion'` 后加 `'acceptSqlAiCompletion'`;
3. `SHORTCUT_ACTION_META_DEFINITIONS` 在 triggerSqlAiCompletion 条目后加:
```ts
acceptSqlAiCompletion: {
labelKey: 'app.shortcuts.action.acceptSqlAiCompletion.label',
descriptionKey: 'app.shortcuts.action.acceptSqlAiCompletion.description',
scope: 'queryEditor',
allowInEditable: true,
allowWithoutModifier: true,
disallowShift: true,
},
```
4. `DEFAULT_SHORTCUT_OPTIONS` 在 triggerSqlAiCompletion 条目后加:
```ts
acceptSqlAiCompletion: {
mac: { combo: 'Right', enabled: true },
windows: { combo: 'Right', enabled: true },
},
```
- [ ] **Step 4: 运行确认通过**
Run: `cd frontend && npx vitest run src/utils/shortcuts.test.ts`
Expected: PASS(新增 5 条 + 原有全过)。
- [ ] **Step 5: 提交**
```bash
git add frontend/src/utils/shortcuts.ts frontend/src/utils/shortcuts.test.ts
git commit -m "✨ feat(shortcuts): 新增接受 SQL AI 自动补全快捷键(默认 Right)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
```
---
### Task 2: i18n 文案(6 语言)
**Files:**
- Modify: `shared/i18n/zh-CN.json``en-US.json``zh-TW.json``ja-JP.json``de-DE.json``ru-RU.json`(每文件在 `app.shortcuts.action.triggerSqlAiCompletion.*` 条目后插入)
**Interfaces:**
- Produces: 6 语言各 2 个 key:`app.shortcuts.action.acceptSqlAiCompletion.label` / `.description`。文案须与 Task 1 注册的 labelKey/descriptionKey 对应;`catalog.test.ts` 完整性测试校验 key 存在性。
- [ ] **Step 1: 写 6 语言条目**
在每个 JSON 的 triggerSqlAiCompletion 条目后插入(对照各文件已有翻译风格):
| 语言 | label | description |
|---|---|---|
| zh-CN | 接受 SQL AI 自动补全 | 当编辑器中显示 AI 灰色补全时,按下此键将补全内容写入编辑器 |
| en-US | Accept SQL AI Completion | When an AI inline completion is shown in the editor, press this key to insert it into the editor |
| zh-TW | 接受 SQL AI 自動補全 | 當編輯器中顯示 AI 灰色補全時,按下此鍵將補全內容寫入編輯器 |
| ja-JP | SQL AI 自動補完を受け入れる | エディターに AI インライン補完が表示されているとき、このキーを押すと補完内容がエディターに挿入されます |
| de-DE | SQL-AI-Vervollständigung übernehmen | Wenn eine KI-Inline-Vervollständigung im Editor angezeigt wird, drücken Sie diese Taste, um sie in den Editor einzufügen |
| ru-RU | Принять автодополнение SQL AI | Когда в редакторе отображается встроенное дополнение ИИ, нажмите эту клавишу, чтобы вставить его в редактор |
- [ ] **Step 2: 跑 i18n 完整性测试确认通过**
Run: `cd frontend && npx vitest run src/i18n/catalogIntegrity.test.ts src/i18n/catalog.test.ts`
Expected: PASS(6 语言 key 齐全;若 catalog.test.ts 有显式 key 清单则按报错补齐)。
- [ ] **Step 3: 提交**
```bash
git add shared/i18n/
git commit -m "🌐 i18n: 接受 SQL AI 自动补全快捷键文案(6 语言)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
```
---
### Task 3: QueryEditor 接入 onKeyDown 接受机制并移除硬编码
**Files:**
- Modify: `frontend/src/components/QueryEditor.tsx`
- 绑定 useMemo:紧跟 `triggerSqlAiCompletionShortcutBinding`(~L1764)后
- ref:紧跟 `triggerAiInlineCompletionRef`(~L1462)后新增 `acceptAiInlineCompletionRef`
- 内部赋值:紧跟 `triggerAiInlineCompletionRef.current = ...`(~L4294)后
- 清理:紧跟 dispose 处 `triggerAiInlineCompletionRef.current = null;`(~L4900)后
- 删除 addCommand(RightArrow, ghostContext)(~L4298-4313 中 context 为 `QUERY_EDITOR_AI_INLINE_CONTEXT_KEY` 的那条,另一条 `inlineSuggestionVisible` 保留)
- 新增 keydown effect:紧跟 `triggerSqlAiCompletionKeydownDisposableRef` effect(~L7906-7944)后
- Test: `frontend/src/components/QueryEditor.external-sql-save.test.tsx`(~L2031-2083 的既有接受断言改为 keydown 触发)
**Interfaces:**
- Consumes: `resolveShortcutBinding``isShortcutMatch`(已 import,确认)、`acceptAiInlineGhost()`(内部作用域函数,经 ref 暴露)
- Produces: `acceptSqlAiCompletionShortcutBinding: ShortcutPlatformBinding`;`acceptAiInlineCompletionRef: React.RefObject<(() => boolean) | null>`(返回 true 表示接受成功);keydown 监听在绑定启用时注册、接受成功才拦截事件。
- [ ] **Step 1: 先改既有测试为 keydown 触发(预期失败)**
`QueryEditor.external-sql-save.test.tsx` ~L2031-2083,把 `acceptInlineGhostCall` 机制替换为 keydown 触发:
```ts
// 替换原来通过 addCommand mock 取回调的写法:
// const acceptInlineGhostCall = editorState.editor.addCommand.mock.calls.find(...)
const acceptRightArrowEvent = () => {
const shortcutEvent = {
type: 'keydown',
key: 'ArrowRight',
code: 'ArrowRight',
keyCode: 39,
which: 39,
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
isComposing: false,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
};
const monacoShortcutEvent = {
browserEvent: shortcutEvent,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
};
editorState.keyDownListeners.forEach((listener) => listener(monacoShortcutEvent));
return { monacoShortcutEvent, shortcutEvent };
};
```
把测试体内 `acceptInlineGhostCall?.[1]?.()` 两处调用替换为 `acceptRightArrowEvent()`,并新增断言:
```ts
const { monacoShortcutEvent } = acceptRightArrowEvent();
...
expect(monacoShortcutEvent.preventDefault).toHaveBeenCalled();
expect(monacoShortcutEvent.stopPropagation).toHaveBeenCalled();
```
- [ ] **Step 2: 运行确认失败**
Run: `cd frontend && npx vitest run src/components/QueryEditor.external-sql-save.test.tsx -t "accepts"`(或直接跑整个文件)
Expected: FAIL — keydown 无监听器,接受不发生,`executeEdits` 未被调用。
- [ ] **Step 3: 实现 keydown 接受机制**
QueryEditor.tsx:
1. 绑定 useMemo(紧跟 `triggerSqlAiCompletionShortcutBinding` 后):
```ts
const acceptSqlAiCompletionShortcutBinding = useMemo(
() => resolveShortcutBinding(shortcutOptions, 'acceptSqlAiCompletion', activeShortcutPlatform),
[activeShortcutPlatform, shortcutOptions],
);
```
2. ref(紧跟 `triggerAiInlineCompletionRef` 后):
```ts
const acceptAiInlineCompletionRef = useRef<(() => boolean) | null>(null);
```
3. 内部赋值(紧跟 `triggerAiInlineCompletionRef.current = () => { requestAiInlineGhost(0, true, true); };` 后):
```ts
acceptAiInlineCompletionRef.current = () => acceptAiInlineGhost();
```
4. dispose 清理(紧跟 `triggerAiInlineCompletionRef.current = null;` 后):
```ts
acceptAiInlineCompletionRef.current = null;
```
5. 删除硬编码 addCommand:去掉 `editor.addCommand?.(monaco.KeyCode.RightArrow, () => { acceptAiInlineGhost(); }, QUERY_EDITOR_AI_INLINE_CONTEXT_KEY);` 整块(保留紧随其后的 `monaco.KeyCode.RightArrow` + `'inlineSuggestionVisible'` 那条)。
6. 新增 keydown effect(紧跟 `triggerSqlAiCompletionKeydownDisposableRef` effect 的 `}, [isActive, ...]);` 后):
```ts
useEffect(() => {
acceptSqlAiCompletionKeydownDisposableRef.current?.dispose?.();
acceptSqlAiCompletionKeydownDisposableRef.current = null;
const editor = editorRef.current;
const binding = acceptSqlAiCompletionShortcutBinding;
if (!editor?.onKeyDown || !binding?.enabled || !binding.combo) {
return;
}
acceptSqlAiCompletionKeydownDisposableRef.current = editor.onKeyDown((event: any) => {
if (!isActive) {
return;
}
const browserEvent = event?.browserEvent || event?.event || event;
if (!browserEvent) {
return;
}
if (!isShortcutMatch(browserEvent, binding.combo)) {
return;
}
// 接受成功才拦截按键;幽灵不存在或已过期时返回 false,键走默认行为。
if (acceptAiInlineCompletionRef.current?.() === true) {
event?.preventDefault?.();
event?.stopPropagation?.();
browserEvent.preventDefault?.();
browserEvent.stopPropagation?.();
}
});
return () => {
acceptSqlAiCompletionKeydownDisposableRef.current?.dispose?.();
acceptSqlAiCompletionKeydownDisposableRef.current = null;
};
}, [isActive, acceptSqlAiCompletionShortcutBinding]);
```
7. ref 声明:紧跟 `triggerSqlAiCompletionKeydownDisposableRef` 的 useRef 声明处加:
```ts
const acceptSqlAiCompletionKeydownDisposableRef = useRef<any>(null);
```
(找到 `const triggerSqlAiCompletionKeydownDisposableRef = useRef<any>(null);` 所在行,在其后插入。若该 ref 是内联声明,保持同风格。)
- [ ] **Step 4: 运行确认通过**
Run: `cd frontend && npx vitest run src/components/QueryEditor.external-sql-save.test.tsx`
Expected: PASS(含 Step 1 更新的断言)。
- [ ] **Step 5: 提交**
```bash
git add frontend/src/components/QueryEditor.tsx frontend/src/components/QueryEditor.external-sql-save.test.tsx
git commit -m "✨ feat(query-editor): AI 补全接受改为可配置快捷键(onKeyDown)
移除硬编码 addCommand(RightArrow, ghostContext),接受以
acceptAiInlineGhost() 成功为前提,幽灵过期时按键不拦截。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
```
---
### Task 4: 删除 MonacoEditor fallback 补丁(死代码)
**Files:**
- Modify: `frontend/src/components/MonacoEditor.tsx`
- 删除 `patchQueryEditorAiInlineRightArrowFallback` 函数定义(~L210 起到函数结束)
- 删除 `handleMount` 中调用 `patchQueryEditorAiInlineRightArrowFallback(editor, monaco);`(~L903)
**Interfaces:**
- Consumes: 无(仅清理)
- Produces: 无。注意 `sameEditorPosition`(L55)被其他函数使用(L453/462/540/748),**必须保留**;`QUERY_EDITOR_AI_INLINE_CONTEXT_KEY` 在 QueryEditor 中仍用于 `createContextKey`(L3906)与 overlay 显示,保留。
- [ ] **Step 1: 先确认无其他引用**
Run: `cd frontend && grep -rn "patchQueryEditorAiInlineRightArrowFallback" src/`
Expected: 仅 MonacoEditor.tsx 两处(定义 + 调用);若测试文件有引用,一并删除对应测试。
- [ ] **Step 2: 删除函数与调用点**
删除 `patchQueryEditorAiInlineRightArrowFallback` 完整函数定义;`handleMount` 中删除该行调用。
- [ ] **Step 3: 跑相关测试确认不破坏**
Run: `cd frontend && npx vitest run src/components/MonacoEditor.test.tsx src/components/QueryEditor.external-sql-save.test.tsx`
(若 MonacoEditor.test.tsx 不存在,改为跑 `src/components/QueryEditor*.test.tsx` 全组)
Expected: PASS。
- [ ] **Step 4: 提交**
```bash
git add frontend/src/components/MonacoEditor.tsx
git commit -m "♻️ refactor(monaco-editor): 删除 AI 补全 RightArrow fallback 死代码
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
```
---
### Task 5: 新增接受行为测试(默认 Right / 可配置 Tab / 不拦截场景)
**Files:**
- Test: `frontend/src/components/QueryEditor.external-sql-save.test.tsx`(在既有 AI inline ghost describe 内追加 it)
**Interfaces:**
- Consumes: Task 3 的 keydown 机制(默认绑定 Right);store mock 的 `shortcutOptions`(手写部分对象,缺失 action 由 `resolveShortcutBinding` 回退默认)
- Produces: 4 条行为测试,固化验收标准。
- [ ] **Step 1: 写失败测试**
在文件中既有 AI inline ghost 测试组内追加以下 4 条 `it`。mount/幽灵渲染流程参照既有测试模板(如 L1700-1807 的 "uses grounded AI inline ghost…",含 `vi.stubGlobal('window', { go: { aiservice: { Service: inlineAiService } }, … })``create(<QueryEditor tab={createTab({ query: 'SELECT * FROM ', dbName: 'main' })} />)``editorState.value`/`editorState.position` 赋值)。幽灵渲染等待:输入后 `vi.advanceTimersByTime(220)` + 循环 `await Promise.resolve()`
Right 键事件构造(4 条测试共用):
```ts
const dispatchAcceptKey = async (key: string, code: string, keyCode: number) => {
const shortcutEvent = {
type: 'keydown',
key,
code,
keyCode,
which: keyCode,
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
isComposing: false,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
};
const monacoShortcutEvent = {
browserEvent: shortcutEvent,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
};
await act(async () => {
editorState.keyDownListeners.forEach((listener) => listener(monacoShortcutEvent));
for (let i = 0; i < 8; i += 1) {
await Promise.resolve();
}
});
return { shortcutEvent, monacoShortcutEvent };
};
```
4 条测试:
```ts
it('幽灵可见时按默认键 Right 接受补全并拦截按键', async () => {
// mount + 渲染幽灵:参照 "uses grounded AI inline ghost…" 模板,
// 输入 'SELECT * FROM ' 后 advanceTimersByTime(220) + Promise 循环,
// 断言 ghostOverlay?.textContent 存在(幽灵已渲染)
const { shortcutEvent, monacoShortcutEvent } = await dispatchAcceptKey('ArrowRight', 'ArrowRight', 39);
expect(editorState.editor.executeEdits).toHaveBeenCalledWith(
'gonavi-ai-inline-sql-completion',
[expect.objectContaining({ text: expect.any(String) })],
);
expect(monacoShortcutEvent.preventDefault).toHaveBeenCalled();
expect(monacoShortcutEvent.stopPropagation).toHaveBeenCalled();
expect(shortcutEvent.preventDefault).toHaveBeenCalled();
expect(shortcutEvent.stopPropagation).toHaveBeenCalled();
});
it('幽灵不可见时按下 Right 不拦截', async () => {
// mount 后不触发任何幽灵渲染
const { shortcutEvent, monacoShortcutEvent } = await dispatchAcceptKey('ArrowRight', 'ArrowRight', 39);
expect(monacoShortcutEvent.preventDefault).not.toHaveBeenCalled();
expect(monacoShortcutEvent.stopPropagation).not.toHaveBeenCalled();
expect(shortcutEvent.preventDefault).not.toHaveBeenCalled();
expect(shortcutEvent.stopPropagation).not.toHaveBeenCalled();
expect(editorState.editor.executeEdits).not.toHaveBeenCalled();
});
it('幽灵已过期(光标移动)时按下 Right 不拦截', async () => {
// 渲染幽灵后,把 editorState.position 改为 { lineNumber: 1, column: 1 }(离开幽灵位置)
editorState.position = { lineNumber: 1, column: 1 };
const { shortcutEvent, monacoShortcutEvent } = await dispatchAcceptKey('ArrowRight', 'ArrowRight', 39);
expect(monacoShortcutEvent.preventDefault).not.toHaveBeenCalled();
expect(shortcutEvent.preventDefault).not.toHaveBeenCalled();
});
it('绑定改为 Tab 后按 Tab 接受补全', async () => {
// mount 前设置 storeState.shortcutOptions.acceptSqlAiCompletion:
// { mac: { enabled: true, combo: 'Tab' }, windows: { enabled: true, combo: 'Tab' } }
// (直接赋值即可;resolveShortcutBinding 读取该对象)
// 渲染幽灵后:
await dispatchAcceptKey('Tab', 'Tab', 9);
expect(editorState.editor.executeEdits).toHaveBeenCalledWith(
'gonavi-ai-inline-sql-completion',
[expect.objectContaining({ text: expect.any(String) })],
);
});
```
- [ ] **Step 2: 运行确认通过**
Run: `cd frontend && npx vitest run src/components/QueryEditor.external-sql-save.test.tsx`
Expected: 新增 4 条 PASS,既有全过。
- [ ] **Step 3: 跑整个前端测试套件确认无回归**
Run: `cd frontend && npx vitest run`
Expected: 全部 PASS。
- [ ] **Step 4: 提交**
```bash
git add frontend/src/components/QueryEditor.external-sql-save.test.tsx
git commit -m "✅ test(query-editor): AI 补全接受快捷键行为测试(默认 Right/可配置 Tab/不拦截场景)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
```
---
## 完成检查
- [ ] 快捷键管理 UI 中「接受 SQL AI 自动补全」出现,默认显示 →
- [ ] 改绑 Tab 后:幽灵可见时 Tab 接受;无幽灵时 Tab 缩进/接受原生 suggest 不受影响
- [ ] 幽灵过期时按绑定键不吞键
- [ ] 全量前端测试通过
- [ ] `git log` 包含 5 个任务提交(1 docs spec + 4 功能提交)
- [ ] push 分支到 origin,`gh pr create` 向 Syngnat/GoNavi base dev(PR 描述按仓库风格,结尾附 Claude Code 生成说明)

View File

@@ -1,93 +0,0 @@
# 查询编辑器「接受 AI 补全」可配置快捷键
日期:2026-08-01
分支:`feat/query-editor-accept-ai-completion`(基于上游 dev `772d8391`)
目标 PR:向 `Syngnat/GoNavi``dev` 提 PR
## 背景
GoNavi 查询编辑器在输入 SQL 时,若已配置 AI 主力 provider,会自动请求 AI 灰色幽灵补全(ghost text),以 overlay 形式显示在光标处。当前「接受」逻辑是**硬编码 RightArrow**(`editor.addCommand(KeyCode.RightArrow, acceptAiInlineGhost, ghostContextKey)`),用户无法修改,且存在一个已知边界:幽灵过期时需 MonacoEditor.tsx 的 fallback 补丁模拟 `cursorRight` 兜底。
需求:在快捷键管理中新增「接受 AI 自动补全」项,默认 RightArrow(保持现有行为),允许用户改为 Tab 或任意键;当幽灵补全可见时按下该键,将 AI 提示内容写入编辑器。
## 目标
1. 新增可配置快捷键 `acceptSqlAiCompletion`,默认 `Right`(win/mac),在快捷键管理 UI 中可见、可录制、有冲突检测。
2. 查询编辑器中该键按下且幽灵可见时接受补全;幽灵不可见或已过期时不拦截按键(走默认行为)。
3. 删除旧机制与死代码:硬编码 RightArrow addCommand + MonacoEditor fallback 补丁。
4. 测试覆盖新机制与默认值。
## 非目标
- 不改动 `triggerSqlAiCompletion`(Alt+\\ 触发幽灵)及其行为。
- 不改动 AI 补全的生成链路(QueryEditorAiAssist 纯逻辑层)。
- 不推 fork 远程 dev;仅本地 dev 同步 + 功能分支 push。
## 设计
### A. 快捷键注册(frontend/src/utils/shortcuts.ts)
- `ShortcutAction` 新增 `'acceptSqlAiCompletion'`,插入 `SHORTCUT_ACTION_ORDER`(位于 `triggerSqlAiCompletion` 之后)。
- `SHORTCUT_ACTION_META_DEFINITIONS` 新增:
- `labelKey: 'app.shortcuts.action.acceptSqlAiCompletion.label'`
- `descriptionKey: 'app.shortcuts.action.acceptSqlAiCompletion.description'`
- `scope: 'queryEditor'`(不进 App.tsx 全局调度器)
- `allowInEditable: true`
- `allowWithoutModifier: true`(允许裸键 Tab/→)
- `disallowShift: true`(避免 Shift+Tab / Shift+→ 与选择、反缩进冲突)
- 不设 `requiredKey`(任意键可绑)
- `DEFAULT_SHORTCUT_OPTIONS` 新增:`mac: { combo: 'Right', enabled: true }``windows: { combo: 'Right', enabled: true }`
### B. i18n(shared/i18n/{zh-CN,en-US,zh-TW,ja-JP,de-DE,ru-RU}.json)
- label:「接受 SQL AI 自动补全」/ "Accept SQL AI Completion"(按各语言现有风格翻译)
- description:当编辑器中显示 AI 灰色补全时,按下此键将补全内容写入编辑器 / "When an AI inline completion is shown in the editor, press this key to insert it"
### C. QueryEditor.tsx 接入(onKeyDown 单机制)
1. 新增绑定解析(与 `triggerSqlAiCompletionShortcutBinding` 并列):
`acceptSqlAiCompletionShortcutBinding = useMemo(() => resolveShortcutBinding(shortcutOptions, 'acceptSqlAiCompletion', activeShortcutPlatform), [activeShortcutPlatform, shortcutOptions])`
2. 新增 effect(与 `triggerSqlAiCompletionKeydownDisposableRef` 同模式):
```
editor.onKeyDown((event) => {
if (!isActive) return;
const browserEvent = event?.browserEvent || event?.event || event;
if (!browserEvent) return;
if (!isShortcutMatch(browserEvent, binding.combo)) return; // IME 已自动过滤
if (acceptAiInlineGhost()) { // 接受成功才拦截
event?.preventDefault?.(); event?.stopPropagation?.();
browserEvent.preventDefault?.(); browserEvent.stopPropagation?.();
}
// 失败(幽灵过期/不存在)→ 不拦截,键走默认行为
});
```
依赖 `[isActive, binding]`,绑定变化自动重新注册。
3. 移除硬编码 `editor.addCommand(KeyCode.RightArrow, acceptAiInlineGhost, QUERY_EDITOR_AI_INLINE_CONTEXT_KEY)`(QueryEditor.tsx 中 RightArrow 两条命令中 context 为 `gonaviAiInlineSuggestionVisible` 的那条;`inlineSuggestionVisible` 那条是 Monaco 原生 inlineSuggest 的,保留)。
4. 删除 MonacoEditor.tsx 的 `patchQueryEditorAiInlineRightArrowFallback` 及其调用点(死代码)。
### D. 测试
1. **QueryEditor.external-sql-save.test.tsx**(约 2031-2083 行):改用模拟 keydown 走 onKeyDown 路径,断言 `executeEdits('gonavi-ai-inline-sql-completion')` 与事件被消费。
2. 新增测试:
- 幽灵可见 + 按下配置键(Right)→ 接受且事件被 preventDefault/stopPropagation
- 幽灵不可见 → 不拦截
- 幽灵过期(位置变化)→ 接受失败 → 不拦截
- 绑定改为 `Tab` → 按 Tab 接受幽灵(可配置性)
3. **shortcuts.test.ts**(或现有相关测试):默认值断言(win/mac = `Right`)、`canRecordShortcutForAction('acceptSqlAiCompletion', 'Tab') === true`、`Shift+Tab` 被拒。
4. i18n 完整性测试(`catalog.test.ts`)自动校验 6 语言 key。
## 风险与注意事项
- `isShortcutMatch` 已处理 IME 组合输入,Tab 在中文输入法组合期间不会被误触发。
- 拦截以 `acceptAiInlineGhost()` 返回 true 为前提,「过期幽灵」场景由默认行为自然兜底,无需 fallback 补丁。
- 快捷键管理 UI 纯数据驱动,注册后自动出现,无需改 App.tsx。
- 绑定字母键时,幽灵可见期间该字母将被吞掉(接受补全)— 属用户自选,默认 Right/Tab 均为非输入键。
## 验收标准
- 查询编辑器出现 AI 幽灵补全 → 按 → 接受(与现行为一致)
- 快捷键管理中该项默认显示 →;改为 Tab 后,幽灵可见时按 Tab 接受,无幽灵时 Tab 照常缩进/接受原生 suggest
- 幽灵过期时按键不被吞
- 全部相关测试通过(vitest)