@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.
- package/.prettierrc +7 -0
- package/AGENTS.md +1 -0
- package/README.md +85 -0
- package/README.zh-CN.md +85 -0
- package/agents/explorer.md +10 -0
- package/agents/planner.md +16 -0
- package/agents/researcher.md +22 -0
- package/agents/reviewer.md +10 -0
- package/eslint.config.mjs +14 -0
- package/example-prompts/coordinator.md +21 -0
- package/package.json +48 -0
- package/pm-subagents-prompts/coordinator.md +16 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/bash-readonly.test.ts +331 -0
- package/src/bash-readonly.ts +205 -0
- package/src/coordinator/coordinator.test.ts +28 -0
- package/src/coordinator/coordinator.ts +275 -0
- package/src/custom-select.test.ts +91 -0
- package/src/custom-select.ts +209 -0
- package/src/index.ts +87 -0
- package/src/models-config/models-config.test.ts +88 -0
- package/src/models-config/models-config.ts +205 -0
- package/src/models-config/scoped-models-editor.test.ts +189 -0
- package/src/models-config/scoped-models-editor.ts +412 -0
- package/src/models-config/subagent-model-constants.ts +2 -0
- package/src/models-config/subagent-model-cycle.ts +53 -0
- package/src/models-config/subagent-model-utils.test.ts +250 -0
- package/src/models-config/subagent-model-utils.ts +52 -0
- package/src/pm-mode.test.ts +324 -0
- package/src/pm-mode.ts +142 -0
- package/src/prompts/mode.test.ts +289 -0
- package/src/prompts/mode.ts +31 -0
- package/src/prompts/roles.test.ts +724 -0
- package/src/prompts/roles.ts +119 -0
- package/src/subagent/activity.test.ts +230 -0
- package/src/subagent/activity.ts +60 -0
- package/src/subagent/batcher.test.ts +198 -0
- package/src/subagent/batcher.ts +51 -0
- package/src/subagent/consts.ts +1 -0
- package/src/subagent/demo.ts +773 -0
- package/src/subagent/fleet.test.ts +1758 -0
- package/src/subagent/fleet.ts +376 -0
- package/src/subagent/identity.test.ts +31 -0
- package/src/subagent/identity.ts +16 -0
- package/src/subagent/manager.test.ts +392 -0
- package/src/subagent/manager.ts +277 -0
- package/src/subagent/tools.ts +314 -0
- package/src/subagent/viewer.ts +305 -0
- package/src/types.ts +15 -0
- package/src/ui/border-view.ts +50 -0
- package/src/ui/review-pager.ts +146 -0
- package/src/ui/scroll-view.test.ts +190 -0
- package/src/ui/scroll-view.ts +155 -0
- package/src/utils/format.test.ts +76 -0
- package/src/utils/format.ts +67 -0
- package/src/utils/fs.ts +9 -0
- package/src/utils/markdown.test.ts +442 -0
- package/src/utils/markdown.ts +79 -0
- package/src/utils/messages.test.ts +436 -0
- package/src/utils/messages.ts +131 -0
- package/src/utils/model-ref.test.ts +42 -0
- package/src/utils/model-ref.ts +44 -0
- package/src/utils/state.test.ts +98 -0
- package/src/utils/state.ts +45 -0
- package/src/utils/tools.ts +48 -0
- package/src/utils/truncate.test.ts +41 -0
- package/src/utils/truncate.ts +59 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { UserMessage } from '@earendil-works/pi-ai'
|
|
2
|
+
import type { SessionEntry } from '@earendil-works/pi-coding-agent'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { getLastPmSubagentState } from './state.js'
|
|
6
|
+
|
|
7
|
+
function user(text: string): UserMessage {
|
|
8
|
+
return {
|
|
9
|
+
role: 'user',
|
|
10
|
+
content: [{ type: 'text', text }],
|
|
11
|
+
timestamp: Date.now(),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('getLastPmSubagentState', () => {
|
|
16
|
+
it('returns undefined for empty entries array', () => {
|
|
17
|
+
const result = getLastPmSubagentState([])
|
|
18
|
+
expect(result).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns undefined when entries contain no custom entries', () => {
|
|
22
|
+
const entries: SessionEntry[] = [
|
|
23
|
+
{
|
|
24
|
+
type: 'message',
|
|
25
|
+
id: '1',
|
|
26
|
+
parentId: null,
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
message: user('task'),
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
const result = getLastPmSubagentState(entries)
|
|
32
|
+
expect(result).toBeUndefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns undefined when custom entries have different type', () => {
|
|
36
|
+
const entries: SessionEntry[] = [
|
|
37
|
+
{
|
|
38
|
+
type: 'custom',
|
|
39
|
+
id: '1',
|
|
40
|
+
parentId: null,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
customType: 'other-key',
|
|
43
|
+
data: {},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
const result = getLastPmSubagentState(entries)
|
|
47
|
+
expect(result).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns pm-subagents state from the last matching custom entry', () => {
|
|
51
|
+
const entries: SessionEntry[] = [
|
|
52
|
+
{
|
|
53
|
+
type: 'custom',
|
|
54
|
+
id: '1',
|
|
55
|
+
parentId: null,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
customType: 'pm-subagents',
|
|
58
|
+
data: {
|
|
59
|
+
mode: 'coordinator',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'custom',
|
|
64
|
+
id: '2',
|
|
65
|
+
parentId: null,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
customType: 'pm-subagents',
|
|
68
|
+
data: {
|
|
69
|
+
mode: 'coordinator',
|
|
70
|
+
modeDiffTools: { added: ['tool1'], removed: ['tool2'] },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
const result = getLastPmSubagentState(entries)
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
mode: 'coordinator',
|
|
77
|
+
modeDiffTools: { added: ['tool1'], removed: ['tool2'] },
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('sanitizes stale mode to undefined', () => {
|
|
82
|
+
const entries: SessionEntry[] = [
|
|
83
|
+
{
|
|
84
|
+
type: 'custom',
|
|
85
|
+
id: '1',
|
|
86
|
+
parentId: null,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
customType: 'pm-subagents',
|
|
89
|
+
data: {
|
|
90
|
+
mode: 'plan',
|
|
91
|
+
planMarkdown: 'stale plan',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
const result = getLastPmSubagentState(entries)
|
|
96
|
+
expect(result).toEqual({ mode: undefined })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
SessionEntry,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent'
|
|
5
|
+
|
|
6
|
+
import type { PmMode, PmSubagentState } from '../types.js'
|
|
7
|
+
|
|
8
|
+
export const PLUGIN_KEY = 'pm-subagents'
|
|
9
|
+
|
|
10
|
+
export function createState(): PmSubagentState {
|
|
11
|
+
return { mode: undefined }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function persist(pi: ExtensionAPI, state: PmSubagentState): void {
|
|
15
|
+
pi.appendEntry(PLUGIN_KEY, {
|
|
16
|
+
mode: state.mode,
|
|
17
|
+
modeDiffTools: state.modeDiffTools,
|
|
18
|
+
previousModel: state.previousModel,
|
|
19
|
+
sessionSubagentModel: state.sessionSubagentModel,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getLastPmSubagentState(
|
|
24
|
+
entries: readonly SessionEntry[],
|
|
25
|
+
): Partial<PmSubagentState> | undefined {
|
|
26
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
27
|
+
const entry = entries[i]
|
|
28
|
+
if (!entry || entry.type !== 'custom' || entry.customType !== PLUGIN_KEY)
|
|
29
|
+
continue
|
|
30
|
+
const data = entry.data as
|
|
31
|
+
(Partial<PmSubagentState> & { mode?: string }) | undefined
|
|
32
|
+
if (!data) return undefined
|
|
33
|
+
return {
|
|
34
|
+
mode: sanitizeMode(data.mode),
|
|
35
|
+
modeDiffTools: data.modeDiffTools,
|
|
36
|
+
previousModel: data.previousModel,
|
|
37
|
+
sessionSubagentModel: data.sessionSubagentModel,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sanitizeMode(mode: string | undefined): PmMode | undefined {
|
|
44
|
+
return mode === 'coordinator' ? 'coordinator' : undefined
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ToolDefinition,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent'
|
|
5
|
+
import type { TSchema } from 'typebox'
|
|
6
|
+
|
|
7
|
+
import type { PromptFrontmatter } from './markdown.js'
|
|
8
|
+
|
|
9
|
+
export type ToolConfig = Pick<
|
|
10
|
+
PromptFrontmatter,
|
|
11
|
+
'tools' | 'extraTools' | 'removeTools'
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
export function registerOptionalTools(
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
tools: ReadonlyArray<ToolDefinition<TSchema, unknown, unknown>>,
|
|
17
|
+
sessionStarted: boolean,
|
|
18
|
+
): void {
|
|
19
|
+
const names = new Set(tools.map((t) => t.name))
|
|
20
|
+
for (const tool of tools) {
|
|
21
|
+
pi.registerTool(tool)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const filterIt = () => {
|
|
25
|
+
const activeTools = pi.getActiveTools()
|
|
26
|
+
const filtered = activeTools.filter((name) => !names.has(name))
|
|
27
|
+
if (filtered.length !== activeTools.length) {
|
|
28
|
+
pi.setActiveTools(filtered)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sessionStarted) filterIt()
|
|
33
|
+
else
|
|
34
|
+
pi.on('session_start', async () => {
|
|
35
|
+
filterIt()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function composeTools(
|
|
40
|
+
baseTools: readonly string[],
|
|
41
|
+
config: ToolConfig,
|
|
42
|
+
): string[] {
|
|
43
|
+
const tools = config.tools ?? [...baseTools]
|
|
44
|
+
const extra = config.extraTools ?? []
|
|
45
|
+
const merged = [...new Set([...tools, ...extra])]
|
|
46
|
+
const toRemove = config.removeTools ?? []
|
|
47
|
+
return merged.filter((t) => !toRemove.includes(t))
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { truncateText } from './truncate.js'
|
|
4
|
+
|
|
5
|
+
describe('truncateText', () => {
|
|
6
|
+
it('returns plain ASCII unchanged when it fits', () => {
|
|
7
|
+
expect(truncateText('hello', 10)).toBe('hello')
|
|
8
|
+
expect(truncateText('hello', 5)).toBe('hello')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('truncates plain ASCII with ellipsis when too wide', () => {
|
|
12
|
+
expect(truncateText('hello world', 8)).toBe('hello...')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('preserves ANSI codes when truncating colored text', () => {
|
|
16
|
+
const colored = '\x1b[31mhello world\x1b[0m'
|
|
17
|
+
const result = truncateText(colored, 8)
|
|
18
|
+
expect(result).toBe('\x1b[31mhello\x1b[0m...')
|
|
19
|
+
expect(result).toContain('\x1b[31m')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('pads to exactly maxWidth when pad is true', () => {
|
|
23
|
+
expect(truncateText('hi', 5, '...', true)).toBe('hi ')
|
|
24
|
+
expect(truncateText('hello world', 8, '...', true)).toBe('hello...')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('counts emoji as width 2', () => {
|
|
28
|
+
expect(truncateText('👍👍', 4)).toBe('👍👍')
|
|
29
|
+
expect(truncateText('👍👍👍', 5)).toBe('👍...')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('pads empty text with spaces when pad is true', () => {
|
|
33
|
+
expect(truncateText('', 4, '...', true)).toBe(' ')
|
|
34
|
+
expect(truncateText('', 4)).toBe('')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns empty string when maxWidth <= 0', () => {
|
|
38
|
+
expect(truncateText('hello', 0)).toBe('')
|
|
39
|
+
expect(truncateText('hello', -1)).toBe('')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { visibleWidth } from '@earendil-works/pi-tui'
|
|
2
|
+
|
|
3
|
+
const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
|
|
4
|
+
const segmenter = new Intl.Segmenter()
|
|
5
|
+
|
|
6
|
+
export function truncateText(
|
|
7
|
+
text: string,
|
|
8
|
+
maxWidth: number,
|
|
9
|
+
ellipsis = '...',
|
|
10
|
+
pad = false,
|
|
11
|
+
): string {
|
|
12
|
+
if (maxWidth <= 0) return ''
|
|
13
|
+
|
|
14
|
+
const totalWidth = visibleWidth(text)
|
|
15
|
+
if (totalWidth <= maxWidth) {
|
|
16
|
+
return pad ? text + ' '.repeat(maxWidth - totalWidth) : text
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ellipsisWidth = visibleWidth(ellipsis)
|
|
20
|
+
const useEllipsis = ellipsisWidth < maxWidth
|
|
21
|
+
const targetWidth = useEllipsis ? maxWidth - ellipsisWidth : maxWidth
|
|
22
|
+
|
|
23
|
+
const appendFitting = (chunk: string): boolean => {
|
|
24
|
+
for (const { segment } of segmenter.segment(chunk)) {
|
|
25
|
+
const segmentWidth = visibleWidth(segment)
|
|
26
|
+
if (width + segmentWidth > targetWidth) return false
|
|
27
|
+
result += segment
|
|
28
|
+
width += segmentWidth
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let result = ''
|
|
34
|
+
let width = 0
|
|
35
|
+
let cursor = 0
|
|
36
|
+
let truncated = false
|
|
37
|
+
for (const match of text.matchAll(ANSI_RE)) {
|
|
38
|
+
if (!appendFitting(text.slice(cursor, match.index))) {
|
|
39
|
+
truncated = true
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
result += match[0]
|
|
43
|
+
cursor = match.index + match[0].length
|
|
44
|
+
}
|
|
45
|
+
if (truncated) {
|
|
46
|
+
result += [...text.slice(cursor).matchAll(ANSI_RE)]
|
|
47
|
+
.map((m) => m[0])
|
|
48
|
+
.join('')
|
|
49
|
+
} else {
|
|
50
|
+
appendFitting(text.slice(cursor))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (useEllipsis) result += ellipsis
|
|
54
|
+
if (pad)
|
|
55
|
+
result += ' '.repeat(
|
|
56
|
+
Math.max(0, maxWidth - width - (useEllipsis ? ellipsisWidth : 0)),
|
|
57
|
+
)
|
|
58
|
+
return result
|
|
59
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "dist",
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"lib": ["DOM", "ES2022"],
|
|
8
|
+
"target": "ES2022",
|
|
9
|
+
"module": "nodenext",
|
|
10
|
+
"stripInternal": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"moduleResolution": "nodenext",
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"rewriteRelativeImportExtensions": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"noUncheckedIndexedAccess": true,
|
|
20
|
+
"verbatimModuleSyntax": true,
|
|
21
|
+
"skipLibCheck": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src", "*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs"]
|
|
24
|
+
}
|