From c0bcbda77f3cc8697c9603d6f695e4c88b1aa611 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:34:38 +0800 Subject: [PATCH] Revert "feat: AI entry button at the cell's bottom-right + clear input after success" This reverts commit fd30e53e25e9cddd8b4d83346d22729ac825306e. --- src/__tests__/opencode_cell_actions.spec.ts | 56 ++++----------------- src/components/opencode_cell_actions.ts | 48 ++++-------------- src/components/opencode_inline_prompt.ts | 8 --- style/base.css | 38 +++++++------- 4 files changed, 40 insertions(+), 110 deletions(-) diff --git a/src/__tests__/opencode_cell_actions.spec.ts b/src/__tests__/opencode_cell_actions.spec.ts index 7482a58..371c9db 100644 --- a/src/__tests__/opencode_cell_actions.spec.ts +++ b/src/__tests__/opencode_cell_actions.spec.ts @@ -194,11 +194,8 @@ describe('OpenCodeCellActions', () => { it('renders a single AI button', () => { const cell = makeFakeCell('x = 1'); - // Constructing the actions widget has the side effect of appending - // the floating AI button to cell.node; the widget reference itself - // is not needed in this test. - new OpenCodeCellActions(cell); - const btns = cell.node.querySelectorAll('button.opencode-cell-ai-btn'); + const actions = new OpenCodeCellActions(cell); + const btns = actions.node.querySelectorAll('button'); expect(btns.length).toBe(1); expect(btns[0].textContent).toContain('AI'); }); @@ -213,11 +210,8 @@ describe('OpenCodeCellActions', () => { contentChanged: { connect: jest.fn(), disconnect: jest.fn() }, stateChanged: { connect: jest.fn(), disconnect: jest.fn() } }; - (cell as any).node = document.createElement('div'); - new OpenCodeCellActions(cell); - const btn = cell.node.querySelector( - 'button.opencode-cell-ai-btn' - ) as HTMLButtonElement; + const actions = new OpenCodeCellActions(cell); + const btn = actions.node.querySelector('button') as HTMLButtonElement; expect(btn.disabled).toBe(true); }); @@ -227,7 +221,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); const prompt = cell.node.querySelector( @@ -242,7 +236,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); btn.click(); expect(cell.node.querySelector('.opencode-inline-prompt')).toBeNull(); @@ -280,7 +274,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); const prompt = cell.node.querySelector( @@ -325,7 +319,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); const modelSel = cell.node.querySelector( @@ -354,7 +348,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); const prompt = cell.node.querySelector( @@ -390,7 +384,7 @@ describe('OpenCodeCellActions', () => { Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); expect( @@ -409,7 +403,7 @@ describe('OpenCodeCellActions', () => { // has a target and the cell source check has a baseline). Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); - const btn = cell.node.querySelector('button.opencode-cell-ai-btn') as HTMLButtonElement; + const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); // The history refresh on show is async; spy on _refreshHistory to @@ -428,34 +422,6 @@ describe('OpenCodeCellActions', () => { // A history refresh was triggered (to pick up the new assistant msg). expect(refreshSpy).toHaveBeenCalledWith('foo.ipynb'); }); - - it('clears the input textarea after a successful response', () => { - setOpenCodeProviders(null); - const cell = makeFakeCell('x = 1'); - const actions = new OpenCodeCellActions(cell); - Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); - (actions as any).onAfterAttach({} as any); - - // Open the prompt and type something into the textarea. - const aiBtn = cell.node.querySelector( - 'button.opencode-cell-ai-btn' - ) as HTMLButtonElement; - aiBtn.click(); - const prompt = (actions as any)._prompt; - expect(prompt).not.toBeNull(); - const textarea = prompt._textarea as HTMLTextAreaElement; - textarea.value = 'make it faster'; - - // A successful response clears the input so the user can type a - // follow-up message. - (actions as any)._handleResponse({ - ok: true, - markdown: 'ok', - sessionId: 'ses-1', - notebookPath: 'foo.ipynb' - }); - expect(textarea.value).toBe(''); - }); }); describe('OpenCodeInlinePrompt', () => { diff --git a/src/components/opencode_cell_actions.ts b/src/components/opencode_cell_actions.ts index b70085c..d64bfaf 100644 --- a/src/components/opencode_cell_actions.ts +++ b/src/components/opencode_cell_actions.ts @@ -50,22 +50,11 @@ export class OpenCodeCellActions extends Widget { private _context: CellContext | null = null; private _status: Status = 'idle'; private _prompt: OpenCodeInlinePrompt | null = null; - // Floating "AI" entry button that lives at the cell's bottom-right - // corner (overlaid on the cell, absolutely positioned). The widget's - // own node stays inside the native cell toolbar (as a hidden lifecycle - // holder) and is NOT where the user-visible button renders. - private _floatingBtn: HTMLButtonElement | null = null; constructor(cell: CodeCell) { super(); this._cell = cell; this.addClass('opencode-cell-actions'); - // Hide the widget's own node inside the native cell toolbar — the - // actual UI is the floating button below. - this.node.style.display = 'none'; - // Anchor the absolutely-positioned floating button to the cell. - this._cell.node.style.position = 'relative'; - this._createFloatingButton(); this._cell.model.contentChanged.connect(this._onModelChange, this); this._cell.model.stateChanged.connect(this._onModelChange, this); this._render(); @@ -79,26 +68,9 @@ export class OpenCodeCellActions extends Widget { this._cell.model.contentChanged.disconnect(this._onModelChange, this); this._cell.model.stateChanged.disconnect(this._onModelChange, this); this._hidePrompt(); - if (this._floatingBtn && this._floatingBtn.parentNode) { - this._floatingBtn.parentNode.removeChild(this._floatingBtn); - } - this._floatingBtn = null; super.dispose(); } - private _createFloatingButton(): void { - const btn = document.createElement('button'); - btn.className = 'opencode-cell-ai-btn'; - btn.type = 'button'; - btn.textContent = 'AI'; - btn.title = '让 AI 修改这个 cell 的代码'; - btn.addEventListener('click', () => { - this._togglePrompt(); - }); - this._cell.node.appendChild(btn); - this._floatingBtn = btn; - } - private _onModelChange(): void { this._context = extractCellContextFromCell(this._cell); if (this._prompt) { @@ -109,18 +81,22 @@ export class OpenCodeCellActions extends Widget { } private _render(): void { - // The widget's own node no longer hosts the button (it lives at the - // cell's bottom-right). Just update the floating button's label and - // disabled state. - const btn = this._floatingBtn; - if (!btn) { - return; - } + const node = this.node; + node.textContent = ''; + const baseDisabled = !this._context || this._status === 'loading'; + + const btn = document.createElement('button'); + btn.className = 'opencode-btn opencode-btn-ai'; + btn.textContent = 'AI'; btn.disabled = baseDisabled; btn.title = baseDisabled ? 'OpenCode: 等待 cell 上下文…' : '让 AI 修改这个 cell 的代码'; + btn.addEventListener('click', () => { + this._togglePrompt(); + }); + node.appendChild(btn); } private _togglePrompt(): void { @@ -228,8 +204,6 @@ export class OpenCodeCellActions extends Widget { if (notebookPath) { void this._refreshHistory(notebookPath); } - // Clear the input so the user can type a follow-up message. - this._prompt?.clearInput(); Notification.info( `OpenCode 完成 (${resp.markdown.length} chars). Session: ${resp.sessionId.slice(0, 8)}…` ); diff --git a/src/components/opencode_inline_prompt.ts b/src/components/opencode_inline_prompt.ts index c987551..9d692c8 100644 --- a/src/components/opencode_inline_prompt.ts +++ b/src/components/opencode_inline_prompt.ts @@ -367,12 +367,4 @@ export class OpenCodeInlinePrompt extends Widget { cell.model.sharedModel.setSource(text); Notification.info('已替换单元格内容'); } - - /** - * Clear the user input textarea (called by the cell action after a - * successful response, so the user can type a follow-up message). - */ - clearInput(): void { - this._textarea.value = ''; - } } diff --git a/style/base.css b/style/base.css index a6347bc..6c23365 100644 --- a/style/base.css +++ b/style/base.css @@ -4,31 +4,29 @@ https://jupyterlab.readthedocs.io/en/stable/developer/css.html */ -/* AI entry button: rendered as a floating pill at the bottom-right - corner of the cell. The widget that owns it (opencode-cell-actions) - is registered in the native cell toolbar but its own node is - display:none — only this floating button is visible. */ -.opencode-cell-ai-btn { - position: absolute; - bottom: 4px; - right: 4px; - z-index: 10; - border: 1px solid var(--jp-border-color1, #999); - background: var(--jp-layout-color1, #fff); - color: var(--jp-ui-font-color1, #333); - padding: 1px 8px; - border-radius: 10px; - font-size: var(--jp-ui-font-size0, 11px); - line-height: 1.3; +/* Single AI action button inside the native Cell toolbar (active cell, top-right). */ +.opencode-cell-actions { + display: flex; + align-items: center; +} + +.opencode-cell-actions .opencode-btn { + border: none; + background: transparent; + padding: 0 6px; + font-size: var(--jp-ui-font-size1); + line-height: 20px; cursor: pointer; + white-space: nowrap; } -.opencode-cell-ai-btn:hover { - background: var(--jp-layout-color2, #f0f0f0); +.opencode-cell-actions .opencode-btn:hover:enabled { + background: var(--jp-layout-color2); + border-radius: 2px; } -.opencode-cell-ai-btn:disabled { - opacity: 0.5; +.opencode-cell-actions .opencode-btn:disabled { + opacity: 0.4; cursor: default; }