mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-09 19:42:57 +00:00
Merge pull request #2781 from QwenLM/feat/hooks-remove-experimental
feat(hooks): remove experimental flag and add disabled state UI
This commit is contained in:
commit
a29e059d2e
35 changed files with 745 additions and 350 deletions
|
|
@ -4,13 +4,18 @@
|
|||
|
||||
Qwen Code hooks provide a powerful mechanism for extending and customizing the behavior of the Qwen Code application. Hooks allow users to execute custom scripts or programs at specific points in the application lifecycle, such as before tool execution, after tool execution, at session start/end, and during other key events.
|
||||
|
||||
> **⚠️ EXPERIMENTAL FEATURE**
|
||||
>
|
||||
> Hooks are currently in an experimental stage. To enable hooks, start Qwen Code with the `--experimental-hooks` flag:
|
||||
>
|
||||
> ```bash
|
||||
> qwen --experimental-hooks
|
||||
> ```
|
||||
Hooks are enabled by default. You can temporarily disable all hooks by setting `disableAllHooks` to `true` in your settings file (at the top level, alongside `hooks`):
|
||||
|
||||
```json
|
||||
{
|
||||
"disableAllHooks": true,
|
||||
"hooks": {
|
||||
"PreToolUse": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This disables all hooks without deleting their configurations.
|
||||
|
||||
## What are Hooks?
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-allow-decision', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -73,7 +73,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-allow-tool', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -108,7 +108,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-block-decision', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -137,7 +137,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-block-tool', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -184,7 +184,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-modify-prompt', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -214,7 +214,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-add-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -241,7 +241,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook times out', async () => {
|
||||
await rig.setup('ups-timeout', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -269,7 +269,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook exits with non-blocking error (exit code 1)', async () => {
|
||||
await rig.setup('ups-nonblocking-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -295,7 +295,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should block execution when hook exits with blocking error (exit code 2)', async () => {
|
||||
await rig.setup('ups-blocking-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -320,7 +320,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook command is empty', async () => {
|
||||
await rig.setup('ups-missing-command', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -352,7 +352,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-correct-input', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -382,7 +382,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-system-message', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -414,7 +414,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-one-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -452,7 +452,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-seq-first-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -486,7 +486,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-seq-second-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -525,7 +525,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-all-allow', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -569,7 +569,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-all-block', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -605,7 +605,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -639,7 +639,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-error-with-block', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -673,7 +673,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-timeout-with-block', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -709,7 +709,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-groups', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -750,7 +750,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-groups-one-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -790,7 +790,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-modify', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -827,7 +827,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('ups-multi-system-msg', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -869,7 +869,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-allow', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -897,7 +897,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-allow-final', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -931,7 +931,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-block-decision', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -972,7 +972,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-block-custom-reason', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1014,7 +1014,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-add-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1044,7 +1044,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-multi-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1077,7 +1077,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue stopping when hook times out', async () => {
|
||||
await rig.setup('stop-timeout', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1105,7 +1105,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue stopping when hook has non-blocking error', async () => {
|
||||
await rig.setup('stop-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1131,7 +1131,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue stopping when hook command does not exist', async () => {
|
||||
await rig.setup('stop-missing-command', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1162,7 +1162,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-system-message', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1196,7 +1196,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-multi-one-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1249,7 +1249,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-seq-first-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1303,7 +1303,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-seq-second-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1358,7 +1358,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-multi-all-allow', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1402,7 +1402,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('stop-multi-all-block', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -1457,7 +1457,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -1493,7 +1493,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-first-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -1532,7 +1532,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-passthrough', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -1569,7 +1569,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -1604,7 +1604,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-mixed', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -1641,7 +1641,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('multi-or-logic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -3265,7 +3265,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('combined-both-hooks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -3306,7 +3306,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('combined-ups-sessionend', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -3348,7 +3348,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('combined-three-hooks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -3403,7 +3403,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('combined-all-hooks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
|
|
@ -3474,7 +3474,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('script-file-hook', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -3503,7 +3503,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('script-file-block-hook', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
|
|
@ -3539,7 +3539,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-allow-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3580,7 +3580,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-allow-safe-tools', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3612,7 +3612,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-deny-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3657,7 +3657,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-block-dangerous', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3695,7 +3695,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-multi-allow', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3736,7 +3736,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-sequential-allow', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3774,7 +3774,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-multi-one-denies', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3816,7 +3816,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-sequential-first-denies', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3861,7 +3861,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-matcher-specific', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3890,7 +3890,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('permission-req-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{
|
||||
|
|
@ -3927,7 +3927,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -3958,7 +3958,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -3989,7 +3989,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-context-only', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4019,7 +4019,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4054,7 +4054,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4102,7 +4102,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4151,7 +4151,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-matcher-specific', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4183,7 +4183,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-start-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStart: [
|
||||
{
|
||||
|
|
@ -4222,7 +4222,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4254,7 +4254,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-block-once', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4288,7 +4288,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4323,7 +4323,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4371,7 +4371,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4420,7 +4420,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-matcher-specific', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4452,7 +4452,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('subagent-stop-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
SubagentStop: [
|
||||
{
|
||||
|
|
@ -4490,7 +4490,7 @@ describe('Hooks System Integration', () => {
|
|||
'echo \'{"additionalContext": "Idle prompt notification processed"}\'';
|
||||
await rig.setup('notification-idle-prompt', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4521,7 +4521,7 @@ describe('Hooks System Integration', () => {
|
|||
'echo \'{"additionalContext": "Second idle prompt notification"}\'';
|
||||
await rig.setup('notification-idle-prompt-multiple', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4560,7 +4560,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('notification-elication-dialog', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4591,7 +4591,7 @@ describe('Hooks System Integration', () => {
|
|||
'echo \'{"additionalContext": "Second elication dialog notification"}\'';
|
||||
await rig.setup('notification-elication-dialog-multiple', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4625,7 +4625,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle elication dialog notification with error', async () => {
|
||||
await rig.setup('notification-elication-dialog-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4659,7 +4659,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('notification-multiple-different', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4695,7 +4695,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle missing command gracefully', async () => {
|
||||
await rig.setup('notification-missing-command', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4722,7 +4722,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle non-executable command gracefully', async () => {
|
||||
await rig.setup('notification-non-executable', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4749,7 +4749,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle command with non-zero exit code gracefully', async () => {
|
||||
await rig.setup('notification-nonzero-exit', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4776,7 +4776,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle command timeout gracefully', async () => {
|
||||
await rig.setup('notification-timeout', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
Notification: [
|
||||
{
|
||||
|
|
@ -4814,7 +4814,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-allow-decision', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -4845,7 +4845,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-allow-with-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -4878,7 +4878,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-block-decision', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -4918,7 +4918,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-block-specific-tool', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -4957,7 +4957,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-matcher-specific', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -4989,7 +4989,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5021,7 +5021,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-matcher-no-match', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5052,7 +5052,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook exits with non-blocking error', async () => {
|
||||
await rig.setup('pretooluse-nonblocking-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5080,7 +5080,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook command does not exist', async () => {
|
||||
await rig.setup('pretooluse-missing-command', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5115,7 +5115,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-multi-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5154,7 +5154,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-multi-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5193,7 +5193,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-multi-one-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5232,7 +5232,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-seq-first-blocks', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5273,7 +5273,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('pretooluse-multi-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
|
|
@ -5318,7 +5318,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5351,7 +5351,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-matcher-specific', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5383,7 +5383,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5415,7 +5415,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-matcher-no-match', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5451,7 +5451,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-multi-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5490,7 +5490,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-multi-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5530,7 +5530,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttooluse-multi-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
|
|
@ -5575,7 +5575,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttoolusefailure-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUseFailure: [
|
||||
{
|
||||
|
|
@ -5611,7 +5611,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('posttoolusefailure-with-details', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PostToolUseFailure: [
|
||||
{
|
||||
|
|
@ -5650,7 +5650,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-basic', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5686,7 +5686,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-with-details', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5719,7 +5719,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5752,7 +5752,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-matcher-wildcard', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5784,7 +5784,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-matcher-no-match', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5820,7 +5820,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-multi-parallel', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5859,7 +5859,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-multi-sequential', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5899,7 +5899,7 @@ describe('Hooks System Integration', () => {
|
|||
|
||||
await rig.setup('precompact-multi-context', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5935,7 +5935,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook exits with error', async () => {
|
||||
await rig.setup('precompact-error', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5963,7 +5963,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should continue execution when hook command does not exist', async () => {
|
||||
await rig.setup('precompact-missing-command', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
@ -5991,7 +5991,7 @@ describe('Hooks System Integration', () => {
|
|||
it('should handle hook timeout gracefully', async () => {
|
||||
await rig.setup('precompact-timeout', {
|
||||
settings: {
|
||||
hooksConfig: { enabled: true },
|
||||
disableAllHooks: false,
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ describe('Session', () => {
|
|||
.fn()
|
||||
.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
mockConfig.getPermissionManager = vi.fn().mockReturnValue(null);
|
||||
mockConfig.getEnableHooks = vi.fn().mockReturnValue(true);
|
||||
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(false);
|
||||
mockConfig.getMessageBus = vi.fn().mockReturnValue({});
|
||||
mockChat.sendMessageStream = vi.fn().mockResolvedValue(
|
||||
(async function* () {
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ export class Session implements SessionContext {
|
|||
injectPermissionRulesIfMissing(confirmationDetails, pmCtx);
|
||||
|
||||
const messageBus = this.config.getMessageBus?.();
|
||||
const hooksEnabled = this.config.getEnableHooks?.() ?? false;
|
||||
const hooksEnabled = !this.config.getDisableAllHooks?.();
|
||||
let hookHandled = false;
|
||||
|
||||
if (hooksEnabled && messageBus) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ export async function handleQwenAuth(
|
|||
acp: undefined,
|
||||
experimentalAcp: undefined,
|
||||
experimentalLsp: undefined,
|
||||
experimentalHooks: undefined,
|
||||
extensions: [],
|
||||
listExtensions: undefined,
|
||||
openaiLogging: undefined,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ export interface CliArgs {
|
|||
acp: boolean | undefined;
|
||||
experimentalAcp: boolean | undefined;
|
||||
experimentalLsp: boolean | undefined;
|
||||
experimentalHooks: boolean | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
openaiLogging: boolean | undefined;
|
||||
|
|
@ -352,12 +351,6 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||
'Enable experimental LSP (Language Server Protocol) feature for code intelligence',
|
||||
default: false,
|
||||
})
|
||||
.option('experimental-hooks', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Enable experimental hooks feature for lifecycle event customization',
|
||||
default: false,
|
||||
})
|
||||
.option('channel', {
|
||||
type: 'string',
|
||||
choices: ['VSCode', 'ACP', 'SDK', 'CI'],
|
||||
|
|
@ -1121,9 +1114,7 @@ export async function loadCliConfig(
|
|||
format: outputSettingsFormat,
|
||||
},
|
||||
hooks: settings.hooks,
|
||||
hooksConfig: settings.hooksConfig,
|
||||
enableHooks:
|
||||
argv.experimentalHooks === true || settings.hooksConfig?.enabled === true,
|
||||
disableAllHooks: settings.disableAllHooks ?? false,
|
||||
channel: argv.channel,
|
||||
// Precedence: explicit CLI flag > settings file > default(true).
|
||||
// NOTE: do NOT set a yargs default for `chat-recording`, otherwise argv will
|
||||
|
|
|
|||
|
|
@ -1404,38 +1404,15 @@ const SETTINGS_SCHEMA = {
|
|||
},
|
||||
},
|
||||
|
||||
hooksConfig: {
|
||||
type: 'object',
|
||||
label: 'Hooks Config',
|
||||
disableAllHooks: {
|
||||
type: 'boolean',
|
||||
label: 'Disable All Hooks',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
requiresRestart: true, // Future enhancement: consider supporting mid-session toggle for better UX
|
||||
default: false,
|
||||
description:
|
||||
'Hook configurations for intercepting and customizing agent behavior.',
|
||||
'Temporarily disable all hooks without deleting configurations. Default is false (hooks enabled).',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Hooks',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
|
||||
showInDialog: false,
|
||||
},
|
||||
disabled: {
|
||||
type: 'array',
|
||||
label: 'Disabled Hooks',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: [] as string[],
|
||||
description:
|
||||
'List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.UNION,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
hooks: {
|
||||
|
|
|
|||
|
|
@ -506,7 +506,6 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
authType: undefined,
|
||||
maxSessionTurns: undefined,
|
||||
experimentalLsp: undefined,
|
||||
experimentalHooks: undefined,
|
||||
channel: undefined,
|
||||
chatRecording: undefined,
|
||||
sessionId: undefined,
|
||||
|
|
|
|||
|
|
@ -623,6 +623,19 @@ export default {
|
|||
'No hook config selected': 'Keine Hook-Konfiguration ausgewählt',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'Um diesen Hook zu ändern oder zu entfernen, bearbeiten Sie settings.json direkt oder fragen Sie Qwen.',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'Hook-Konfiguration - Deaktiviert',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'Alle Hooks sind derzeit deaktiviert. Sie haben {{count}} die nicht ausgeführt werden.',
|
||||
'{{count}} configured hook': '{{count}} konfigurierter Hook',
|
||||
'{{count}} configured hooks': '{{count}} konfigurierte Hooks',
|
||||
'When hooks are disabled:': 'Wenn Hooks deaktiviert sind:',
|
||||
'No hook commands will execute': 'Keine Hook-Befehle werden ausgeführt',
|
||||
'StatusLine will not be displayed': 'StatusLine wird nicht angezeigt',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'Tool-Operationen werden ohne Hook-Validierung fortgesetzt',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'Um Hooks wieder zu aktivieren, entfernen Sie "disableAllHooks" aus settings.json oder fragen Sie Qwen Code.',
|
||||
// Hooks - Source
|
||||
Project: 'Projekt',
|
||||
User: 'Benutzer',
|
||||
|
|
|
|||
|
|
@ -696,6 +696,19 @@ export default {
|
|||
'No hook config selected': 'No hook config selected',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'Hook Configuration - Disabled',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.',
|
||||
'{{count}} configured hook': '{{count}} configured hook',
|
||||
'{{count}} configured hooks': '{{count}} configured hooks',
|
||||
'When hooks are disabled:': 'When hooks are disabled:',
|
||||
'No hook commands will execute': 'No hook commands will execute',
|
||||
'StatusLine will not be displayed': 'StatusLine will not be displayed',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'Tool operations will proceed without hook validation',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.',
|
||||
// Hooks - Source
|
||||
Project: 'Project',
|
||||
User: 'User',
|
||||
|
|
|
|||
|
|
@ -409,6 +409,19 @@ export default {
|
|||
'No hook config selected': 'フック設定が選択されていません',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'このフックを変更または削除するには、settings.json を直接編集するか、Qwen に尋ねてください。',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'フック設定 - 無効',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'すべてのフックは現在無効です。{{count}} が実行されていません。',
|
||||
'{{count}} configured hook': '{{count}} 個の設定されたフック',
|
||||
'{{count}} configured hooks': '{{count}} 個の設定されたフック',
|
||||
'When hooks are disabled:': 'フックが無効な場合:',
|
||||
'No hook commands will execute': 'フックコマンドは実行されません',
|
||||
'StatusLine will not be displayed': 'StatusLine は表示されません',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'ツール操作はフック検証なしで続行されます',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'フックを再有効化するには、settings.json から "disableAllHooks" を削除するか、Qwen Code に尋ねてください。',
|
||||
// Hooks - Source
|
||||
Project: 'プロジェクト',
|
||||
User: 'ユーザー',
|
||||
|
|
|
|||
|
|
@ -629,6 +629,19 @@ export default {
|
|||
'No hook config selected': 'Nenhuma configuração de hook selecionada',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'Para modificar ou remover este hook, edite settings.json diretamente ou pergunte ao Qwen.',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'Configuração de Hook - Desativado',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'Todos os hooks estão desativados. Você tem {{count}} que não estão em execução.',
|
||||
'{{count}} configured hook': '{{count}} hook configurado',
|
||||
'{{count}} configured hooks': '{{count}} hooks configurados',
|
||||
'When hooks are disabled:': 'Quando os hooks estão desativados:',
|
||||
'No hook commands will execute': 'Nenhum comando de hook será executado',
|
||||
'StatusLine will not be displayed': 'StatusLine não será exibido',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'As operações de ferramentas prosseguirão sem validação de hook',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'Para reativar os hooks, remova "disableAllHooks" do settings.json ou pergunte ao Qwen Code.',
|
||||
// Hooks - Source
|
||||
Project: 'Projeto',
|
||||
User: 'Usuário',
|
||||
|
|
|
|||
|
|
@ -634,6 +634,19 @@ export default {
|
|||
'No hook config selected': 'Конфигурация хука не выбрана',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'Чтобы изменить или удалить этот хук, отредактируйте settings.json напрямую или спросите Qwen.',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'Конфигурация хуков - Отключено',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'Все хуки в данный момент отключены. У вас {{count}} не выполняются.',
|
||||
'{{count}} configured hook': '{{count}} настроенный хук',
|
||||
'{{count}} configured hooks': '{{count}} настроенных хуков',
|
||||
'When hooks are disabled:': 'Когда хуки отключены:',
|
||||
'No hook commands will execute': 'Никакие команды хуков не будут выполняться',
|
||||
'StatusLine will not be displayed': 'StatusLine не будет отображаться',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'Операции инструментов будут выполняться без проверки хуков',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'Чтобы снова включить хуки, удалите "disableAllHooks" из settings.json или спросите Qwen Code.',
|
||||
// Hooks - Source
|
||||
Project: 'Проект',
|
||||
User: 'Пользователь',
|
||||
|
|
|
|||
|
|
@ -660,6 +660,19 @@ export default {
|
|||
'No hook config selected': '未选择 Hook 配置',
|
||||
'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
|
||||
'要修改或删除此 Hook,请直接编辑 settings.json 或询问 Qwen。',
|
||||
// Hooks - Disabled Step
|
||||
'Hook Configuration - Disabled': 'Hook 配置 - 已禁用',
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.':
|
||||
'所有 Hook 当前已禁用。您有 {{count}} 未运行。',
|
||||
'{{count}} configured hook': '{{count}} 个已配置的 Hook',
|
||||
'{{count}} configured hooks': '{{count}} 个已配置的 Hook',
|
||||
'When hooks are disabled:': '当 Hook 被禁用时:',
|
||||
'No hook commands will execute': '不会执行任何 Hook 命令',
|
||||
'StatusLine will not be displayed': '不会显示状态栏',
|
||||
'Tool operations will proceed without hook validation':
|
||||
'工具操作将在没有 Hook 验证的情况下继续',
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
|
||||
'要重新启用 Hook,请从 settings.json 中删除 "disableAllHooks" 或询问 Qwen Code。',
|
||||
// Hooks - Source
|
||||
Project: '项目',
|
||||
User: '用户',
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ describe('BuiltinCommandLoader', () => {
|
|||
mockConfig = {
|
||||
getFolderTrust: vi.fn().mockReturnValue(true),
|
||||
getUseModelRouter: () => false,
|
||||
getEnableHooks: vi.fn().mockReturnValue(true),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
|
||||
restoreCommandMock.mockReturnValue({
|
||||
|
|
@ -207,18 +207,19 @@ describe('BuiltinCommandLoader', () => {
|
|||
expect(modelCmd?.name).toBe('model');
|
||||
});
|
||||
|
||||
it('should include hooks command when enableHooks is true', async () => {
|
||||
const loader = new BuiltinCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
const hooksCmd = commands.find((c) => c.name === 'hooks');
|
||||
expect(hooksCmd).toBeDefined();
|
||||
});
|
||||
it('should always include hooks command regardless of disableAllHooks', async () => {
|
||||
// When disableAllHooks is false
|
||||
const loader1 = new BuiltinCommandLoader(mockConfig);
|
||||
const commands1 = await loader1.loadCommands(new AbortController().signal);
|
||||
const hooksCmd1 = commands1.find((c) => c.name === 'hooks');
|
||||
expect(hooksCmd1).toBeDefined();
|
||||
|
||||
it('should exclude hooks command when enableHooks is false', async () => {
|
||||
(mockConfig.getEnableHooks as Mock).mockReturnValue(false);
|
||||
const loader = new BuiltinCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
const hooksCmd = commands.find((c) => c.name === 'hooks');
|
||||
expect(hooksCmd).toBeUndefined();
|
||||
// When disableAllHooks is true - hooks command should still be available
|
||||
// (it will show a disabled state page in the UI instead of hiding the command)
|
||||
(mockConfig.getDisableAllHooks as Mock).mockReturnValue(true);
|
||||
const loader2 = new BuiltinCommandLoader(mockConfig);
|
||||
const commands2 = await loader2.loadCommands(new AbortController().signal);
|
||||
const hooksCmd2 = commands2.find((c) => c.name === 'hooks');
|
||||
expect(hooksCmd2).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||
exportCommand,
|
||||
extensionsCommand,
|
||||
helpCommand,
|
||||
...(this.config?.getEnableHooks() ? [hooksCommand] : []),
|
||||
hooksCommand,
|
||||
await ideCommand(),
|
||||
initCommand,
|
||||
languageCommand,
|
||||
|
|
|
|||
124
packages/cli/src/ui/components/hooks/HooksDisabledStep.test.tsx
Normal file
124
packages/cli/src/ui/components/hooks/HooksDisabledStep.test.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { HooksDisabledStep } from './HooksDisabledStep.js';
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('../../../i18n/index.js', () => ({
|
||||
t: vi.fn((key: string, options?: { count?: string }) => {
|
||||
// Handle pluralization
|
||||
if (key === '{{count}} configured hook' && options?.count) {
|
||||
return `${options.count} configured hook`;
|
||||
}
|
||||
if (key === '{{count}} configured hooks' && options?.count) {
|
||||
return `${options.count} configured hooks`;
|
||||
}
|
||||
// Handle interpolation for main message
|
||||
if (
|
||||
key ===
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.' &&
|
||||
options?.count
|
||||
) {
|
||||
return `All hooks are currently disabled. You have ${options.count} that are not running.`;
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock semantic-colors
|
||||
vi.mock('../../semantic-colors.js', () => ({
|
||||
theme: {
|
||||
text: {
|
||||
primary: 'white',
|
||||
secondary: 'gray',
|
||||
},
|
||||
status: {
|
||||
warning: 'yellow',
|
||||
error: 'red',
|
||||
success: 'green',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('HooksDisabledStep', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render disabled title', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={2} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Hook Configuration - Disabled');
|
||||
});
|
||||
|
||||
it('should show configured hooks count', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={2} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('2 configured hooks');
|
||||
});
|
||||
|
||||
it('should show singular form for single hook', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={1} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('1 configured hook');
|
||||
});
|
||||
|
||||
it('should show zero hooks message', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('0 configured hooks');
|
||||
});
|
||||
|
||||
it('should show explanation items', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={2} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('When hooks are disabled:');
|
||||
expect(output).toContain('No hook commands will execute');
|
||||
expect(output).toContain('StatusLine will not be displayed');
|
||||
expect(output).toContain(
|
||||
'Tool operations will proceed without hook validation',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show re-enable instructions', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={2} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('To re-enable hooks');
|
||||
expect(lastFrame()).toContain('disableAllHooks');
|
||||
expect(lastFrame()).toContain('settings.json');
|
||||
});
|
||||
|
||||
it('should show Esc hint', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={2} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Esc to close');
|
||||
});
|
||||
|
||||
it('should handle large hook counts', () => {
|
||||
const { lastFrame } = render(
|
||||
<HooksDisabledStep configuredHooksCount={100} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('100 configured hooks');
|
||||
});
|
||||
});
|
||||
89
packages/cli/src/ui/components/hooks/HooksDisabledStep.tsx
Normal file
89
packages/cli/src/ui/components/hooks/HooksDisabledStep.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface HooksDisabledStepProps {
|
||||
configuredHooksCount: number;
|
||||
}
|
||||
|
||||
export function HooksDisabledStep({
|
||||
configuredHooksCount,
|
||||
}: HooksDisabledStepProps): React.JSX.Element {
|
||||
// Note: The i18n t() function expects string parameters (Record<string, string>).
|
||||
// Pluralization is handled manually by selecting the appropriate translation key
|
||||
// based on the count, since the i18n system doesn't support ICU MessageFormat.
|
||||
const hooksText =
|
||||
configuredHooksCount === 1
|
||||
? t('{{count}} configured hook', { count: String(configuredHooksCount) })
|
||||
: t('{{count}} configured hooks', {
|
||||
count: String(configuredHooksCount),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{/* Title */}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.status.warning}>
|
||||
{t('Hook Configuration - Disabled')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Main message */}
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
{t(
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.',
|
||||
{
|
||||
count: hooksText,
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Explanation */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{t('When hooks are disabled:')}
|
||||
</Text>
|
||||
{/* Note: Using middle dot (·) as bullet character. This is consistent with
|
||||
other CLI components. If a design system evolves, consider extracting
|
||||
to a shared constant or using a BulletList component. */}
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>
|
||||
{` · ${t('No hook commands will execute')}`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>
|
||||
{` · ${t('StatusLine will not be displayed')}`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>
|
||||
{` · ${t('Tool operations will proceed without hook validation')}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* How to re-enable */}
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t(
|
||||
'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Footer hint */}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>{t('Esc to close')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,9 +4,18 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { HooksManagementDialog } from './HooksManagementDialog.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import type { Key } from '../../contexts/KeypressContext.js';
|
||||
|
||||
// Mock useKeypress
|
||||
vi.mock('../../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedUseKeypress = vi.mocked(useKeypress);
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('../../../i18n/index.js', () => ({
|
||||
|
|
@ -18,6 +27,20 @@ vi.mock('../../../i18n/index.js', () => ({
|
|||
if (key === '{{count}} hooks configured' && options?.count) {
|
||||
return `${options.count} hooks configured`;
|
||||
}
|
||||
if (key === '{{count}} configured hook' && options?.count) {
|
||||
return `${options.count} configured hook`;
|
||||
}
|
||||
if (key === '{{count}} configured hooks' && options?.count) {
|
||||
return `${options.count} configured hooks`;
|
||||
}
|
||||
// Handle interpolation for disabled message
|
||||
if (
|
||||
key ===
|
||||
'All hooks are currently disabled. You have {{count}} that are not running.' &&
|
||||
options?.count
|
||||
) {
|
||||
return `All hooks are currently disabled. You have ${options.count} that are not running.`;
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
}));
|
||||
|
|
@ -35,6 +58,7 @@ vi.mock('../../contexts/ConfigContext.js', async (importOriginal) => {
|
|||
...actual,
|
||||
useConfig: vi.fn(() => ({
|
||||
getExtensions: vi.fn(() => []),
|
||||
getDisableAllHooks: vi.fn(() => false),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
|
@ -62,6 +86,7 @@ vi.mock('../../semantic-colors.js', () => ({
|
|||
status: {
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
},
|
||||
border: {
|
||||
default: 'gray',
|
||||
|
|
@ -82,46 +107,161 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
// Helper to create a key object
|
||||
function createKey(name: string, sequence = ''): Key {
|
||||
return {
|
||||
name,
|
||||
sequence,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe('HooksManagementDialog', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
let keypressHandler: ((key: Key) => void) | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
keypressHandler = null;
|
||||
|
||||
// Mock useKeypress to capture the handler
|
||||
mockedUseKeypress.mockImplementation((handler) => {
|
||||
keypressHandler = handler;
|
||||
});
|
||||
});
|
||||
|
||||
it('should render loading state initially', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Loading hooks');
|
||||
afterEach(() => {
|
||||
keypressHandler = null;
|
||||
});
|
||||
|
||||
it('should render with border', async () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
describe('Initial rendering', () => {
|
||||
it('should render loading state initially', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(lastFrame()).toContain('Loading hooks');
|
||||
});
|
||||
|
||||
// The dialog should have a border (rendered as box-drawing characters)
|
||||
const output = lastFrame();
|
||||
expect(output).toBeTruthy();
|
||||
it('should render with border', async () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
unmount();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// The dialog should have a border (rendered as box-drawing characters)
|
||||
const output = lastFrame();
|
||||
expect(output).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle empty hooks list gracefully', async () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const output = lastFrame();
|
||||
// Should show 0 hooks configured when no hooks are configured
|
||||
expect(output).toContain('0 hooks configured');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty hooks list gracefully', async () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
describe('Keyboard navigation - HOOKS_LIST step', () => {
|
||||
it('should register keypress handler with isActive: true', async () => {
|
||||
renderWithProviders(<HooksManagementDialog onClose={mockOnClose} />);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const output = lastFrame();
|
||||
// Should show 0 hooks configured when no hooks are configured
|
||||
expect(output).toContain('0 hooks configured');
|
||||
expect(mockedUseKeypress).toHaveBeenCalled();
|
||||
const options = mockedUseKeypress.mock.calls[0][1];
|
||||
expect(options).toEqual({ isActive: true });
|
||||
});
|
||||
|
||||
unmount();
|
||||
it('should close dialog on Escape key', async () => {
|
||||
renderWithProviders(<HooksManagementDialog onClose={mockOnClose} />);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(keypressHandler).not.toBeNull();
|
||||
keypressHandler!(createKey('escape', '\x1b'));
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not go above first item when pressing up', async () => {
|
||||
const { unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Press up multiple times from first item
|
||||
keypressHandler!(createKey('up'));
|
||||
keypressHandler!(createKey('up'));
|
||||
keypressHandler!(createKey('up'));
|
||||
|
||||
// Should still be at first item (no crash)
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard navigation - HOOKS_DISABLED step', () => {
|
||||
it('should show disabled state when disableAllHooks is true', async () => {
|
||||
// Override the mock for this test
|
||||
const configContext = await import('../../contexts/ConfigContext.js');
|
||||
vi.mocked(configContext.useConfig).mockReturnValue({
|
||||
getExtensions: vi.fn(() => []),
|
||||
getDisableAllHooks: vi.fn(() => true),
|
||||
} as unknown as ReturnType<typeof configContext.useConfig>);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HooksManagementDialog onClose={mockOnClose} />,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Hook Configuration - Disabled');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should close dialog on Escape key when hooks are disabled', async () => {
|
||||
const configContext = await import('../../contexts/ConfigContext.js');
|
||||
vi.mocked(configContext.useConfig).mockReturnValue({
|
||||
getExtensions: vi.fn(() => []),
|
||||
getDisableAllHooks: vi.fn(() => true),
|
||||
} as unknown as ReturnType<typeof configContext.useConfig>);
|
||||
|
||||
renderWithProviders(<HooksManagementDialog onClose={mockOnClose} />);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(keypressHandler).not.toBeNull();
|
||||
keypressHandler!(createKey('escape', '\x1b'));
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading and error states', () => {
|
||||
it('should allow Escape to close during loading state', () => {
|
||||
renderWithProviders(<HooksManagementDialog onClose={mockOnClose} />);
|
||||
|
||||
// Don't wait for loading to complete
|
||||
expect(keypressHandler).not.toBeNull();
|
||||
keypressHandler!(createKey('escape', '\x1b'));
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { HOOKS_MANAGEMENT_STEPS } from './types.js';
|
|||
import { HooksListStep } from './HooksListStep.js';
|
||||
import { HookDetailStep } from './HookDetailStep.js';
|
||||
import { HookConfigDetailStep } from './HookConfigDetailStep.js';
|
||||
import { HooksDisabledStep } from './HooksDisabledStep.js';
|
||||
import {
|
||||
DISPLAY_HOOK_EVENTS,
|
||||
getTranslatedSourceDisplayMap,
|
||||
|
|
@ -111,8 +112,17 @@ export function HooksManagementDialog({
|
|||
const { columns: width } = useTerminalSize();
|
||||
const boxWidth = width - 4;
|
||||
|
||||
// Check if hooks are disabled
|
||||
// Note: This value is captured at dialog open time. If disableAllHooks
|
||||
// changes while the dialog is open (e.g., via settings.json edit),
|
||||
// the dialog will not react to the change until it's closed and reopened.
|
||||
// This is intentional - the dialog represents a snapshot of the current state.
|
||||
const disableAllHooks = config?.getDisableAllHooks() ?? false;
|
||||
|
||||
const [navigationStack, setNavigationStack] = useState<string[]>([
|
||||
HOOKS_MANAGEMENT_STEPS.HOOKS_LIST,
|
||||
disableAllHooks
|
||||
? HOOKS_MANAGEMENT_STEPS.HOOKS_DISABLED
|
||||
: HOOKS_MANAGEMENT_STEPS.HOOKS_LIST,
|
||||
]);
|
||||
const [selectedHookIndex, setSelectedHookIndex] = useState<number>(-1);
|
||||
const [selectedConfigIndex, setSelectedConfigIndex] = useState<number>(-1);
|
||||
|
|
@ -148,6 +158,12 @@ export function HooksManagementDialog({
|
|||
}
|
||||
|
||||
switch (currentStep) {
|
||||
case HOOKS_MANAGEMENT_STEPS.HOOKS_DISABLED:
|
||||
if (key.name === 'escape') {
|
||||
onClose();
|
||||
}
|
||||
break;
|
||||
|
||||
case HOOKS_MANAGEMENT_STEPS.HOOKS_LIST:
|
||||
if (key.name === 'up') {
|
||||
setListSelectedIndex((prev) => Math.max(0, prev - 1));
|
||||
|
|
@ -339,8 +355,19 @@ export function HooksManagementDialog({
|
|||
return null;
|
||||
}, [selectedHook, selectedConfigIndex]);
|
||||
|
||||
// Calculate total configured hooks count
|
||||
const configuredHooksCount = useMemo(
|
||||
() => hooks.reduce((sum, hook) => sum + hook.configs.length, 0),
|
||||
[hooks],
|
||||
);
|
||||
|
||||
// Render based on current step
|
||||
const renderContent = () => {
|
||||
// Show disabled state first (before loading check)
|
||||
if (currentStep === HOOKS_MANAGEMENT_STEPS.HOOKS_DISABLED) {
|
||||
return <HooksDisabledStep configuredHooksCount={configuredHooksCount} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
|
|
|
|||
|
|
@ -185,21 +185,13 @@ export function getTranslatedSourceDisplayMap(): Record<
|
|||
|
||||
/**
|
||||
* List of hook events to display in the UI
|
||||
* Automatically synced with HookEventName enum from core.
|
||||
* Note: Order follows the enum definition order. If UI presentation order
|
||||
* needs to be different (e.g., grouped by lifecycle phase), consider using
|
||||
* an explicit sorted array instead. Current enum order is acceptable for display.
|
||||
*/
|
||||
export const DISPLAY_HOOK_EVENTS: HookEventName[] = [
|
||||
HookEventName.Stop,
|
||||
HookEventName.PreToolUse,
|
||||
HookEventName.PostToolUse,
|
||||
HookEventName.PostToolUseFailure,
|
||||
HookEventName.Notification,
|
||||
HookEventName.UserPromptSubmit,
|
||||
HookEventName.SessionStart,
|
||||
HookEventName.SessionEnd,
|
||||
HookEventName.SubagentStart,
|
||||
HookEventName.SubagentStop,
|
||||
HookEventName.PreCompact,
|
||||
HookEventName.PermissionRequest,
|
||||
];
|
||||
export const DISPLAY_HOOK_EVENTS: HookEventName[] =
|
||||
Object.values(HookEventName);
|
||||
|
||||
/**
|
||||
* Create empty hook event display info
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
export { HooksManagementDialog } from './HooksManagementDialog.js';
|
||||
export { HooksListStep } from './HooksListStep.js';
|
||||
export { HookDetailStep } from './HookDetailStep.js';
|
||||
export { HookConfigDetailStep } from './HookConfigDetailStep.js';
|
||||
export { HooksDisabledStep } from './HooksDisabledStep.js';
|
||||
export * from './types.js';
|
||||
export * from './constants.js';
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export interface HookConfigDisplayInfo {
|
|||
* Hook management dialog step names
|
||||
*/
|
||||
export const HOOKS_MANAGEMENT_STEPS = {
|
||||
HOOKS_DISABLED: 'hooks_disabled',
|
||||
HOOKS_LIST: 'hooks_list',
|
||||
HOOK_DETAIL: 'hook_detail',
|
||||
HOOK_CONFIG_DETAIL: 'hook_config_detail',
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const useAttentionNotifications = ({
|
|||
// Fire idle_prompt notification hook when entering idle state
|
||||
if (config && !idleNotificationSentRef.current) {
|
||||
const messageBus = config.getMessageBus();
|
||||
const hooksEnabled = config.getEnableHooks();
|
||||
const hooksEnabled = !config.getDisableAllHooks();
|
||||
if (hooksEnabled && messageBus) {
|
||||
fireNotificationHook(
|
||||
messageBus,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const mockConfig = {
|
|||
getShellExecutionConfig: () => ({ terminalWidth: 80, terminalHeight: 24 }),
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
getHookSystem: vi.fn().mockReturnValue(undefined),
|
||||
getDebugLogger: vi.fn().mockReturnValue({
|
||||
debug: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ describe('Server Config (config.ts)', () => {
|
|||
const mockMessageBus = { request: vi.fn() };
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
enableHooks: true,
|
||||
disableAllHooks: false,
|
||||
});
|
||||
// Set messageBus using the setter
|
||||
config.setMessageBus(mockMessageBus as unknown as MessageBus);
|
||||
|
|
@ -378,7 +378,7 @@ describe('Server Config (config.ts)', () => {
|
|||
it('should not fire notification hook when hooks are disabled', async () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
enableHooks: false,
|
||||
disableAllHooks: true,
|
||||
});
|
||||
const authType = AuthType.USE_GEMINI;
|
||||
const mockContentConfig = {
|
||||
|
|
|
|||
|
|
@ -419,12 +419,15 @@ export interface ConfigParameters {
|
|||
modelProvidersConfig?: ModelProvidersConfig;
|
||||
/** Multi-agent collaboration settings (Arena, Team, Swarm) */
|
||||
agents?: AgentsCollabSettings;
|
||||
/** Enable hook system for lifecycle events */
|
||||
enableHooks?: boolean;
|
||||
/**
|
||||
* Disable all hooks (default: false, hooks enabled).
|
||||
* Migration note: This replaces the deprecated hooksConfig.enabled setting.
|
||||
* Users with old settings.json containing hooksConfig.enabled should migrate
|
||||
* to use disableAllHooks instead (note: inverted logic - enabled:true → disableAllHooks:false).
|
||||
*/
|
||||
disableAllHooks?: boolean;
|
||||
/** Hooks configuration from settings */
|
||||
hooks?: Record<string, unknown>;
|
||||
/** Hooks config settings (enabled, disabled list) */
|
||||
hooksConfig?: Record<string, unknown>;
|
||||
/** Warnings generated during configuration resolution */
|
||||
warnings?: string[];
|
||||
/**
|
||||
|
|
@ -594,9 +597,8 @@ export class Config {
|
|||
private readonly eventEmitter?: EventEmitter;
|
||||
private readonly channel: string | undefined;
|
||||
private readonly defaultFileEncoding: FileEncodingType | undefined;
|
||||
private readonly enableHooks: boolean;
|
||||
private readonly disableAllHooks: boolean;
|
||||
private readonly hooks?: Record<string, unknown>;
|
||||
private readonly hooksConfig?: Record<string, unknown>;
|
||||
private hookSystem?: HookSystem;
|
||||
private messageBus?: MessageBus;
|
||||
|
||||
|
|
@ -761,9 +763,8 @@ export class Config {
|
|||
enabledExtensionOverrides: this.overrideExtensions,
|
||||
isWorkspaceTrusted: this.isTrustedFolder(),
|
||||
});
|
||||
this.enableHooks = params.enableHooks ?? false;
|
||||
this.disableAllHooks = params.disableAllHooks ?? false;
|
||||
this.hooks = params.hooks;
|
||||
this.hooksConfig = params.hooksConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -788,7 +789,7 @@ export class Config {
|
|||
this.debugLogger.debug('Extension manager initialized');
|
||||
|
||||
// Initialize hook system if enabled
|
||||
if (this.enableHooks) {
|
||||
if (!this.disableAllHooks) {
|
||||
this.hookSystem = new HookSystem(this);
|
||||
await this.hookSystem.initialize();
|
||||
this.debugLogger.debug('Hook system initialized');
|
||||
|
|
@ -1074,7 +1075,7 @@ export class Config {
|
|||
|
||||
// Fire auth_success notification hook (supports both interactive & non-interactive)
|
||||
const messageBus = this.getMessageBus();
|
||||
const hooksEnabled = this.getEnableHooks();
|
||||
const hooksEnabled = !this.getDisableAllHooks();
|
||||
if (hooksEnabled && messageBus) {
|
||||
fireNotificationHook(
|
||||
messageBus,
|
||||
|
|
@ -1781,10 +1782,10 @@ export class Config {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if hooks are enabled.
|
||||
* Check if all hooks are disabled.
|
||||
*/
|
||||
getEnableHooks(): boolean {
|
||||
return this.enableHooks;
|
||||
getDisableAllHooks(): boolean {
|
||||
return this.disableAllHooks;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1803,17 +1804,6 @@ export class Config {
|
|||
this.messageBus = messageBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of disabled hook names.
|
||||
* This is used by the HookRegistry to filter out disabled hooks.
|
||||
*/
|
||||
getDisabledHooks(): string[] {
|
||||
const hooksConfig = this.hooksConfig;
|
||||
if (!hooksConfig) return [];
|
||||
const disabled = hooksConfig['disabled'];
|
||||
return Array.isArray(disabled) ? (disabled as string[]) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project-level hooks configuration.
|
||||
* This is used by the HookRegistry to load project-specific hooks.
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ describe('Gemini Client (client.ts)', () => {
|
|||
getChatRecordingService: vi.fn().mockReturnValue(undefined),
|
||||
getResumedSessionData: vi.fn().mockReturnValue(undefined),
|
||||
getArenaAgentClient: vi.fn().mockReturnValue(null),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
getArenaManager: vi.fn().mockReturnValue(null),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
hasHooksForEvent: vi.fn().mockReturnValue(false),
|
||||
|
|
@ -2415,11 +2415,11 @@ Other open files:
|
|||
request: vi.fn(),
|
||||
response: vi.fn(),
|
||||
};
|
||||
vi.spyOn(client['config'], 'getEnableHooks').mockReturnValue(true);
|
||||
vi.spyOn(client['config'], 'getMessageBus').mockReturnValue(
|
||||
vi.mocked(mockConfig.getDisableAllHooks).mockReturnValue(false);
|
||||
vi.mocked(mockConfig.getMessageBus).mockReturnValue(
|
||||
mockMessageBus as unknown as ReturnType<Config['getMessageBus']>,
|
||||
);
|
||||
vi.spyOn(client['config'], 'hasHooksForEvent').mockReturnValue(false);
|
||||
vi.mocked(mockConfig.hasHooksForEvent).mockReturnValue(false);
|
||||
|
||||
const stream = client.sendMessageStream(
|
||||
[{ text: 'Hi' }],
|
||||
|
|
@ -2439,11 +2439,11 @@ Other open files:
|
|||
request: vi.fn(),
|
||||
response: vi.fn(),
|
||||
};
|
||||
vi.spyOn(client['config'], 'getEnableHooks').mockReturnValue(true);
|
||||
vi.spyOn(client['config'], 'getMessageBus').mockReturnValue(
|
||||
vi.mocked(mockConfig.getDisableAllHooks).mockReturnValue(false);
|
||||
vi.mocked(mockConfig.getMessageBus).mockReturnValue(
|
||||
mockMessageBus as unknown as ReturnType<Config['getMessageBus']>,
|
||||
);
|
||||
vi.spyOn(client['config'], 'hasHooksForEvent').mockReturnValue(false);
|
||||
vi.mocked(mockConfig.hasHooksForEvent).mockReturnValue(false);
|
||||
|
||||
const stream = client.sendMessageStream(
|
||||
[{ text: 'Hi' }],
|
||||
|
|
@ -2463,11 +2463,11 @@ Other open files:
|
|||
request: vi.fn().mockResolvedValue({ modifiedPrompt: undefined }),
|
||||
response: vi.fn(),
|
||||
};
|
||||
vi.spyOn(client['config'], 'getEnableHooks').mockReturnValue(true);
|
||||
vi.spyOn(client['config'], 'getMessageBus').mockReturnValue(
|
||||
vi.mocked(mockConfig.getDisableAllHooks).mockReturnValue(false);
|
||||
vi.mocked(mockConfig.getMessageBus).mockReturnValue(
|
||||
mockMessageBus as unknown as ReturnType<Config['getMessageBus']>,
|
||||
);
|
||||
vi.spyOn(client['config'], 'hasHooksForEvent').mockImplementation(
|
||||
vi.mocked(mockConfig.hasHooksForEvent).mockImplementation(
|
||||
(event: string) => event === 'UserPromptSubmit',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -463,7 +463,7 @@ export class GeminiClient {
|
|||
}
|
||||
|
||||
// Fire UserPromptSubmit hook through MessageBus (only if hooks are enabled)
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
const hooksEnabled = !this.config.getDisableAllHooks();
|
||||
const messageBus = this.config.getMessageBus();
|
||||
if (
|
||||
messageType !== SendMessageType.Retry &&
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ describe('CoreToolScheduler', () => {
|
|||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -345,7 +345,7 @@ describe('CoreToolScheduler', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -390,7 +390,7 @@ describe('CoreToolScheduler', () => {
|
|||
getPermissionsDeny: () => undefined,
|
||||
isInteractive: () => true,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
|
|
@ -433,7 +433,7 @@ describe('CoreToolScheduler', () => {
|
|||
getPermissionsDeny: () => ['write_file', 'edit', 'run_shell_command'],
|
||||
isInteractive: () => false, // Value doesn't matter, but included for completeness
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
|
|
@ -465,7 +465,7 @@ describe('CoreToolScheduler', () => {
|
|||
getPermissionsDeny: () => ['write_file', 'edit'],
|
||||
isInteractive: () => false, // Value doesn't matter
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
|
|
@ -508,7 +508,7 @@ describe('CoreToolScheduler', () => {
|
|||
getPermissionsDeny: () => undefined,
|
||||
isInteractive: () => true,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
|
|
@ -588,7 +588,7 @@ describe('CoreToolScheduler', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -676,7 +676,7 @@ describe('CoreToolScheduler', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -770,7 +770,7 @@ describe('CoreToolScheduler with payload', () => {
|
|||
getExperimentalZedIntegration: () => false,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1112,7 +1112,7 @@ describe('CoreToolScheduler edit cancellation', () => {
|
|||
getExperimentalZedIntegration: () => false,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1221,7 +1221,7 @@ describe('CoreToolScheduler YOLO mode', () => {
|
|||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1364,7 +1364,7 @@ describe('CoreToolScheduler cancellation during executing with live output', ()
|
|||
}),
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1466,7 +1466,7 @@ describe('CoreToolScheduler request queueing', () => {
|
|||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1590,7 +1590,7 @@ describe('CoreToolScheduler request queueing', () => {
|
|||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -1667,7 +1667,7 @@ describe('CoreToolScheduler request queueing', () => {
|
|||
getExperimentalZedIntegration: () => false,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const testTool = new TestApprovalTool(mockConfig);
|
||||
|
|
@ -1832,7 +1832,7 @@ describe('CoreToolScheduler truncated output protection', () => {
|
|||
getChatRecordingService: () => undefined,
|
||||
isInteractive: () => true,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -2031,7 +2031,7 @@ describe('CoreToolScheduler Sequential Execution', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -2153,7 +2153,7 @@ describe('CoreToolScheduler Sequential Execution', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
|
|
@ -2329,7 +2329,7 @@ describe('CoreToolScheduler plan mode with ask_user_question', () => {
|
|||
getExperimentalZedIntegration: () => false,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
return new CoreToolScheduler({
|
||||
|
|
@ -2972,7 +2972,7 @@ describe('Fire hook functions integration', () => {
|
|||
getGeminiClient: () => null,
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
|
||||
return new CoreToolScheduler({
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,7 @@ export class CoreToolScheduler {
|
|||
const messageBus = this.config.getMessageBus() as
|
||||
| MessageBus
|
||||
| undefined;
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
const hooksEnabled = !this.config.getDisableAllHooks();
|
||||
|
||||
if (hooksEnabled && messageBus) {
|
||||
const permissionMode = String(this.config.getApprovalMode());
|
||||
|
|
@ -1326,7 +1326,7 @@ export class CoreToolScheduler {
|
|||
|
||||
// Get MessageBus for hook execution
|
||||
const messageBus = this.config.getMessageBus() as MessageBus | undefined;
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
const hooksEnabled = !this.config.getDisableAllHooks();
|
||||
|
||||
// PreToolUse Hook
|
||||
if (hooksEnabled && messageBus) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ describe('executeToolCall', () => {
|
|||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getChatRecordingService: () => undefined,
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
getHookSystem: vi.fn().mockReturnValue(undefined),
|
||||
getDebugLogger: vi.fn().mockReturnValue({
|
||||
debug: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ describe('HookRegistry', () => {
|
|||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
getHooks: vi.fn().mockReturnValue(undefined),
|
||||
getProjectHooks: vi.fn().mockReturnValue(undefined),
|
||||
getDisabledHooks: vi.fn().mockReturnValue([]),
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
mockFeedbackEmitter = {
|
||||
|
|
@ -123,21 +122,20 @@ describe('HookRegistry', () => {
|
|||
expect(postHooks[0].config.name).toBe('post-hook');
|
||||
});
|
||||
|
||||
it('should filter out disabled hooks', async () => {
|
||||
mockConfig.getDisabledHooks = vi.fn().mockReturnValue(['disabled-hook']);
|
||||
it('should register all hooks as enabled by default', async () => {
|
||||
const hooksConfig = {
|
||||
[HookEventName.PreToolUse]: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
command: 'echo enabled',
|
||||
name: 'enabled-hook',
|
||||
command: 'echo first',
|
||||
name: 'first-hook',
|
||||
},
|
||||
{
|
||||
type: HookType.Command,
|
||||
command: 'echo disabled',
|
||||
name: 'disabled-hook',
|
||||
command: 'echo second',
|
||||
name: 'second-hook',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -149,8 +147,9 @@ describe('HookRegistry', () => {
|
|||
await registry.initialize();
|
||||
|
||||
const hooks = registry.getHooksForEvent(HookEventName.PreToolUse);
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].config.name).toBe('enabled-hook');
|
||||
expect(hooks).toHaveLength(2);
|
||||
expect(hooks[0].enabled).toBe(true);
|
||||
expect(hooks[1].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should sort hooks by source priority', async () => {
|
||||
|
|
@ -181,37 +180,6 @@ describe('HookRegistry', () => {
|
|||
});
|
||||
|
||||
describe('setHookEnabled', () => {
|
||||
it('should enable a disabled hook', async () => {
|
||||
mockConfig.getDisabledHooks = vi.fn().mockReturnValue(['test-hook']);
|
||||
const hooksConfig = {
|
||||
[HookEventName.PreToolUse]: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
command: 'echo test',
|
||||
name: 'test-hook',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
mockConfig.getHooks = vi.fn().mockReturnValue(hooksConfig);
|
||||
|
||||
const registry = new HookRegistry(mockConfig);
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getHooksForEvent(HookEventName.PreToolUse)).toHaveLength(
|
||||
0,
|
||||
);
|
||||
|
||||
registry.setHookEnabled('test-hook', true);
|
||||
|
||||
const hooks = registry.getHooksForEvent(HookEventName.PreToolUse);
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable an enabled hook', async () => {
|
||||
const hooksConfig = {
|
||||
[HookEventName.PreToolUse]: [
|
||||
|
|
@ -237,9 +205,40 @@ describe('HookRegistry', () => {
|
|||
|
||||
registry.setHookEnabled('test-hook', false);
|
||||
|
||||
const hooks = registry.getHooksForEvent(HookEventName.PreToolUse);
|
||||
expect(hooks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should enable a disabled hook', async () => {
|
||||
const hooksConfig = {
|
||||
[HookEventName.PreToolUse]: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
command: 'echo test',
|
||||
name: 'test-hook',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
mockConfig.getHooks = vi.fn().mockReturnValue(hooksConfig);
|
||||
|
||||
const registry = new HookRegistry(mockConfig);
|
||||
await registry.initialize();
|
||||
|
||||
// First disable the hook
|
||||
registry.setHookEnabled('test-hook', false);
|
||||
expect(registry.getHooksForEvent(HookEventName.PreToolUse)).toHaveLength(
|
||||
0,
|
||||
);
|
||||
|
||||
// Then enable it again
|
||||
registry.setHookEnabled('test-hook', true);
|
||||
expect(registry.getHooksForEvent(HookEventName.PreToolUse)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update all hooks with matching name', async () => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export interface HookRegistryConfig {
|
|||
isTrustedFolder(): boolean;
|
||||
getHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined;
|
||||
getProjectHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined;
|
||||
getDisabledHooks(): string[];
|
||||
getExtensions(): ExtensionWithHooks[];
|
||||
}
|
||||
|
||||
|
|
@ -250,18 +249,13 @@ please review the project settings (.qwen/settings.json) and remove them.`;
|
|||
return;
|
||||
}
|
||||
|
||||
// Get disabled hooks list from settings
|
||||
const disabledHooks = this.config.getDisabledHooks();
|
||||
|
||||
for (const hookConfig of definition.hooks) {
|
||||
if (
|
||||
hookConfig &&
|
||||
typeof hookConfig === 'object' &&
|
||||
this.validateHookConfig(hookConfig, eventName, source)
|
||||
) {
|
||||
// Check if this hook is in the disabled list
|
||||
const hookName = this.getHookName({ config: hookConfig });
|
||||
const isDisabled = disabledHooks.includes(hookName);
|
||||
|
||||
// Check for duplicate hooks (same name+command+source+eventName+matcher+sequential)
|
||||
const isDuplicate = this.entries.some(
|
||||
|
|
@ -288,7 +282,7 @@ please review the project settings (.qwen/settings.json) and remove them.`;
|
|||
eventName,
|
||||
matcher: definition.matcher,
|
||||
sequential: definition.sequential,
|
||||
enabled: !isDisabled,
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
// Invalid hooks are logged and discarded here, they won't reach HookRunner
|
||||
|
|
|
|||
|
|
@ -640,23 +640,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"hooksConfig": {
|
||||
"description": "Hook configurations for intercepting and customizing agent behavior.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"disabled": {
|
||||
"description": "List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
"disableAllHooks": {
|
||||
"description": "Temporarily disable all hooks without deleting configurations. Default is false (hooks enabled).",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"hooks": {
|
||||
"description": "Hook event configurations for extending CLI behavior at various lifecycle points.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue