Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ TaskSync is a human-in-the-loop workflow toolkit for AI-assisted development. Th
- `tasksync-chat/biome.json` — Biome linter/formatter config
- `tasksync-chat/vitest.config.ts` — Test config
- `tasksync-chat/web/` — Remote access PWA (login, service worker)
- `tasksync-chat/src/constants/hookContent.ts` — Copilot hook message SSOT
- `Prompt/` — Standalone prompt/protocol markdown files (not actively developed)

## Build, Test, and Validate
Expand Down Expand Up @@ -98,3 +99,4 @@ Follow OWASP Top 10 principles. Specific patterns enforced in this codebase:
- Debounced history saves (2 s) for disk I/O performance.
- Remote server uses WebSocket over HTTP with PIN-based auth and session tokens.
- `esbuild.js` watch mode monitors `remoteConstants.ts` changes and rebuilds shared constants with 100ms debounce.
- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). The default hook file is generated to `~/.copilot/hooks/tasksync.json` and adds non-blocking `SessionStart`, `PreCompact`, and `SubagentStart` guidance. This repository also keeps a matching workspace copy at `.github/hooks/tasksync.json`. The `TaskSync: Setup Global Copilot Hooks` command generates the user-scoped hook file from the SSOT. Agent-scoped hooks can also be set in `.agent.md` frontmatter.
23 changes: 23 additions & 0 deletions .github/hooks/tasksync.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"MANDATORY: You MUST call ask_user with session_id after EVERY completed task and whenever you need user input. Plain chat is invisible to the user. Reuse the previous session_id. NEVER finish silently.\"}}'"
}
],
"PreCompact": [
{
"type": "command",
"command": "echo '{\"systemMessage\":\"CRITICAL: You MUST keep calling ask_user with session_id on every turn after compaction. Reuse the previous session_id. NEVER end without ask_user.\"}'"
}
],
"SubagentStart": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"MANDATORY: You are a subagent. NEVER call ask_user. Return results to the main agent, then STOP.\"}}'"
}
]
},
"version": 1
}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ These principles are mandatory for all changes:
- Session state uses a boolean `sessionTerminated` flag — do not use string matching for termination detection.
- Debounced history saves (2 s) are used for disk I/O performance.
- New Session now supports a modal-first flow and starts a fresh Copilot chat session via `startFreshCopilotChatWithQuery`.
- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). The default hook file is generated to `~/.copilot/hooks/tasksync.json` and adds non-blocking `SessionStart`, `PreCompact`, and `SubagentStart` guidance. This repository also keeps a matching workspace copy at `.github/hooks/tasksync.json`. The `TaskSync: Setup Global Copilot Hooks` command generates the user-scoped hook file from the SSOT. Agent-scoped hooks can also be defined in `.agent.md` frontmatter.
- Remote Code Review is read-only by design:
- Diff browsing is available (`/api/changes`, `/api/diff`)
- Write operations (stage/unstage/discard/commit/push) are blocked remotely
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

All notable changes to this project will be documented in this file.

## TaskSync v3.0.12 (04-07-26)
- feat: add global Copilot hooks setup with non-blocking SessionStart, PreCompact, and SubagentStart guidance for `ask_user`

## TaskSync v3.0.10 (04-03-26)
- feat: add agent orchestration toggle, single-session routing mode, and always-returned session_id tool payloads
- fix: tighten the gap below the view toolbar, focus dialogs on open, and let TaskSync dialogs close on `Escape`
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ Recommended settings for agent mode:

**Enable "Auto Approve" in settings for uninterrupted agent operation. Sessions beyond 2 hours may produce lower-quality results — TaskSync will warn you when it's time to consider starting a fresh session.**

### Copilot Hooks (Preview)

TaskSync includes [Copilot hooks](https://code.visualstudio.com/docs/copilot/customization/hooks) that inject the `ask_user` contract at session start and preserve it through context compaction. Run **`TaskSync: Setup Global Copilot Hooks`** from the command palette to generate `~/.copilot/hooks/tasksync.json` in your user profile. This repo also keeps a matching workspace hook file at `.github/hooks/tasksync.json`. This adds:

- **SessionStart hook** — injects the `ask_user` contract when a session begins
- **PreCompact hook** — reminds the agent to preserve `session_id` after context compaction
- **SubagentStart hook** — tells subagents not to call `ask_user`

The default hook set is non-blocking, so it does not force extra turns at stop time. In this repository, the committed workspace hook file mirrors the same hook content, so workspace scope wins without changing behavior.

This is a user-scoped setup by default, so it applies across workspaces. If a workspace hook exists for the same event, VS Code will prefer the workspace hook.

Copilot Hooks require VS Code 1.109.3+ and the `chat.agent.hooks` setting enabled. The extension itself runs on older supported VS Code versions without hooks.

## Discussions

The TaskSync community can be found on [GitHub Discussions](https://github.com/4regab/TaskSync/discussions) where you can ask questions, voice ideas, and share your prompts with other people. Contributions to TaskSync are welcome and highly appreciated.
Expand Down
4 changes: 2 additions & 2 deletions tasksync-chat/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions tasksync-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"displayName": "TaskSync",
"description": "Queue your prompts or tasks. Work uninterrupted.",
"icon": "media/Tasksync-logo.png",
"version": "3.0.11",
"version": "3.0.12",
"engines": {
"vscode": "^1.90.0"
},
Expand Down Expand Up @@ -345,6 +345,10 @@
"command": "tasksync.goRemote",
"title": "Remote",
"icon": "$(broadcast)"
},
{
"command": "tasksync.setupHooks",
"title": "TaskSync: Setup Global Copilot Hooks"
}
],
"menus": {
Expand Down Expand Up @@ -411,4 +415,4 @@
"typescript": "^5.3.3",
"vitest": "^4.0.18"
}
}
}
61 changes: 61 additions & 0 deletions tasksync-chat/src/constants/hookContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copilot hook content constants (SSOT)
*
* Used by:
* - extension.ts → setupHooksCmd (generates ~/.copilot/hooks/tasksync.json)
*/

export const HOOK_SESSION_START_MESSAGE =
"MANDATORY: You MUST call ask_user with session_id after EVERY completed task and whenever you need user input. Plain chat is invisible to the user. Reuse the previous session_id. NEVER finish silently.";

export const HOOK_PRECOMPACT_MESSAGE =
"CRITICAL: You MUST keep calling ask_user with session_id on every turn after compaction. Reuse the previous session_id. NEVER end without ask_user.";

export const HOOK_SUBAGENT_START_MESSAGE =
"MANDATORY: You are a subagent. NEVER call ask_user. Return results to the main agent, then STOP.";

function buildAdditionalContextCommand(
hookEventName: "SessionStart" | "SubagentStart",
additionalContext: string,
): string {
const payload = JSON.stringify({
hookSpecificOutput: {
hookEventName,
additionalContext,
},
});

return `echo '${payload}'`;
}

/** Build the full hook file JSON object for writing to disk. */
export function buildHookFileContent(): object {
return {
hooks: {
SessionStart: [
{
type: "command",
command: buildAdditionalContextCommand(
"SessionStart",
HOOK_SESSION_START_MESSAGE,
),
},
],
PreCompact: [
{
type: "command",
command: `echo '{"systemMessage":"${HOOK_PRECOMPACT_MESSAGE}"}'`,
},
],
SubagentStart: [
{
type: "command",
command: buildAdditionalContextCommand(
"SubagentStart",
HOOK_SUBAGENT_START_MESSAGE,
),
},
],
},
};
}
82 changes: 82 additions & 0 deletions tasksync-chat/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as os from "node:os";
import * as path from "node:path";
import * as vscode from "vscode";
import { buildHookFileContent } from "./constants/hookContent";
import {
CONFIG_SECTION,
DEFAULT_REMOTE_PORT,
Expand All @@ -14,6 +17,39 @@ let webviewProvider: TaskSyncWebviewProvider | undefined;
let contextManager: ContextManager | undefined;
let remoteServer: RemoteServer | undefined;

const GLOBAL_HOOKS_DIR_PATH = path.join(os.homedir(), ".copilot", "hooks");
const GLOBAL_HOOK_FILE_NAME = "tasksync.json";
const GLOBAL_HOOK_FILE_DISPLAY_PATH = `~/.copilot/hooks/${GLOBAL_HOOK_FILE_NAME}`;

function getGlobalHooksDirUri(): vscode.Uri {
return vscode.Uri.file(GLOBAL_HOOKS_DIR_PATH);
}

function getGlobalHookFileUri(): vscode.Uri {
return vscode.Uri.file(
path.join(GLOBAL_HOOKS_DIR_PATH, GLOBAL_HOOK_FILE_NAME),
);
}

/** Auto-create ~/.copilot/hooks/tasksync.json if it is missing. */
async function ensureCopilotHooks(): Promise<void> {
const hooksDir = getGlobalHooksDirUri();
const hookFile = getGlobalHookFileUri();

try {
await vscode.workspace.fs.stat(hookFile);
return; // File already exists — nothing to do
} catch {
// File doesn't exist — create it
}
const content = JSON.stringify(buildHookFileContent(), null, 4);
await vscode.workspace.fs.createDirectory(hooksDir);
await vscode.workspace.fs.writeFile(
hookFile,
Buffer.from(`${content}\n`, "utf-8"),
);
}

export function activate(context: vscode.ExtensionContext): void {
// Initialize context manager for #terminal, #problems features
contextManager = new ContextManager();
Expand All @@ -40,6 +76,11 @@ export function activate(context: vscode.ExtensionContext): void {
/* fallback to sync read */
});

// Auto-setup Copilot hooks if workspace exists and hooks file is missing
ensureCopilotHooks().catch(() => {
/* best-effort — no user-facing error */
});

// Register VS Code LM Tools (always available for Copilot)
registerTools(context, provider);

Expand Down Expand Up @@ -308,6 +349,46 @@ export function activate(context: vscode.ExtensionContext): void {
},
);

// Setup Copilot hooks command — writes ~/.copilot/hooks/tasksync.json to user profile
const setupHooksCmd = vscode.commands.registerCommand(
"tasksync.setupHooks",
async () => {
const hooksDir = getGlobalHooksDirUri();
const hookFile = getGlobalHookFileUri();

// Check if file already exists
try {
await vscode.workspace.fs.stat(hookFile);
const overwrite = await vscode.window.showWarningMessage(
`${GLOBAL_HOOK_FILE_DISPLAY_PATH} already exists. Overwrite?`,
{ modal: true },
"Overwrite",
);
if (overwrite !== "Overwrite") return;
} catch {
// File doesn't exist — proceed
}

const hookContent = JSON.stringify(buildHookFileContent(), null, 4);

try {
await vscode.workspace.fs.createDirectory(hooksDir);
await vscode.workspace.fs.writeFile(
hookFile,
Buffer.from(hookContent + "\n", "utf-8"),
);

vscode.window.showInformationMessage(
`TaskSync hooks created at ${GLOBAL_HOOK_FILE_DISPLAY_PATH}`,
);
} catch (err) {
vscode.window.showErrorMessage(
`Failed to create TaskSync hooks: ${getSafeErrorMessage(err)}`,
);
}
},
);

context.subscriptions.push(
sendMessageCmd,
openHistoryCmd,
Expand All @@ -318,6 +399,7 @@ export function activate(context: vscode.ExtensionContext): void {
startRemoteLanCmd,
stopRemoteCmd,
goRemoteCmd,
setupHooksCmd,
);
}

Expand Down
Loading