feat(ai-assist): first-use confirmation + wire Check-for-Updates to updater

Inline AI first-use confirmation (v4.13.0):
  - inline-ai-controller accepts optional confirmFirstUse + getProviderLabel
    deps. On the first action of the session, calls confirmFirstUse with
    {providerLabel, selectionLength, action}; if it returns false, aborts
    and shows error state. Once confirmed, never re-prompts for the
    session.
  - new 'ai-assistant:confirm-info' invoke handler returns
    {provider, model} (no key material) so the renderer can label the
    confirm dialog.
  - electronAPI.aiAssistant.confirmInfo() exposed via preload.
  - renderer.js wires window.confirm() with the provider label as the
    v1 prompt UX; can be upgraded to a custom modal later without
    touching the controller.

Check-for-Updates menu (v4.13.0):
  - Help > Check for Updates now calls ensureAutoUpdater().check()
    instead of opening the Releases page in the browser.
  - Falls back to the browser page if the check throws (dev runs with
    no app-update.yml, network failures, etc.) so the user always has
    a way to update manually.

4 new controller tests cover: confirm called once with provider label
and selection length; declining aborts + surfaces error state; confirm
not asked twice in a session; missing confirmFirstUse dep skips the
prompt and streams normally.

Amit Haridas
This commit is contained in:
Amit Haridas
2026-09-30 21:02:28 +05:30
parent 47315add80
commit e826c0e624
5 changed files with 145 additions and 2 deletions
+69
View File
@@ -269,6 +269,75 @@ describe('createInlineAiController — detach', () => {
});
});
describe('createInlineAiController — first-use confirmation', () => {
test('confirmFirstUse is called with provider label and selection length', () => {
const confirmFirstUse = jest.fn().mockReturnValue(true);
const editor = makeEditor('hello world longer');
editor._setSelection(0, 5);
const { api } = makeApi();
const controller = require('../src/renderer/inline-ai-controller').createInlineAiController({
getEditorView: () => editor,
electronAPI: api,
getProviderLabel: () => 'openai (gpt-4o)',
confirmFirstUse,
});
controller.showForSelection();
getPopoverButton('rewrite').click();
expect(confirmFirstUse).toHaveBeenCalledTimes(1);
expect(confirmFirstUse).toHaveBeenCalledWith(
expect.objectContaining({
providerLabel: 'openai (gpt-4o)',
selectionLength: 5, // selection is 'hello' (5 chars)
action: 'rewrite',
})
);
});
test('declining the confirm aborts and surfaces an error state', () => {
const confirmFirstUse = jest.fn().mockReturnValue(false);
const editor = makeEditor('hello world');
editor._setSelection(0, 5);
const { api, _state } = makeApi();
const controller = require('../src/renderer/inline-ai-controller').createInlineAiController({
getEditorView: () => editor,
electronAPI: api,
confirmFirstUse,
});
controller.showForSelection();
getPopoverButton('rewrite').click();
expect(_state.started).toHaveLength(0);
const popovers = document.querySelectorAll('.inline-ai-popover');
expect(popovers[popovers.length - 1].dataset.state).toBe('error');
});
test('confirm is only asked once per session', () => {
const confirmFirstUse = jest.fn().mockReturnValue(true);
const editor = makeEditor('hello world');
editor._setSelection(0, 5);
const { api } = makeApi();
const controller = require('../src/renderer/inline-ai-controller').createInlineAiController({
getEditorView: () => editor,
electronAPI: api,
confirmFirstUse,
});
controller.showForSelection();
getPopoverButton('rewrite').click();
// Reset selection for a second action — confirm should NOT be asked again
editor._setSelection(0, 5);
getPopoverButton('shorten').click();
expect(confirmFirstUse).toHaveBeenCalledTimes(1);
});
test('no confirmFirstUse dep means no prompt (still streams)', () => {
const editor = makeEditor('hello world');
editor._setSelection(0, 5);
const { controller, _state } = mount({ editor });
controller.showForSelection();
getPopoverButton('rewrite').click();
expect(_state.started).toHaveLength(1);
});
});
describe('createInlineAiController — validation', () => {
test('throws when getEditorView is missing', () => {
expect(() =>