@raidou/pi-pm-subagents 0.1.1

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 (69) hide show
  1. package/.prettierrc +7 -0
  2. package/AGENTS.md +1 -0
  3. package/README.md +85 -0
  4. package/README.zh-CN.md +85 -0
  5. package/agents/explorer.md +10 -0
  6. package/agents/planner.md +16 -0
  7. package/agents/researcher.md +22 -0
  8. package/agents/reviewer.md +10 -0
  9. package/eslint.config.mjs +14 -0
  10. package/example-prompts/coordinator.md +21 -0
  11. package/package.json +48 -0
  12. package/pm-subagents-prompts/coordinator.md +16 -0
  13. package/pnpm-workspace.yaml +5 -0
  14. package/src/bash-readonly.test.ts +331 -0
  15. package/src/bash-readonly.ts +205 -0
  16. package/src/coordinator/coordinator.test.ts +28 -0
  17. package/src/coordinator/coordinator.ts +275 -0
  18. package/src/custom-select.test.ts +91 -0
  19. package/src/custom-select.ts +209 -0
  20. package/src/index.ts +87 -0
  21. package/src/models-config/models-config.test.ts +88 -0
  22. package/src/models-config/models-config.ts +205 -0
  23. package/src/models-config/scoped-models-editor.test.ts +189 -0
  24. package/src/models-config/scoped-models-editor.ts +412 -0
  25. package/src/models-config/subagent-model-constants.ts +2 -0
  26. package/src/models-config/subagent-model-cycle.ts +53 -0
  27. package/src/models-config/subagent-model-utils.test.ts +250 -0
  28. package/src/models-config/subagent-model-utils.ts +52 -0
  29. package/src/pm-mode.test.ts +324 -0
  30. package/src/pm-mode.ts +142 -0
  31. package/src/prompts/mode.test.ts +289 -0
  32. package/src/prompts/mode.ts +31 -0
  33. package/src/prompts/roles.test.ts +724 -0
  34. package/src/prompts/roles.ts +119 -0
  35. package/src/subagent/activity.test.ts +230 -0
  36. package/src/subagent/activity.ts +60 -0
  37. package/src/subagent/batcher.test.ts +198 -0
  38. package/src/subagent/batcher.ts +51 -0
  39. package/src/subagent/consts.ts +1 -0
  40. package/src/subagent/demo.ts +773 -0
  41. package/src/subagent/fleet.test.ts +1758 -0
  42. package/src/subagent/fleet.ts +376 -0
  43. package/src/subagent/identity.test.ts +31 -0
  44. package/src/subagent/identity.ts +16 -0
  45. package/src/subagent/manager.test.ts +392 -0
  46. package/src/subagent/manager.ts +277 -0
  47. package/src/subagent/tools.ts +314 -0
  48. package/src/subagent/viewer.ts +305 -0
  49. package/src/types.ts +15 -0
  50. package/src/ui/border-view.ts +50 -0
  51. package/src/ui/review-pager.ts +146 -0
  52. package/src/ui/scroll-view.test.ts +190 -0
  53. package/src/ui/scroll-view.ts +155 -0
  54. package/src/utils/format.test.ts +76 -0
  55. package/src/utils/format.ts +67 -0
  56. package/src/utils/fs.ts +9 -0
  57. package/src/utils/markdown.test.ts +442 -0
  58. package/src/utils/markdown.ts +79 -0
  59. package/src/utils/messages.test.ts +436 -0
  60. package/src/utils/messages.ts +131 -0
  61. package/src/utils/model-ref.test.ts +42 -0
  62. package/src/utils/model-ref.ts +44 -0
  63. package/src/utils/state.test.ts +98 -0
  64. package/src/utils/state.ts +45 -0
  65. package/src/utils/tools.ts +48 -0
  66. package/src/utils/truncate.test.ts +41 -0
  67. package/src/utils/truncate.ts +59 -0
  68. package/tsconfig.json +24 -0
  69. package/vitest.config.ts +8 -0
@@ -0,0 +1,209 @@
1
+ import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent'
2
+ import { DynamicBorder } from '@earendil-works/pi-coding-agent'
3
+ import type { Focusable, TUI } from '@earendil-works/pi-tui'
4
+ import {
5
+ Container,
6
+ fuzzyFilter,
7
+ Input,
8
+ Key,
9
+ matchesKey,
10
+ Spacer,
11
+ Text,
12
+ } from '@earendil-works/pi-tui'
13
+
14
+ export interface CustomSelectItem {
15
+ readonly key: string
16
+ readonly text: string
17
+ }
18
+
19
+ export interface CustomSelectOptions {
20
+ readonly items: readonly CustomSelectItem[]
21
+ readonly title?: string
22
+ readonly placeholder?: string
23
+ readonly maxVisible?: number
24
+ }
25
+
26
+ /** Open a searchable, height-limited selector over `{ key, text }` items. */
27
+ export async function customSelect(
28
+ ctx: ExtensionContext,
29
+ options: CustomSelectOptions,
30
+ ): Promise<string | undefined> {
31
+ return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
32
+ return new CustomSelectComponent(
33
+ tui,
34
+ theme,
35
+ options,
36
+ (key) => {
37
+ done(key)
38
+ },
39
+ () => {
40
+ done(undefined)
41
+ },
42
+ )
43
+ })
44
+ }
45
+
46
+ /**
47
+ * Searchable, height-limited selector over a list of `{ key, text }` items.
48
+ * Returns the picked item's `key` (or undefined on cancel).
49
+ */
50
+ export class CustomSelectComponent extends Container implements Focusable {
51
+ readonly #tui: TUI
52
+ readonly #theme: Theme
53
+ readonly #items: readonly CustomSelectItem[]
54
+ readonly #placeholder: string
55
+ readonly #maxVisible: number
56
+ readonly #searchInput: Input
57
+ readonly #listContainer: Container
58
+ readonly #onSelect: (key: string) => void
59
+ readonly #onCancel: () => void
60
+ #focused = false
61
+ #filtered: CustomSelectItem[]
62
+ #selectedIndex = 0
63
+
64
+ constructor(
65
+ tui: TUI,
66
+ theme: Theme,
67
+ options: CustomSelectOptions,
68
+ onSelect: (key: string) => void,
69
+ onCancel: () => void,
70
+ ) {
71
+ super()
72
+ this.#tui = tui
73
+ this.#theme = theme
74
+ this.#items = options.items
75
+ this.#placeholder = options.placeholder ?? 'filter'
76
+ this.#maxVisible = options.maxVisible ?? 10
77
+ this.#onSelect = onSelect
78
+ this.#onCancel = onCancel
79
+ this.#filtered = [...this.#items]
80
+
81
+ const borderColor = (s: string) => theme.fg('borderMuted', s)
82
+
83
+ this.addChild(new DynamicBorder(borderColor))
84
+ this.addChild(new Spacer(1))
85
+ this.addChild(
86
+ new Text(theme.fg('accent', theme.bold(options.title ?? 'Select')), 1, 0),
87
+ )
88
+ this.addChild(new Spacer(1))
89
+ this.addChild(new Text(theme.fg('muted', this.#placeholder), 1, 0))
90
+
91
+ this.#searchInput = new Input()
92
+ this.#searchInput.onSubmit = () => {
93
+ this.#confirm()
94
+ }
95
+ this.addChild(this.#searchInput)
96
+ this.addChild(new Spacer(1))
97
+
98
+ this.#listContainer = new Container()
99
+ this.addChild(this.#listContainer)
100
+ this.addChild(new Spacer(1))
101
+ this.addChild(new DynamicBorder(borderColor))
102
+
103
+ this.#renderList()
104
+ }
105
+
106
+ get focused(): boolean {
107
+ return this.#focused
108
+ }
109
+
110
+ set focused(value: boolean) {
111
+ this.#focused = value
112
+ this.#searchInput.focused = value
113
+ }
114
+
115
+ handleInput(keyData: string): void {
116
+ if (matchesKey(keyData, Key.up) || matchesKey(keyData, Key.ctrl('p'))) {
117
+ this.#move(-1)
118
+ } else if (
119
+ matchesKey(keyData, Key.down) ||
120
+ matchesKey(keyData, Key.ctrl('n'))
121
+ ) {
122
+ this.#move(1)
123
+ } else if (
124
+ matchesKey(keyData, Key.pageUp) ||
125
+ matchesKey(keyData, Key.ctrl('u'))
126
+ ) {
127
+ this.#move(-this.#maxVisible)
128
+ } else if (
129
+ matchesKey(keyData, Key.pageDown) ||
130
+ matchesKey(keyData, Key.ctrl('d'))
131
+ ) {
132
+ this.#move(this.#maxVisible)
133
+ } else if (matchesKey(keyData, Key.enter)) {
134
+ this.#confirm()
135
+ } else if (
136
+ matchesKey(keyData, Key.escape) ||
137
+ matchesKey(keyData, Key.ctrl('c'))
138
+ ) {
139
+ this.#onCancel()
140
+ } else {
141
+ this.#searchInput.handleInput(keyData)
142
+ this.#refilter(this.#searchInput.getValue())
143
+ }
144
+ }
145
+
146
+ #move(delta: number): void {
147
+ if (this.#filtered.length === 0) return
148
+ const last = this.#filtered.length - 1
149
+ this.#selectedIndex =
150
+ delta > 0
151
+ ? Math.min(last, this.#selectedIndex + delta)
152
+ : Math.max(0, this.#selectedIndex + delta)
153
+ this.#renderList()
154
+ this.#tui.requestRender()
155
+ }
156
+
157
+ #confirm(): void {
158
+ const picked = this.#filtered[this.#selectedIndex]
159
+ if (picked) this.#onSelect(picked.key)
160
+ }
161
+
162
+ #refilter(query: string): void {
163
+ const needle = query.trim().toLowerCase()
164
+ const source: CustomSelectItem[] = [...this.#items]
165
+ this.#filtered = needle
166
+ ? fuzzyFilter(source, needle, (item) => item.text)
167
+ : source
168
+ this.#selectedIndex = 0
169
+ this.#renderList()
170
+ this.#tui.requestRender()
171
+ }
172
+
173
+ #renderList(): void {
174
+ this.#listContainer.clear()
175
+ const total = this.#filtered.length
176
+ const visible = Math.min(this.#maxVisible, total)
177
+ const start = Math.max(
178
+ 0,
179
+ Math.min(
180
+ this.#selectedIndex - Math.floor(this.#maxVisible / 2),
181
+ total - visible,
182
+ ),
183
+ )
184
+ const end = start + visible
185
+ for (let i = start; i < end; i++) {
186
+ const item = this.#filtered[i]
187
+ if (!item) continue
188
+ const prefix = i === this.#selectedIndex ? '→ ' : ' '
189
+ const text =
190
+ i === this.#selectedIndex
191
+ ? this.#theme.fg('accent', item.text)
192
+ : item.text
193
+ this.#listContainer.addChild(new Text(`${prefix}${text}`, 1, 0))
194
+ }
195
+ if (total > visible) {
196
+ this.#listContainer.addChild(
197
+ new Text(
198
+ this.#theme.fg('muted', ` (${this.#selectedIndex + 1}/${total})`),
199
+ 1,
200
+ 0,
201
+ ),
202
+ )
203
+ } else if (total === 0) {
204
+ this.#listContainer.addChild(
205
+ new Text(this.#theme.fg('muted', ' No matches'), 1, 0),
206
+ )
207
+ }
208
+ }
209
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
2
+
3
+ import { setupBashReadonlyTool } from './bash-readonly.js'
4
+ import {
5
+ applyCoordinatorMode,
6
+ enterCoordinatorMode,
7
+ setupCoordinator,
8
+ } from './coordinator/coordinator.js'
9
+ import {
10
+ getPmSubagentsConfig,
11
+ loadPmSubagentsConfig,
12
+ setupPmSubagentsConfig,
13
+ } from './models-config/models-config.js'
14
+ import { setupSubagentModelCycle } from './models-config/subagent-model-cycle.js'
15
+ import { readModePrompt } from './prompts/mode.js'
16
+ import { isSubagentSpawnContext } from './subagent/identity.js'
17
+ import type { PmSubagentState } from './types.js'
18
+ import { createState, getLastPmSubagentState } from './utils/state.js'
19
+
20
+ let pendingPmSubagentState: Partial<PmSubagentState> | undefined
21
+
22
+ export default async function pmSubagentsExtension(
23
+ pi: ExtensionAPI,
24
+ ): Promise<void> {
25
+ setupBashReadonlyTool(pi)
26
+ if (isSubagentSpawnContext()) {
27
+ return
28
+ }
29
+
30
+ const state = createState()
31
+ await loadPmSubagentsConfig()
32
+
33
+ const coordinatorDefinition = await readModePrompt('coordinator')
34
+
35
+ const demoEnabled = process.env.PI_DEMO === '1'
36
+
37
+ await setupCoordinator(pi, state, {
38
+ demoEnabled,
39
+ coordinatorDefinition,
40
+ })
41
+ setupPmSubagentsConfig(pi, state, coordinatorDefinition)
42
+ setupSubagentModelCycle(pi, state)
43
+
44
+ pi.on('session_before_switch', (_event, ctx) => {
45
+ pendingPmSubagentState = getLastPmSubagentState(
46
+ ctx.sessionManager.getEntries(),
47
+ )
48
+ })
49
+
50
+ pi.on('session_before_fork', (_event, ctx) => {
51
+ pendingPmSubagentState = getLastPmSubagentState(
52
+ ctx.sessionManager.getEntries(),
53
+ )
54
+ })
55
+
56
+ pi.on('session_start', async (_event, ctx) => {
57
+ let data = getLastPmSubagentState(ctx.sessionManager.getEntries())
58
+
59
+ if (!data && pendingPmSubagentState) {
60
+ data = pendingPmSubagentState
61
+ pendingPmSubagentState = undefined
62
+ }
63
+
64
+ if (data) {
65
+ state.mode = data.mode
66
+ state.modeDiffTools = data.modeDiffTools
67
+ state.previousModel = data.previousModel
68
+ state.sessionSubagentModel = data.sessionSubagentModel
69
+
70
+ if (state.mode === 'coordinator') {
71
+ await applyCoordinatorMode(pi, state, ctx, coordinatorDefinition)
72
+ }
73
+
74
+ return
75
+ }
76
+
77
+ if (getPmSubagentsConfig().defaultMode === 'coordinator') {
78
+ await enterCoordinatorMode(
79
+ pi,
80
+ state,
81
+ undefined,
82
+ ctx,
83
+ coordinatorDefinition,
84
+ )
85
+ }
86
+ })
87
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { PmSubagentsConfig } from './models-config.js'
4
+ import { sanitizeConfig } from './models-config.js'
5
+ import { MODEL_DEFAULT } from './subagent-model-constants.js'
6
+
7
+ describe('PmSubagentsConfig schema', () => {
8
+ it('has subagentModelScope field', () => {
9
+ const config: PmSubagentsConfig = {
10
+ subagentModelScoped: [MODEL_DEFAULT, 'provider/model'],
11
+ }
12
+ expect(config.subagentModelScoped).toBeDefined()
13
+ expect(config.subagentModelScoped?.length).toBe(2)
14
+ })
15
+
16
+ it('has subagentModel field', () => {
17
+ const config: PmSubagentsConfig = {
18
+ subagentModel: 'anthropic/claude-sonnet-4-5',
19
+ }
20
+ expect(config.subagentModel).toBe('anthropic/claude-sonnet-4-5')
21
+ })
22
+ })
23
+
24
+ describe('sanitizeConfig', () => {
25
+ it('keeps defaultMode when it is coordinator', () => {
26
+ expect(sanitizeConfig({ defaultMode: 'coordinator' })).toEqual({
27
+ defaultMode: 'coordinator',
28
+ })
29
+ })
30
+
31
+ it('clears invalid defaultMode', () => {
32
+ expect(sanitizeConfig({ defaultMode: 'bogus' })).toEqual({})
33
+ })
34
+
35
+ it('keeps valid subagentModel and scope', () => {
36
+ const config = sanitizeConfig({
37
+ subagentModel: 'provider/model',
38
+ subagentModelScoped: ['provider/model', MODEL_DEFAULT],
39
+ })
40
+ expect(config.subagentModel).toBe('provider/model')
41
+ expect(config.subagentModelScoped).toEqual([
42
+ 'provider/model',
43
+ MODEL_DEFAULT,
44
+ ])
45
+ })
46
+
47
+ it('clears invalid subagentModel and empty scope', () => {
48
+ const config = sanitizeConfig({
49
+ subagentModel: 'no-slash',
50
+ subagentModelScoped: ['also-bad'],
51
+ })
52
+ expect(config.subagentModel).toBeUndefined()
53
+ expect(config.subagentModelScoped).toBeUndefined()
54
+ })
55
+
56
+ it('keeps valid skipPluginAgents', () => {
57
+ expect(sanitizeConfig({ skipPluginAgents: true })).toEqual({
58
+ skipPluginAgents: true,
59
+ })
60
+ })
61
+
62
+ it('clears invalid skipPluginAgents', () => {
63
+ expect(
64
+ sanitizeConfig({ skipPluginAgents: 'yes' as unknown as boolean }),
65
+ ).toEqual({})
66
+ })
67
+
68
+ it('clears invalid defaultMode without affecting a valid subagentModel', () => {
69
+ expect(
70
+ sanitizeConfig({
71
+ subagentModel: 'provider/model',
72
+ defaultMode: 'bogus',
73
+ }),
74
+ ).toEqual({ subagentModel: 'provider/model' })
75
+ })
76
+
77
+ it('does not mutate the input record', () => {
78
+ const record: PmSubagentsConfig = {
79
+ subagentModel: 'no-slash',
80
+ defaultMode: 'bogus',
81
+ }
82
+ sanitizeConfig(record)
83
+ expect(record).toEqual({
84
+ subagentModel: 'no-slash',
85
+ defaultMode: 'bogus',
86
+ })
87
+ })
88
+ })
@@ -0,0 +1,205 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import type {
5
+ ExtensionAPI,
6
+ ExtensionContext,
7
+ } from '@earendil-works/pi-coding-agent'
8
+ import { getAgentDir } from '@earendil-works/pi-coding-agent'
9
+ import type { Static } from 'typebox'
10
+ import { Type } from 'typebox'
11
+ import { Parse } from 'typebox/value'
12
+
13
+ import {
14
+ enterCoordinatorMode,
15
+ renderCoordinatorModeWidget,
16
+ } from '../coordinator/coordinator.js'
17
+ import { customSelect } from '../custom-select.js'
18
+ import type { PmSubagentState } from '../types.js'
19
+ import type { PromptDefinition } from '../utils/markdown.js'
20
+ import {
21
+ modelOptionOf,
22
+ parseModelRef,
23
+ resolveModelRef,
24
+ } from '../utils/model-ref.js'
25
+ import { scopedModelsEditor } from './scoped-models-editor.js'
26
+ import {
27
+ MODEL_DEFAULT,
28
+ MODEL_DEFAULT_LABEL,
29
+ } from './subagent-model-constants.js'
30
+
31
+ const PmSubagentsConfigSchema = Type.Object({
32
+ subagentModel: Type.Optional(Type.String()),
33
+ subagentModelScoped: Type.Optional(Type.Array(Type.String())),
34
+ defaultMode: Type.Optional(Type.String()),
35
+ skipPluginAgents: Type.Optional(Type.Boolean()),
36
+ })
37
+
38
+ export function sanitizeConfig(record: PmSubagentsConfig): PmSubagentsConfig {
39
+ const model = record.subagentModel
40
+ const subagentModel = model && !parseModelRef(model) ? undefined : model
41
+
42
+ let subagentModelScoped = record.subagentModelScoped
43
+ if (subagentModelScoped) {
44
+ const filtered = subagentModelScoped.filter(
45
+ (ref) => ref === MODEL_DEFAULT || parseModelRef(ref) !== undefined,
46
+ )
47
+ subagentModelScoped = filtered.length > 0 ? filtered : undefined
48
+ }
49
+
50
+ const defaultMode =
51
+ record.defaultMode === 'coordinator' ? 'coordinator' : undefined
52
+
53
+ const skipPluginAgents =
54
+ typeof record.skipPluginAgents === 'boolean'
55
+ ? record.skipPluginAgents
56
+ : undefined
57
+
58
+ return {
59
+ subagentModel,
60
+ subagentModelScoped,
61
+ defaultMode,
62
+ skipPluginAgents,
63
+ }
64
+ }
65
+
66
+ export type PmSubagentsConfig = Static<typeof PmSubagentsConfigSchema>
67
+
68
+ let pmSubagentsConfig: PmSubagentsConfig = {}
69
+
70
+ function configPath(): string {
71
+ return join(getAgentDir(), 'pi-pm-subagents.json')
72
+ }
73
+
74
+ export async function loadPmSubagentsConfig(): Promise<void> {
75
+ try {
76
+ const raw: unknown = JSON.parse(await readFile(configPath(), 'utf8'))
77
+ pmSubagentsConfig = sanitizeConfig(Parse(PmSubagentsConfigSchema, raw))
78
+ } catch {
79
+ pmSubagentsConfig = {}
80
+ }
81
+ }
82
+
83
+ export function getPmSubagentsConfig(): PmSubagentsConfig {
84
+ return pmSubagentsConfig
85
+ }
86
+
87
+ export async function setSubagentModelScoped(scope: string[]): Promise<void> {
88
+ pmSubagentsConfig.subagentModelScoped = scope
89
+ await savePmSubagentsConfig()
90
+ }
91
+
92
+ export async function setSubagentModel(
93
+ model: string | undefined,
94
+ ): Promise<void> {
95
+ pmSubagentsConfig.subagentModel = model
96
+ await savePmSubagentsConfig()
97
+ }
98
+
99
+ export async function setDefaultMode(
100
+ mode: 'coordinator' | undefined,
101
+ ): Promise<void> {
102
+ pmSubagentsConfig.defaultMode = mode
103
+ await savePmSubagentsConfig()
104
+ }
105
+
106
+ async function savePmSubagentsConfig(): Promise<void> {
107
+ await mkdir(dirname(configPath()), { recursive: true })
108
+ await writeFile(
109
+ configPath(),
110
+ `${JSON.stringify(pmSubagentsConfig, null, 2)}\n`,
111
+ 'utf8',
112
+ )
113
+ }
114
+
115
+ async function pickModel(ctx: ExtensionContext): Promise<string | undefined> {
116
+ const items: Array<{ key: string; text: string }> = [
117
+ { key: MODEL_DEFAULT, text: MODEL_DEFAULT_LABEL },
118
+ ...ctx.modelRegistry.getAvailable().map(modelOptionOf),
119
+ ]
120
+ return customSelect(ctx, {
121
+ items,
122
+ title: 'Choose model',
123
+ placeholder: 'filter (provider/id substring)',
124
+ })
125
+ }
126
+
127
+ export function setupPmSubagentsConfig(
128
+ pi: ExtensionAPI,
129
+ state: PmSubagentState,
130
+ coordinatorDefinition: PromptDefinition,
131
+ ): void {
132
+ pi.registerCommand('pm-subagent-model', {
133
+ description: 'Configure the subagent model',
134
+ handler: async (_args, ctx) => {
135
+ const model = await pickModel(ctx)
136
+ if (!model) {
137
+ ctx.ui.notify('No model selected', 'warning')
138
+ return
139
+ }
140
+
141
+ const isDefaultModel = model === MODEL_DEFAULT
142
+ if (!isDefaultModel && !resolveModelRef(ctx, [model])) {
143
+ ctx.ui.notify(`Unknown model "${model}"`, 'warning')
144
+ return
145
+ }
146
+
147
+ await setSubagentModel(isDefaultModel ? undefined : model)
148
+ state.sessionSubagentModel = isDefaultModel ? undefined : model
149
+
150
+ const current = pmSubagentsConfig.subagentModel ?? MODEL_DEFAULT
151
+ ctx.ui.notify(`Subagent model: ${current}`, 'info')
152
+ if (state.mode === 'coordinator') renderCoordinatorModeWidget(ctx, state)
153
+ },
154
+ })
155
+
156
+ pi.registerCommand('pm-subagent-scoped', {
157
+ description: 'Manage subagent model scope (cycle pool)',
158
+ handler: async (_args, ctx) => {
159
+ const currentDefault = pmSubagentsConfig.subagentModel
160
+ const currentScope =
161
+ pmSubagentsConfig.subagentModelScoped ??
162
+ (currentDefault ? [currentDefault] : [])
163
+ const result = await scopedModelsEditor(ctx, {
164
+ items: [
165
+ { key: MODEL_DEFAULT, text: MODEL_DEFAULT_LABEL, provider: '' },
166
+ ...ctx.modelRegistry.getAvailable().map(modelOptionOf),
167
+ ],
168
+ initialChecked: currentScope,
169
+ title: 'Subagent Model Scope',
170
+ })
171
+ if (result === undefined) {
172
+ ctx.ui.notify('Scope unchanged.', 'info')
173
+ return
174
+ }
175
+ await setSubagentModelScoped(result)
176
+ ctx.ui.notify(`Subagent scope saved (${result.length} items).`, 'info')
177
+ if (state.mode === 'coordinator') renderCoordinatorModeWidget(ctx, state)
178
+ },
179
+ })
180
+
181
+ const runPmDefaultCommand = async (_args: string, ctx: ExtensionContext) => {
182
+ const enabled = pmSubagentsConfig.defaultMode !== 'coordinator'
183
+ await setDefaultMode(enabled ? 'coordinator' : undefined)
184
+ ctx.ui.notify(`pm mode on startup: ${enabled ? 'on' : 'off'}`, 'info')
185
+ if (enabled && state.mode !== 'coordinator') {
186
+ await enterCoordinatorMode(
187
+ pi,
188
+ state,
189
+ undefined,
190
+ ctx,
191
+ coordinatorDefinition,
192
+ )
193
+ }
194
+ }
195
+
196
+ pi.registerCommand('coordinator-default', {
197
+ description: 'Toggle pm (coordinator) mode enabled by default on startup',
198
+ handler: runPmDefaultCommand,
199
+ })
200
+
201
+ pi.registerCommand('pm-default', {
202
+ description: 'Toggle pm (coordinator) mode enabled by default on startup',
203
+ handler: runPmDefaultCommand,
204
+ })
205
+ }