@selesai/code 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.6] - 2026-08-15
8
+
9
+ ### Added
10
+ - **Auto-relaunching workflows.** The four `/workflow-*` modes (task, prototype, quicktype, loop) now run fully unattended: when a scripted workflow exhausts its per-run fan-out budget before the goal is clean, the async executor mints a fresh budget and re-runs the same script with the same mission and progress file, up to `config.maxWorkflowAutoRelaunches` (default 12; `0` = unlimited). Only `budget` results relaunch — real child failures still surface immediately. `AsyncStatus` gained `workflowRelaunchCount`.
11
+ - **Packaged prompts now load.** The pi-subagents extension registers `resources_discover`, so the bundled prompts (`review-loop.md`, `parallel-*.md`, `gather-context-and-clarify.md`) are actually discoverable. The `workflow-drive.md` prompt was removed as superseded by auto-relaunch.
12
+ - **Goal visibility on workflow launches.** Each `/workflow-*` launch emits its goal at start (visible in `subagent status` workflow emits) and names the auto-created mission after the goal (`mission.list` / `mission.show`).
13
+
14
+ ### Changed
15
+ - **Bounded build→review→fix rounds.** The workflow auto-loop now instructs the builder to implement one small, self-contained slice per round ("do not attempt the whole plan") instead of the entire plan at once, with per-run timeouts (build/fix 45m, review 15m).
16
+ - **Progress-file scoped reviews.** Builder appends a `## Round N` ledger entry (files, summary, validation) to `.pi-subagents/progress/<mode>.md`; the commentator scopes its review to the latest round entry plus the immediately preceding fix entry, falling back to the full uncommitted diff when the file is missing or empty. Blocking reviews must list a `Remaining work:` section; the next build round picks up from it.
17
+ - **Previous review feedback now reaches the next builder round** (previously it was dropped).
18
+
19
+ ## [0.8.5] - 2026-08-14
20
+
21
+ ### Added
22
+ - **`/settings-factory-reset` slash command.** Restores `~/.selesai/agent/settings.json` to the bundled factory defaults after a warning and confirmation. A backup is saved as `settings.json.bak` before the reset, credentials (`auth.json`), sessions, extensions, skills, and themes are untouched, and settings reload live without a restart.
23
+ - **Install from the public repository.** `npm install -g github:SelesaiInTech/selesai-code` now works: the package gained a `prepare` script that builds `dist` from source on install.
24
+
25
+ ### Changed
26
+ - **Simplified install guide.** The documented install command is now plain `npm install -g @selesai/code`. The previous `--ignore-scripts` advice was actively harmful: `@ast-grep/cli` (a runtime dependency) requires its `postinstall` to fetch its platform binary. Quickstart, README, and doc-web install pages updated, and quickstart no longer references the upstream `pi` binary or `~/.pi` paths.
27
+ - **Bumped default `retry.maxRetries` from 5 to 7** in bundled defaults.
28
+
7
29
  ## [0.8.4] - 2026-08-13
8
30
 
9
31
  ### Fixed
package/README.md CHANGED
@@ -7,7 +7,7 @@ The goal is simple: make a capable coding agent useful out of the box, maximize
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g --ignore-scripts @selesai/code
10
+ npm install -g @selesai/code
11
11
  selesai
12
12
  ```
13
13
 
@@ -17,6 +17,12 @@ For a one-off run using the latest published package:
17
17
  npx @selesai/code
18
18
  ```
19
19
 
20
+ To install directly from the public repository (builds the latest `main` from source):
21
+
22
+ ```bash
23
+ npm install -g github:SelesaiInTech/selesai-code
24
+ ```
25
+
20
26
  The published npm package is **`@selesai/code`** and its executable is **`selesai`**. Selesai uses `~/.selesai/agent` for user state and `.selesai/` for project-local settings and resources.
21
27
 
22
28
  Start with a prompt:
@@ -1,6 +1,7 @@
1
1
  import { APP_NAME } from "../config.js";
2
2
  export const BUILTIN_SLASH_COMMANDS = [
3
3
  { name: "settings", description: "Open settings menu" },
4
+ { name: "settings-factory-reset", description: "Reset settings to factory defaults (with confirmation)" },
4
5
  { name: "model", description: "Select model (opens selector UI)", argumentHint: "<provider/model>" },
5
6
  { name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
6
7
  { name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
@@ -15,13 +15,8 @@
15
15
  "name": "DeepSeek V4 Flash",
16
16
  "reasoning": true,
17
17
  "input": ["text"],
18
- "contextWindow": 1000000,
19
- "cost": {
20
- "input": 0.15,
21
- "output": 0.3,
22
- "cacheRead": 0.003,
23
- "cacheWrite": 0
24
- },
18
+ "contextWindow": 512000,
19
+ "maxTokens": 64000,
25
20
  "thinkingLevelMap": {
26
21
  "minimal": null,
27
22
  "low": null,
@@ -37,50 +32,40 @@
37
32
  }
38
33
  },
39
34
  {
40
- "id": "kimi-k3",
41
- "name": "Kimi K3",
35
+ "id": "deepseek-v4-pro",
36
+ "name": "DeepSeek V4 pro",
42
37
  "reasoning": true,
43
- "input": ["text", "image"],
44
- "contextWindow": 262128,
45
- "cost": {
46
- "input": 3,
47
- "output": 15,
48
- "cacheRead": 1.5,
49
- "cacheWrite": 0
38
+ "input": ["text"],
39
+ "contextWindow": 512000,
40
+ "maxTokens": 64000,
41
+ "thinkingLevelMap": {
42
+ "minimal": null,
43
+ "low": null,
44
+ "medium": null,
45
+ "high": null,
46
+ "xhigh": null,
47
+ "max": "max"
50
48
  },
51
49
  "compat": {
52
50
  "supportsDeveloperRole": false,
53
- "supportsReasoningEffort": false
51
+ "requiresReasoningContentOnAssistantMessages": true,
52
+ "thinkingFormat": "deepseek"
54
53
  }
55
54
  },
56
55
  {
57
- "id": "kimi-k2.7-code",
58
- "name": "Kimi K2.7 Code",
56
+ "id": "kimi-k3",
57
+ "name": "Kimi K3",
59
58
  "reasoning": true,
60
59
  "input": ["text", "image"],
61
- "contextWindow": 262128,
62
- "cost": {
63
- "input": 0.95,
64
- "output": 4,
65
- "cacheRead": 0.2375,
66
- "cacheWrite": 0
67
- },
68
- "compat": {
69
- "supportsDeveloperRole": false,
70
- "supportsReasoningEffort": false
71
- }
72
- },
73
- {
74
- "id": "qwen3.5-397b",
75
- "name": "Qwen3.5 397B",
76
- "reasoning": true,
77
- "input": ["text"],
78
- "contextWindow": 262128,
79
- "cost": {
80
- "input": 0.69,
81
- "output": 4.14,
82
- "cacheRead": 0.1725,
83
- "cacheWrite": 0
60
+ "contextWindow": 512000,
61
+ "maxTokens": 64000,
62
+ "thinkingLevelMap": {
63
+ "minimal": null,
64
+ "low": "low",
65
+ "medium": null,
66
+ "high": "high",
67
+ "xhigh": null,
68
+ "max": "max"
84
69
  },
85
70
  "compat": {
86
71
  "supportsDeveloperRole": false,
@@ -88,16 +73,19 @@
88
73
  }
89
74
  },
90
75
  {
91
- "id": "qwen3.6-35b",
92
- "name": "Qwen3.6 35B",
76
+ "id": "kimi-k2.7-code",
77
+ "name": "Kimi K2.7 Code",
93
78
  "reasoning": true,
94
79
  "input": ["text", "image"],
95
- "contextWindow": 131056,
96
- "cost": {
97
- "input": 0.29,
98
- "output": 1.15,
99
- "cacheRead": 0.0725,
100
- "cacheWrite": 0
80
+ "contextWindow": 262128,
81
+ "maxTokens": 64000,
82
+ "thinkingLevelMap": {
83
+ "minimal": null,
84
+ "low": "low",
85
+ "medium": null,
86
+ "high": "high",
87
+ "xhigh": null,
88
+ "max": "max"
101
89
  },
102
90
  "compat": {
103
91
  "supportsDeveloperRole": false,
@@ -110,11 +98,14 @@
110
98
  "reasoning": true,
111
99
  "input": ["text"],
112
100
  "contextWindow": 1000000,
113
- "cost": {
114
- "input": 1.45,
115
- "output": 4.5,
116
- "cacheRead": 0.3625,
117
- "cacheWrite": 0
101
+ "maxTokens": 64000,
102
+ "thinkingLevelMap": {
103
+ "minimal": null,
104
+ "low": "low",
105
+ "medium": null,
106
+ "high": "high",
107
+ "xhigh": null,
108
+ "max": null
118
109
  },
119
110
  "compat": {
120
111
  "supportsDeveloperRole": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "autoHandoff": {
3
- "enabled": false,
4
- "thresholdTokens": 128000
3
+ "enabled": true,
4
+ "thresholdTokens": 256000
5
5
  },
6
6
  "collapseChangelog": true,
7
7
  "compaction": {
@@ -9,19 +9,18 @@
9
9
  "keepRecentTokens": 32000,
10
10
  "reserveTokens": 64000
11
11
  },
12
- "defaultModel": "glm-5.2",
12
+ "defaultModel": "deepseek-v4-flash",
13
13
  "defaultProvider": "tokenin",
14
- "defaultThinkingLevel": "high",
14
+ "defaultThinkingLevel": "max",
15
15
  "doubleEscapeAction": "tree",
16
16
  "editorPaddingX": 0,
17
17
  "outputPad": 0,
18
- "showCacheMissNotices": true,
18
+ "showCacheMissNotices": false,
19
19
  "followUpMode": "one-at-a-time",
20
20
  "hideThinkingBlock": false,
21
21
  "images": {
22
22
  "autoResize": true
23
23
  },
24
- "lastChangelogVersion": "0.80.2",
25
24
  "packages": [
26
25
  "git:github.com/MasuRii/pi-tool-display",
27
26
  "git:github.com/omaclaren/pi-markdown-preview"
@@ -29,7 +28,7 @@
29
28
  "quietStartup": true,
30
29
  "retry": {
31
30
  "enabled": true,
32
- "maxRetries": 3
31
+ "maxRetries": 7
33
32
  },
34
33
  "steeringMode": "all",
35
34
  "terminal": {
@@ -39,7 +38,7 @@
39
38
  "subagents": {
40
39
  "agentOverrides": {
41
40
  "architect": {
42
- "model": "tokenin/kimi-k3:high"
41
+ "model": "tokenin/deepseek-v4-pro:max"
43
42
  },
44
43
  "builder": {
45
44
  "model": "tokenin/deepseek-v4-flash:max"
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
2
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import agentBrowserSetupExtension, {
6
6
  DECLINED_MARKER_NAME,
7
7
  ensureAgentBrowser,
@@ -192,4 +192,180 @@ describe("agent-browser setup extension", () => {
192
192
  it("declined marker path lives in the agent dir", () => {
193
193
  expect(getDeclinedMarkerPath()).toBe(join(tmpDir, DECLINED_MARKER_NAME));
194
194
  });
195
+
196
+ it("skips probing when the decline marker exists", async () => {
197
+ const { pi, handlers, exec } = makePi();
198
+ await agentBrowserSetupExtension(pi);
199
+ const markerPath = getDeclinedMarkerPath();
200
+ mkdirSync(dirname(markerPath), { recursive: true });
201
+ writeFileSync(markerPath, "1", "utf-8");
202
+ try {
203
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, makeCtx());
204
+ expect(exec).not.toHaveBeenCalled();
205
+ } finally {
206
+ rmSync(markerPath, { force: true });
207
+ }
208
+ });
209
+
210
+ it("isAgentBrowserInstalled returns false when exec throws", async () => {
211
+ const { pi } = makePi(async () => {
212
+ throw new Error("spawn failed");
213
+ });
214
+ expect(await isAgentBrowserInstalled(pi)).toBe(false);
215
+ });
216
+
217
+ it("isAgentBrowserInstalled returns false when the probe is killed", async () => {
218
+ const { pi } = makePi(async () => ({ code: 0, stdout: "", stderr: "", killed: true }));
219
+ expect(await isAgentBrowserInstalled(pi)).toBe(false);
220
+ });
221
+
222
+ it("reports a timed-out npm install", async () => {
223
+ const execImpl = vi
224
+ .fn()
225
+ .mockResolvedValueOnce(fail) // probe: missing
226
+ .mockResolvedValueOnce({ code: 0, stdout: "", stderr: "", killed: true }); // npm killed
227
+ const { pi, handlers, exec } = makePi(execImpl as any);
228
+ await agentBrowserSetupExtension(pi);
229
+ const ctx = makeCtx();
230
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
231
+
232
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("timed out"), "error");
233
+ expect(exec).toHaveBeenCalledTimes(2);
234
+ });
235
+
236
+ it("reports npm success but missing binary on PATH", async () => {
237
+ const execImpl = vi
238
+ .fn()
239
+ .mockResolvedValueOnce(fail) // probe: missing
240
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
241
+ .mockResolvedValueOnce(fail) // probe: still missing
242
+ .mockResolvedValueOnce(ok); // (unused)
243
+ const { pi, handlers, exec } = makePi(execImpl as any);
244
+ await agentBrowserSetupExtension(pi);
245
+ const ctx = makeCtx();
246
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
247
+
248
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("still not on PATH"), "warning");
249
+ expect(exec).toHaveBeenCalledTimes(3);
250
+ });
251
+
252
+ it("warns when browser provisioning fails after install", async () => {
253
+ const execImpl = vi
254
+ .fn()
255
+ .mockResolvedValueOnce(fail) // probe: missing
256
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
257
+ .mockResolvedValueOnce(ok) // probe: now present
258
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "browser failed", killed: false }); // agent-browser install fails
259
+ const { pi, handlers, exec } = makePi(execImpl as any);
260
+ await agentBrowserSetupExtension(pi);
261
+ const ctx = makeCtx();
262
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
263
+
264
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Run `agent-browser install`"), "info");
265
+ expect(exec).toHaveBeenCalledTimes(4);
266
+ });
267
+
268
+ it("provisionBrowser returns false when exec throws", async () => {
269
+ const execImpl = vi
270
+ .fn()
271
+ .mockResolvedValueOnce(fail) // probe: missing
272
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
273
+ .mockResolvedValueOnce(ok) // probe: now present
274
+ .mockRejectedValueOnce(new Error("spawn failed")); // agent-browser install throws
275
+ const { pi, handlers, exec } = makePi(execImpl as any);
276
+ await agentBrowserSetupExtension(pi);
277
+ const ctx = makeCtx();
278
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
279
+
280
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Run `agent-browser install`"), "info");
281
+ expect(exec).toHaveBeenCalledTimes(4);
282
+ });
283
+
284
+ it("ensureAgentBrowser returns false when the user declines", async () => {
285
+ const execImpl = vi.fn().mockResolvedValue(fail);
286
+ const { pi } = makePi(execImpl as any);
287
+ const ctx = makeCtx({ confirmResult: false });
288
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
289
+ });
290
+
291
+ it("ensureAgentBrowser returns true when already installed", async () => {
292
+ const { pi } = makePi(async () => ok);
293
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(true);
294
+ });
295
+
296
+ it("ensureAgentBrowser returns false without UI", async () => {
297
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
298
+ const execImpl = vi.fn().mockResolvedValue(fail);
299
+ const { pi } = makePi(execImpl as any);
300
+ expect(await ensureAgentBrowser(pi, makeCtx({ hasUI: false }))).toBe(false);
301
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(INSTALL_COMMAND));
302
+ });
303
+
304
+ it("ensureAgentBrowser returns false when npm install fails", async () => {
305
+ const execImpl = vi
306
+ .fn()
307
+ .mockResolvedValueOnce(fail) // probe
308
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "EACCES", killed: false }); // npm fails
309
+ const { pi } = makePi(execImpl as any);
310
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(false);
311
+ });
312
+
313
+ it("npm install failure surfaces stdout when stderr is empty", async () => {
314
+ const execImpl = vi
315
+ .fn()
316
+ .mockResolvedValueOnce(fail) // probe
317
+ .mockResolvedValueOnce({ code: 1, stdout: "npm error log", stderr: "", killed: false }); // npm fails with stdout only
318
+ const { pi } = makePi(execImpl as any);
319
+ const ctx = makeCtx();
320
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
321
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("npm error log"), "error");
322
+ });
323
+
324
+ it("npm install failure falls back to exit status when output is empty", async () => {
325
+ const execImpl = vi
326
+ .fn()
327
+ .mockResolvedValueOnce(fail) // probe
328
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "", killed: false }); // npm fails with no output
329
+ const { pi } = makePi(execImpl as any);
330
+ const ctx = makeCtx();
331
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
332
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("exit status 1"), "error");
333
+ });
334
+
335
+ it("notify is a no-op without UI even when the CLI is installed", async () => {
336
+ const { pi } = makePi(async () => ok);
337
+ const ctx = makeCtx({ hasUI: false });
338
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(true);
339
+ });
340
+
341
+ it("ensureAgentBrowser returns true after full install flow", async () => {
342
+ const execImpl = vi
343
+ .fn()
344
+ .mockResolvedValueOnce(fail) // probe: missing
345
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
346
+ .mockResolvedValueOnce(ok) // probe: now present
347
+ .mockResolvedValueOnce(ok); // agent-browser install
348
+ const { pi } = makePi(execImpl as any);
349
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(true);
350
+ });
351
+
352
+ it("cliCommand wraps args in cmd.exe on win32", () => {
353
+ const original = process.platform;
354
+ Object.defineProperty(process, "platform", { value: "win32" });
355
+ try {
356
+ const { ensureAgentBrowser: _e, ...rest } = {} as any;
357
+ // Re-import to pick up the platform at module scope is not needed;
358
+ // cliCommand is module-private, so exercise it through the public flow.
359
+ const execImpl = vi.fn().mockResolvedValue(ok);
360
+ const { pi } = makePi(execImpl as any);
361
+ return ensureAgentBrowser(pi, makeCtx()).then((result) => {
362
+ expect(result).toBe(true);
363
+ const call = execImpl.mock.calls[0];
364
+ expect(call[0]).toBe("cmd");
365
+ expect(call[1]).toEqual(["/c", "agent-browser", "--version"]);
366
+ });
367
+ } finally {
368
+ Object.defineProperty(process, "platform", { value: original });
369
+ }
370
+ });
195
371
  });
@@ -75,6 +75,8 @@ async function runNpmInstall(pi: ExtensionAPI): Promise<string | null> {
75
75
  const result = await pi.exec(command, args, { timeout: INSTALL_TIMEOUT_MS });
76
76
  if (result.killed) return "npm install timed out";
77
77
  if (result.code !== 0) {
78
+ // The exit-status fallback is always non-empty, so the final || is unreachable.
79
+ /* v8 ignore next 1 */
78
80
  return (result.stderr || result.stdout || `exit status ${result.code}`).trim() || "npm install failed";
79
81
  }
80
82
  return null;