Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions tasksync-chat/media/webview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -431,6 +435,31 @@
'</div>';
modalContent.appendChild(timeoutSection);

// Session Warning section - warning threshold in hours
var sessionWarningSection = document.createElement('div');
sessionWarningSection.className = 'settings-section';
sessionWarningSection.innerHTML = '<div class="settings-section-header">' +
'<div class="settings-section-title">' +
'<span class="codicon codicon-watch"></span> Session Warning' +
'<span class="settings-info-icon" title="Show a one-time warning after this many hours in the same session. Set to 0 to disable.">' +
'<span class="codicon codicon-info"></span></span>' +
'</div>' +
'</div>' +
'<div class="form-row">' +
'<select class="form-input form-select" id="session-warning-hours-select">' +
'<option value="0">Disabled</option>' +
'<option value="1">1 hour</option>' +
'<option value="2">2 hours</option>' +
'<option value="3">3 hours</option>' +
'<option value="4">4 hours</option>' +
'<option value="5">5 hours</option>' +
'<option value="6">6 hours</option>' +
'<option value="7">7 hours</option>' +
'<option value="8">8 hours</option>' +
'</select>' +
'</div>';
modalContent.appendChild(sessionWarningSection);

// Max Consecutive Auto-Responses section - number input
var maxAutoSection = document.createElement('div');
maxAutoSection.className = 'settings-section';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -1058,6 +1102,7 @@
updateAutopilotToggleUI();
updateAutopilotTextUI();
updateResponseTimeoutUI();
updateSessionWarningHoursUI();
updateMaxAutoResponsesUI();
updateHumanDelayUI();
renderPromptsList();
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions tasksync-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -302,4 +310,4 @@
"esbuild": "^0.27.2",
"typescript": "^5.3.3"
}
}
}
94 changes: 82 additions & 12 deletions tasksync-chat/src/webview/webviewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> }
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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<number>([
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;

Expand Down Expand Up @@ -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') ||
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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<string>('responseTimeout', String(this._RESPONSE_TIMEOUT_DEFAULT_MINUTES));
return this._normalizeResponseTimeout(configuredTimeout);
}

private _loadSettings(): void {
const config = vscode.workspace.getConfiguration('tasksync');
this._soundEnabled = config.get<boolean>('notificationSound', true);
Expand Down Expand Up @@ -592,6 +635,10 @@ export class TaskSyncWebviewProvider implements vscode.WebviewViewProvider, vsco
this._humanLikeDelayEnabled = config.get<boolean>('humanLikeDelay', true);
this._humanLikeDelayMin = config.get<number>('humanLikeDelayMin', 2);
this._humanLikeDelayMax = config.get<number>('humanLikeDelayMax', 6);
const configuredWarningHours = config.get<number>('sessionWarningHours', 2);
this._sessionWarningHours = Number.isFinite(configuredWarningHours)
? Math.min(8, Math.max(0, Math.floor(configuredWarningHours)))
: 2;
this._sendWithCtrlEnter = config.get<boolean>('sendWithCtrlEnter', false);
// Ensure min <= max
if (this._humanLikeDelayMin > this._humanLikeDelayMax) {
Expand Down Expand Up @@ -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<string>('responseTimeout', '60'), 10);
const responseTimeout = this._readResponseTimeoutMinutes(config);
const maxConsecutiveAutoResponses = config.get<number>('maxConsecutiveAutoResponses', 5);

this._view?.webview.postMessage({
Expand All @@ -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,
Expand Down Expand Up @@ -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<string>('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;
}

Expand Down Expand Up @@ -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<string>('responseTimeout', '60');
const timeoutMinutes = this._readResponseTimeoutMinutes(config);
const maxConsecutive = config.get<number>('maxConsecutiveAutoResponses', 5);

// Increment and enforce consecutive auto-response limit
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
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;
}
Expand Down Expand Up @@ -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;
}
Expand Down