@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,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
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: false,
6
+ include: ['src/**/*.test.ts'],
7
+ },
8
+ })