Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat(ai-chat): auto-apply + position modes (side/bottom/popup)' (#358) from feat/ai-chat-auto-apply-positions into main

scott c9bf0bfd 8c636a82

+149 -4
+9
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ## [0.31.0] — 2026-04-10 11 + 12 + ### Added 13 + - AI Chat: "Auto-apply all edits" setting — when on, every new action card is applied on arrival with no confirmation; turning it on also retroactively applies any pending cards already in the conversation (#604) 14 + - AI Chat: position cycle button in the header — click to toggle between `side` (right dock), `bottom` (bottom drawer, resizable vertically), and `popup` (floating window, resizable both directions). Persisted per-browser (#604) 15 + 16 + ### Changed 17 + - AI Chat: default side-dock width bumped from 360px to 400px so the chat has more breathing room by default (#604) 18 + 10 19 ## [0.30.3] — 2026-04-10 11 20 12 21 ### Fixed
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.30.3", 3 + "version": "0.31.0", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",
+51 -1
src/css/app.css
··· 7820 7820 7821 7821 .ai-chat-sidebar { 7822 7822 position: relative; 7823 - width: 360px; 7823 + width: 400px; 7824 7824 border-left: 1px solid var(--color-border); 7825 7825 background: var(--color-surface); 7826 7826 display: flex; 7827 7827 flex-direction: column; 7828 7828 overflow: hidden; 7829 7829 flex-shrink: 0; 7830 + } 7831 + 7832 + /* --- Position modes: side (default) / bottom / popup --- */ 7833 + 7834 + .ai-chat-sidebar[data-pos="bottom"] { 7835 + position: fixed; 7836 + left: 0; 7837 + right: 0; 7838 + bottom: 0; 7839 + top: auto; 7840 + width: 100%; 7841 + height: 360px; 7842 + min-height: 200px; 7843 + max-height: 85vh; 7844 + border-left: none; 7845 + border-top: 1px solid var(--color-border); 7846 + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08); 7847 + z-index: var(--z-tooltip); 7848 + resize: vertical; 7849 + } 7850 + 7851 + [data-theme="dark"] .ai-chat-sidebar[data-pos="bottom"] { 7852 + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.4); 7853 + } 7854 + 7855 + .ai-chat-sidebar[data-pos="popup"] { 7856 + position: fixed; 7857 + right: 24px; 7858 + bottom: 24px; 7859 + top: auto; 7860 + left: auto; 7861 + width: 440px; 7862 + height: min(640px, calc(100vh - 48px)); 7863 + min-width: 320px; 7864 + min-height: 320px; 7865 + max-width: calc(100vw - 48px); 7866 + max-height: calc(100vh - 48px); 7867 + border: 1px solid var(--color-border); 7868 + border-radius: var(--radius-md); 7869 + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18); 7870 + z-index: var(--z-tooltip); 7871 + resize: both; 7872 + } 7873 + 7874 + [data-theme="dark"] .ai-chat-sidebar[data-pos="popup"] { 7875 + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6); 7876 + } 7877 + 7878 + .ai-chat-position-btn { 7879 + position: relative; 7830 7880 } 7831 7881 7832 7882 .ai-chat-header {
+23
src/lib/ai-chat/sidebar-dom.ts
··· 22 22 clearBtn: HTMLButtonElement; 23 23 settingsBtn: HTMLButtonElement; 24 24 closeBtn: HTMLButtonElement; 25 + positionBtn: HTMLButtonElement; 25 26 settingsPanel: HTMLElement; 26 27 endpointInput: HTMLInputElement; 27 28 modelSelect: HTMLSelectElement; 28 29 contextToggle: HTMLInputElement; 29 30 actionsToggle: HTMLInputElement; 31 + autoApplyToggle: HTMLInputElement; 30 32 } { 31 33 const container = document.createElement('div'); 32 34 container.className = 'ai-chat-sidebar'; ··· 42 44 <span class="ai-chat-model-badge" id="ai-model-badge"></span> 43 45 </div> 44 46 <div class="ai-chat-header-actions"> 47 + <button class="btn-icon ai-chat-position-btn" id="ai-chat-position-btn" title="Change position (side / bottom / popup)" aria-label="Change chat position"> 48 + <svg class="tb-icon ai-chat-pos-icon ai-chat-pos-icon--side" viewBox="0 0 16 16" style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="1"/><line x1="10" y1="2.5" x2="10" y2="13.5"/></svg> 49 + <svg class="tb-icon ai-chat-pos-icon ai-chat-pos-icon--bottom" viewBox="0 0 16 16" style="width:14px;height:14px;display:none" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="1"/><line x1="1.5" y1="10" x2="14.5" y2="10"/></svg> 50 + <svg class="tb-icon ai-chat-pos-icon ai-chat-pos-icon--popup" viewBox="0 0 16 16" style="width:14px;height:14px;display:none" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.5" y="1.5" width="10" height="9" rx="1"/><rect x="5.5" y="5.5" width="9" height="9" rx="1"/></svg> 51 + </button> 45 52 <button class="btn-icon ai-chat-settings-btn" id="ai-chat-settings-btn" title="Settings" aria-label="Chat settings"> 46 53 <svg class="tb-icon" viewBox="0 0 16 16" style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="2.5"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg> 47 54 </button> ··· 75 82 Allow content edits 76 83 </label> 77 84 </div> 85 + <div class="ai-chat-settings-field ai-chat-context-row"> 86 + <label class="ai-chat-toggle-label"> 87 + <input type="checkbox" id="ai-auto-apply-toggle"> 88 + Auto-apply all edits (no confirmation) 89 + </label> 90 + </div> 78 91 <details class="ai-chat-advanced"> 79 92 <summary>Advanced</summary> 80 93 <div class="ai-chat-settings-field"> ··· 123 136 clearBtn: container.querySelector('#ai-chat-clear-btn') as HTMLButtonElement, 124 137 settingsBtn: container.querySelector('#ai-chat-settings-btn') as HTMLButtonElement, 125 138 closeBtn: container.querySelector('#ai-chat-close') as HTMLButtonElement, 139 + positionBtn: container.querySelector('#ai-chat-position-btn') as HTMLButtonElement, 126 140 settingsPanel: container.querySelector('#ai-chat-settings') as HTMLElement, 127 141 endpointInput: container.querySelector('#ai-endpoint') as HTMLInputElement, 128 142 modelSelect: container.querySelector('#ai-model') as HTMLSelectElement, 129 143 contextToggle: container.querySelector('#ai-context-toggle') as HTMLInputElement, 130 144 actionsToggle: container.querySelector('#ai-actions-toggle') as HTMLInputElement, 145 + autoApplyToggle: container.querySelector('#ai-auto-apply-toggle') as HTMLInputElement, 131 146 }; 132 147 } 133 148 ··· 245 260 246 261 list.appendChild(card); 247 262 list.scrollTop = list.scrollHeight; 263 + 264 + // If auto-apply is enabled on the parent sidebar, fire Apply immediately. 265 + // Defer to a microtask so the card is in the DOM before callbacks run. 266 + const sidebar = list.closest('.ai-chat-sidebar') as HTMLElement | null; 267 + if (sidebar?.dataset.autoApply === '1') { 268 + queueMicrotask(() => applyBtn.click()); 269 + } 270 + 248 271 return card; 249 272 }
+16
src/lib/ai-chat/types.ts
··· 11 11 ts: number; 12 12 } 13 13 14 + export type ChatPosition = 'side' | 'bottom' | 'popup'; 15 + 14 16 export interface ChatConfig { 15 17 endpoint: string; 16 18 model: string; 17 19 maxTokens: number; 20 + autoApply: boolean; 21 + position: ChatPosition; 18 22 } 19 23 20 24 export interface ChatState { ··· 28 32 29 33 const LS_ENDPOINT = 'tools-ai-endpoint'; 30 34 const LS_MODEL = 'tools-ai-model'; 35 + const LS_AUTO_APPLY = 'tools-ai-auto-apply'; 36 + const LS_POSITION = 'tools-ai-position'; 31 37 32 38 const DEFAULT_ENDPOINT = '/api/ai'; 33 39 const DEFAULT_MODEL = 'anthropic/claude-sonnet-4.6'; 34 40 const DEFAULT_MAX_TOKENS = 4096; 41 + const DEFAULT_POSITION: ChatPosition = 'side'; 42 + 43 + function loadPosition(): ChatPosition { 44 + const raw = localStorage.getItem(LS_POSITION); 45 + return raw === 'bottom' || raw === 'popup' || raw === 'side' ? raw : DEFAULT_POSITION; 46 + } 35 47 36 48 export function loadConfig(): ChatConfig { 37 49 return { 38 50 endpoint: localStorage.getItem(LS_ENDPOINT) || DEFAULT_ENDPOINT, 39 51 model: localStorage.getItem(LS_MODEL) || DEFAULT_MODEL, 40 52 maxTokens: DEFAULT_MAX_TOKENS, 53 + autoApply: localStorage.getItem(LS_AUTO_APPLY) === '1', 54 + position: loadPosition(), 41 55 }; 42 56 } 43 57 44 58 export function saveConfig(cfg: Partial<ChatConfig>): void { 45 59 if (cfg.endpoint !== undefined) localStorage.setItem(LS_ENDPOINT, cfg.endpoint); 46 60 if (cfg.model !== undefined) localStorage.setItem(LS_MODEL, cfg.model); 61 + if (cfg.autoApply !== undefined) localStorage.setItem(LS_AUTO_APPLY, cfg.autoApply ? '1' : '0'); 62 + if (cfg.position !== undefined) localStorage.setItem(LS_POSITION, cfg.position); 47 63 } 48 64 49 65 export function isConfigured(cfg: ChatConfig): boolean {
+49 -2
src/lib/ai-chat/wiring.ts
··· 3 3 */ 4 4 5 5 import { isConfigured, saveConfig, MODEL_OPTIONS } from './types.js'; 6 - import type { ChatConfig, ChatState } from './types.js'; 6 + import type { ChatConfig, ChatPosition, ChatState } from './types.js'; 7 7 import type { EditorType } from './system-prompt.js'; 8 8 import type { createChatSidebar } from './sidebar-dom.js'; 9 9 import { autoResizeTextarea } from './sidebar-dom.js'; ··· 52 52 } 53 53 updateModelBadge(); 54 54 55 + // Position modes: side / bottom / popup 56 + const POSITIONS: readonly ChatPosition[] = ['side', 'bottom', 'popup'] as const; 57 + function applyPosition(pos: ChatPosition): void { 58 + chatUI.container.dataset.pos = pos; 59 + const posBtn = chatUI.positionBtn; 60 + posBtn.querySelectorAll<HTMLElement>('.ai-chat-pos-icon').forEach((el) => { 61 + el.style.display = 'none'; 62 + }); 63 + const active = posBtn.querySelector<HTMLElement>(`.ai-chat-pos-icon--${pos}`); 64 + if (active) active.style.display = ''; 65 + posBtn.title = `Position: ${pos} — click to cycle (side → bottom → popup)`; 66 + } 67 + applyPosition(chatConfig.position); 68 + 69 + chatUI.positionBtn.addEventListener('click', () => { 70 + const idx = POSITIONS.indexOf(chatConfig.position); 71 + const next: ChatPosition = POSITIONS[(idx + 1) % POSITIONS.length] ?? 'side'; 72 + chatConfig = { ...chatConfig, position: next }; 73 + opts.chatConfig = chatConfig; 74 + saveConfig({ position: next }); 75 + applyPosition(next); 76 + }); 77 + 78 + // Auto-apply toggle: when on, new action cards auto-apply via sidebar-dom 79 + // behavior. Also retroactively applies any pending cards when toggled on. 80 + function applyAutoApplyState(enabled: boolean): void { 81 + chatUI.container.dataset.autoApply = enabled ? '1' : ''; 82 + } 83 + chatUI.autoApplyToggle.checked = chatConfig.autoApply; 84 + applyAutoApplyState(chatConfig.autoApply); 85 + 86 + chatUI.autoApplyToggle.addEventListener('change', () => { 87 + const enabled = chatUI.autoApplyToggle.checked; 88 + chatConfig = { ...chatConfig, autoApply: enabled }; 89 + opts.chatConfig = chatConfig; 90 + saveConfig({ autoApply: enabled }); 91 + applyAutoApplyState(enabled); 92 + 93 + // Retroactively approve pending cards when turning on. 94 + if (enabled) { 95 + const pending = chatUI.messageList.querySelectorAll<HTMLButtonElement>( 96 + '.ai-action-card:not(.ai-action-card--applied):not(.ai-action-card--dismissed):not(.ai-action-card--suggested) .ai-action-btn--apply', 97 + ); 98 + pending.forEach((btn) => btn.click()); 99 + } 100 + }); 101 + 55 102 // Toggle panel 56 103 function togglePanel(): void { 57 104 const isOpen = chatUI.container.style.display !== 'none'; ··· 93 140 : chatUI.modelSelect.value; 94 141 95 142 chatConfig = { 143 + ...chatConfig, 96 144 endpoint: chatUI.endpointInput.value.trim(), 97 145 model: model || 'anthropic/claude-sonnet-4.6', 98 - maxTokens: chatConfig.maxTokens, 99 146 }; 100 147 opts.chatConfig = chatConfig; 101 148 saveConfig(chatConfig);