diff --git a/tasksync-chat/media/webview.js b/tasksync-chat/media/webview.js index b6e3109..99c3515 100644 --- a/tasksync-chat/media/webview.js +++ b/tasksync-chat/media/webview.js @@ -29,7 +29,11 @@ let autopilotText = ''; let autopilotTextDebounceTimer = null; let responseTimeout = 60; + let sessionWarningHours = 2; let maxConsecutiveAutoResponses = 5; + // Keep timeout options aligned with select values to avoid invalid UI state. + var RESPONSE_TIMEOUT_ALLOWED_VALUES = new Set([0, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 150, 180, 210, 240]); + var RESPONSE_TIMEOUT_DEFAULT = 60; // Human-like delay: random jitter simulates natural reading/typing time let humanLikeDelayEnabled = true; let humanLikeDelayMin = 2; // minimum seconds @@ -80,7 +84,7 @@ // Settings modal elements let settingsModal, settingsModalOverlay, settingsModalClose; let soundToggle, interactiveApprovalToggle, sendShortcutToggle, autopilotEditBtn, autopilotToggle, autopilotTextInput, promptsList, addPromptBtn, addPromptForm; - let responseTimeoutSelect, maxAutoResponsesInput; + let responseTimeoutSelect, sessionWarningHoursSelect, maxAutoResponsesInput; let humanDelayToggle, humanDelayRangeContainer, humanDelayMinInput, humanDelayMaxInput; function init() { @@ -431,6 +435,31 @@ ''; modalContent.appendChild(timeoutSection); + // Session Warning section - warning threshold in hours + var sessionWarningSection = document.createElement('div'); + sessionWarningSection.className = 'settings-section'; + sessionWarningSection.innerHTML = '
' + + '
' + + ' Session Warning' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
'; + modalContent.appendChild(sessionWarningSection); + // Max Consecutive Auto-Responses section - number input var maxAutoSection = document.createElement('div'); maxAutoSection.className = 'settings-section'; @@ -498,6 +527,7 @@ autopilotEditBtn = document.getElementById('autopilot-edit-btn'); autopilotTextInput = document.getElementById('autopilot-text'); responseTimeoutSelect = document.getElementById('response-timeout-select'); + sessionWarningHoursSelect = document.getElementById('session-warning-hours-select'); maxAutoResponsesInput = document.getElementById('max-auto-responses-input'); humanDelayToggle = document.getElementById('human-delay-toggle'); humanDelayRangeContainer = document.getElementById('human-delay-range'); @@ -620,6 +650,9 @@ if (responseTimeoutSelect) { responseTimeoutSelect.addEventListener('change', handleResponseTimeoutChange); } + if (sessionWarningHoursSelect) { + sessionWarningHoursSelect.addEventListener('change', handleSessionWarningHoursChange); + } if (maxAutoResponsesInput) { maxAutoResponsesInput.addEventListener('change', handleMaxAutoResponsesChange); maxAutoResponsesInput.addEventListener('blur', handleMaxAutoResponsesChange); @@ -1001,6 +1034,16 @@ if (queueSection) queueSection.classList.toggle('collapsed'); } + function normalizeResponseTimeout(value) { + if (!Number.isFinite(value)) { + return RESPONSE_TIMEOUT_DEFAULT; + } + if (!RESPONSE_TIMEOUT_ALLOWED_VALUES.has(value)) { + return RESPONSE_TIMEOUT_DEFAULT; + } + return value; + } + function handleExtensionMessage(event) { var message = event.data; console.log('[TaskSync Webview] Received message:', message.type, message); @@ -1047,7 +1090,8 @@ autopilotEnabled = message.autopilotEnabled === true; autopilotText = typeof message.autopilotText === 'string' ? message.autopilotText : ''; reusablePrompts = message.reusablePrompts || []; - responseTimeout = typeof message.responseTimeout === 'number' ? message.responseTimeout : 60; + responseTimeout = normalizeResponseTimeout(message.responseTimeout); + sessionWarningHours = typeof message.sessionWarningHours === 'number' ? message.sessionWarningHours : 2; maxConsecutiveAutoResponses = typeof message.maxConsecutiveAutoResponses === 'number' ? message.maxConsecutiveAutoResponses : 5; humanLikeDelayEnabled = message.humanLikeDelayEnabled !== false; humanLikeDelayMin = typeof message.humanLikeDelayMin === 'number' ? message.humanLikeDelayMin : 2; @@ -1058,6 +1102,7 @@ updateAutopilotToggleUI(); updateAutopilotTextUI(); updateResponseTimeoutUI(); + updateSessionWarningHoursUI(); updateMaxAutoResponsesUI(); updateHumanDelayUI(); renderPromptsList(); @@ -2185,6 +2230,23 @@ responseTimeoutSelect.value = String(responseTimeout); } + function handleSessionWarningHoursChange() { + if (!sessionWarningHoursSelect) return; + + var value = parseInt(sessionWarningHoursSelect.value, 10); + if (!isNaN(value) && value >= 0 && value <= 8) { + sessionWarningHours = value; + vscode.postMessage({ type: 'updateSessionWarningHours', value: value }); + } + + sessionWarningHoursSelect.value = String(sessionWarningHours); + } + + function updateSessionWarningHoursUI() { + if (!sessionWarningHoursSelect) return; + sessionWarningHoursSelect.value = String(sessionWarningHours); + } + function handleMaxAutoResponsesChange() { if (!maxAutoResponsesInput) return; var value = parseInt(maxAutoResponsesInput.value, 10); diff --git a/tasksync-chat/package.json b/tasksync-chat/package.json index 8f6be84..0961228 100644 --- a/tasksync-chat/package.json +++ b/tasksync-chat/package.json @@ -2,7 +2,7 @@ "name": "tasksync-chat", "publisher": "4regab", "displayName": "TaskSync", - "description": "Automate AI conversations. Queue your prompts or tasks. Work uninterrupted.", + "description": "Queue your prompts or tasks. Work uninterrupted.", "icon": "media/Tasksync-logo.png", "version": "2.0.21", "engines": { @@ -160,6 +160,14 @@ ], "description": "Auto-respond to pending tool calls if user doesn't respond within this time. When Autopilot is enabled, the Autopilot text is sent. When disabled, a session termination message is sent." }, + "tasksync.sessionWarningHours": { + "type": "number", + "default": 2, + "minimum": 0, + "maximum": 8, + "scope": "window", + "description": "Show a one-time warning after this many hours (0..8) in the same session. Set to 0 to disable session warning." + }, "tasksync.maxConsecutiveAutoResponses": { "type": "number", "default": 5, @@ -302,4 +310,4 @@ "esbuild": "^0.27.2", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/tasksync-chat/src/webview/webviewProvider.ts b/tasksync-chat/src/webview/webviewProvider.ts index d9eab42..91b5638 100644 --- a/tasksync-chat/src/webview/webviewProvider.ts +++ b/tasksync-chat/src/webview/webviewProvider.ts @@ -75,7 +75,7 @@ type ToWebviewMessage = | { type: 'updateAttachments'; attachments: AttachmentInfo[] } | { type: 'imageSaved'; attachment: AttachmentInfo } | { type: 'openSettingsModal' } - | { type: 'updateSettings'; soundEnabled: boolean; interactiveApprovalEnabled: boolean; autopilotEnabled: boolean; autopilotText: string; reusablePrompts: ReusablePrompt[]; responseTimeout: number; maxConsecutiveAutoResponses: number; humanLikeDelayEnabled: boolean; humanLikeDelayMin: number; humanLikeDelayMax: number; sendWithCtrlEnter: boolean } + | { type: 'updateSettings'; soundEnabled: boolean; interactiveApprovalEnabled: boolean; autopilotEnabled: boolean; autopilotText: string; reusablePrompts: ReusablePrompt[]; responseTimeout: number; sessionWarningHours: number; maxConsecutiveAutoResponses: number; humanLikeDelayEnabled: boolean; humanLikeDelayMin: number; humanLikeDelayMax: number; sendWithCtrlEnter: boolean } | { type: 'slashCommandResults'; prompts: ReusablePrompt[] } | { type: 'playNotificationSound' } | { type: 'contextSearchResults'; suggestions: Array<{ type: string; label: string; description: string; detail: string }> } @@ -114,6 +114,7 @@ type FromWebviewMessage = | { type: 'openExternal'; url: string } | { type: 'openFileLink'; target: string } | { type: 'updateResponseTimeout'; value: number } + | { type: 'updateSessionWarningHours'; value: number } | { type: 'updateMaxConsecutiveAutoResponses'; value: number } | { type: 'updateHumanDelaySetting'; enabled: boolean } | { type: 'updateHumanDelayMin'; value: number } @@ -197,6 +198,15 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco private _humanLikeDelayMin: number = 2; // seconds private _humanLikeDelayMax: number = 6; // seconds + // Session warning threshold (hours). 0 disables the warning. + private _sessionWarningHours: number = 2; + + // Allowed timeout values (minutes) shared across config reads/writes and UI sync. + private readonly _RESPONSE_TIMEOUT_ALLOWED_MINUTES = new Set([ + 0, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 150, 180, 210, 240 + ]); + private readonly _RESPONSE_TIMEOUT_DEFAULT_MINUTES = 60; + // Send behavior: false => Enter, true => Ctrl/Cmd+Enter private _sendWithCtrlEnter: boolean = false; @@ -253,6 +263,7 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco e.affectsConfiguration('tasksync.autoAnswerText') || e.affectsConfiguration('tasksync.reusablePrompts') || e.affectsConfiguration('tasksync.responseTimeout') || + e.affectsConfiguration('tasksync.sessionWarningHours') || e.affectsConfiguration('tasksync.maxConsecutiveAutoResponses') || e.affectsConfiguration('tasksync.humanLikeDelay') || e.affectsConfiguration('tasksync.humanLikeDelayMin') || @@ -469,12 +480,14 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco if (this._view) { this._view.title = this._formatElapsed(elapsed); } - // Show a one-time warning after 2 hours of session activity - if (!this._sessionWarningShown && elapsed >= 2 * 60 * 60 * 1000) { + // Warn once when a long-running session crosses the configured threshold. + const warningThresholdMs = this._sessionWarningHours * 60 * 60 * 1000; + if (this._sessionWarningHours > 0 && !this._sessionWarningShown && elapsed >= warningThresholdMs) { this._sessionWarningShown = true; const callCount = this._currentSessionCalls.length; + const hoursLabel = this._sessionWarningHours === 1 ? 'hour' : 'hours'; vscode.window.showWarningMessage( - `Your session has been running for over 2 hours (${callCount} tool calls). Consider starting a new session to maintain quality.`, + `Your session has been running for over ${this._sessionWarningHours} ${hoursLabel} (${callCount} tool calls). Consider starting a new session to maintain quality.`, 'New Session', 'Dismiss' ).then(action => { @@ -531,6 +544,36 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco return text.trim().length > 0 ? text : defaultAutopilotText; } + private _normalizeResponseTimeout(value: unknown): number { + let parsedValue: number; + + if (typeof value === 'number') { + parsedValue = value; + } else if (typeof value === 'string') { + const normalizedValue = value.trim(); + if (normalizedValue.length === 0) { + return this._RESPONSE_TIMEOUT_DEFAULT_MINUTES; + } + parsedValue = Number(normalizedValue); + } else { + return this._RESPONSE_TIMEOUT_DEFAULT_MINUTES; + } + + if (!Number.isFinite(parsedValue) || !Number.isInteger(parsedValue)) { + return this._RESPONSE_TIMEOUT_DEFAULT_MINUTES; + } + if (!this._RESPONSE_TIMEOUT_ALLOWED_MINUTES.has(parsedValue)) { + return this._RESPONSE_TIMEOUT_DEFAULT_MINUTES; + } + return parsedValue; + } + + private _readResponseTimeoutMinutes(config?: vscode.WorkspaceConfiguration): number { + const settings = config ?? vscode.workspace.getConfiguration('tasksync'); + const configuredTimeout = settings.get('responseTimeout', String(this._RESPONSE_TIMEOUT_DEFAULT_MINUTES)); + return this._normalizeResponseTimeout(configuredTimeout); + } + private _loadSettings(): void { const config = vscode.workspace.getConfiguration('tasksync'); this._soundEnabled = config.get('notificationSound', true); @@ -592,6 +635,10 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco this._humanLikeDelayEnabled = config.get('humanLikeDelay', true); this._humanLikeDelayMin = config.get('humanLikeDelayMin', 2); this._humanLikeDelayMax = config.get('humanLikeDelayMax', 6); + const configuredWarningHours = config.get('sessionWarningHours', 2); + this._sessionWarningHours = Number.isFinite(configuredWarningHours) + ? Math.min(8, Math.max(0, Math.floor(configuredWarningHours))) + : 2; this._sendWithCtrlEnter = config.get('sendWithCtrlEnter', false); // Ensure min <= max if (this._humanLikeDelayMin > this._humanLikeDelayMax) { @@ -621,7 +668,7 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco */ private _updateSettingsUI(): void { const config = vscode.workspace.getConfiguration('tasksync'); - const responseTimeout = parseInt(config.get('responseTimeout', '60'), 10); + const responseTimeout = this._readResponseTimeoutMinutes(config); const maxConsecutiveAutoResponses = config.get('maxConsecutiveAutoResponses', 5); this._view?.webview.postMessage({ @@ -632,6 +679,7 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco autopilotText: this._autopilotText, reusablePrompts: this._reusablePrompts, responseTimeout: responseTimeout, + sessionWarningHours: this._sessionWarningHours, maxConsecutiveAutoResponses: maxConsecutiveAutoResponses, humanLikeDelayEnabled: this._humanLikeDelayEnabled, humanLikeDelayMin: this._humanLikeDelayMin, @@ -980,12 +1028,10 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco } // Get timeout from config (in minutes) - const config = vscode.workspace.getConfiguration('tasksync'); - const rawValue = config.get('responseTimeout', '60'); - const timeoutMinutes = parseInt(rawValue, 10); + const timeoutMinutes = this._readResponseTimeoutMinutes(); // If timeout is 0 or disabled, don't start a timer - if (timeoutMinutes <= 0 || isNaN(timeoutMinutes)) { + if (timeoutMinutes <= 0) { return; } @@ -1020,7 +1066,7 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco // Use autopilot text only if autopilot is enabled; otherwise use session termination message const config = vscode.workspace.getConfiguration('tasksync'); - const timeoutMinutes = config.get('responseTimeout', '60'); + const timeoutMinutes = this._readResponseTimeoutMinutes(config); const maxConsecutive = config.get('maxConsecutiveAutoResponses', 5); // Increment and enforce consecutive auto-response limit @@ -1178,6 +1224,9 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco case 'updateResponseTimeout': this._handleUpdateResponseTimeout(message.value); break; + case 'updateSessionWarningHours': + this._handleUpdateSessionWarningHours(message.value); + break; case 'updateMaxConsecutiveAutoResponses': this._handleUpdateMaxConsecutiveAutoResponses(message.value); break; @@ -1933,7 +1982,28 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco this._isUpdatingConfig = true; try { const config = vscode.workspace.getConfiguration('tasksync'); - await config.update('responseTimeout', String(value), vscode.ConfigurationTarget.Workspace); + const normalizedValue = this._normalizeResponseTimeout(value); + await config.update('responseTimeout', String(normalizedValue), vscode.ConfigurationTarget.Workspace); + } finally { + this._isUpdatingConfig = false; + } + } + + /** + * Handle updating session warning threshold in hours. + */ + private async _handleUpdateSessionWarningHours(value: number): Promise { + if (!Number.isFinite(value)) { + return; + } + + const normalizedValue = Math.min(8, Math.max(0, Math.floor(value))); + this._sessionWarningHours = normalizedValue; + + this._isUpdatingConfig = true; + try { + const config = vscode.workspace.getConfiguration('tasksync'); + await config.update('sessionWarningHours', normalizedValue, vscode.ConfigurationTarget.Workspace); } finally { this._isUpdatingConfig = false; } @@ -2024,7 +2094,7 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco this._isUpdatingConfig = true; try { const config = vscode.workspace.getConfiguration('tasksync'); - await config.update('sendWithCtrlEnter', enabled, vscode.ConfigurationTarget.Workspace); + await config.update('sendWithCtrlEnter', enabled, vscode.ConfigurationTarget.Global); } finally { this._isUpdatingConfig = false; }