pi-code 1.0.51 → 1.0.52
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/extensions/claude-rules.ts +41 -6
- package/extensions/env-settings.ts +10 -8
- package/extensions/hooks/decisions.ts +13 -1
- package/extensions/internal/atomic-write.ts +14 -0
- package/extensions/internal/mcp-oauth.ts +6 -1
- package/extensions/mcp/index.ts +5 -1
- package/extensions/memory.ts +1 -8
- package/extensions/output-styles.ts +30 -14
- package/extensions/status-line.ts +8 -2
- package/extensions/subagent/background.ts +11 -1
- package/extensions/subagent/index.ts +9 -24
- package/package.json +1 -1
|
@@ -39,12 +39,47 @@ function unquote(value: string): string {
|
|
|
39
39
|
return value.replace(/^["']|["']$/g, '')
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Split a YAML flow sequence on its top-level commas. A comma inside a brace group
|
|
43
|
+
* (`*.{ts,tsx}`, which Claude expands) or inside quotes belongs to its entry, so
|
|
44
|
+
* splitting on every comma produced patterns that match nothing. */
|
|
42
45
|
function splitInline(value: string): string[] {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const entries: string[] = []
|
|
47
|
+
let current = ''
|
|
48
|
+
let depth = 0
|
|
49
|
+
let quote: string | undefined
|
|
50
|
+
for (const char of value.replace(/^\[/, '').replace(/\]$/, '')) {
|
|
51
|
+
if (quote !== undefined) {
|
|
52
|
+
if (char === quote) quote = undefined
|
|
53
|
+
current += char
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (char === '"' || char === "'") {
|
|
57
|
+
quote = char
|
|
58
|
+
current += char
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (char === '{') depth++
|
|
62
|
+
else if (char === '}') depth = Math.max(0, depth - 1)
|
|
63
|
+
else if (char === ',' && depth === 0) {
|
|
64
|
+
entries.push(current)
|
|
65
|
+
current = ''
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
current += char
|
|
69
|
+
}
|
|
70
|
+
entries.push(current)
|
|
71
|
+
return entries.map((entry) => unquote(entry.trim())).filter(Boolean)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A flow sequence that opens on the `paths:` line and closes on a later one, joined
|
|
75
|
+
* back into a single value. An unterminated list runs to the end of the frontmatter. */
|
|
76
|
+
function joinFlowSequence(lines: string[], index: number, opening: string): string {
|
|
77
|
+
let joined = opening
|
|
78
|
+
for (let i = index + 1; i < lines.length; i++) {
|
|
79
|
+
joined += ` ${lines[i].trim()}`
|
|
80
|
+
if (lines[i].includes(']')) break
|
|
81
|
+
}
|
|
82
|
+
return joined
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
function parsePaths(frontmatter: string): string[] {
|
|
@@ -52,7 +87,7 @@ function parsePaths(frontmatter: string): string[] {
|
|
|
52
87
|
const index = lines.findIndex((line) => /^\s*paths\s*:/.test(line))
|
|
53
88
|
if (index === -1) return []
|
|
54
89
|
const inline = lines[index].replace(/^\s*paths\s*:/, '').trim()
|
|
55
|
-
if (inline) return splitInline(inline)
|
|
90
|
+
if (inline) return splitInline(inline.startsWith('[') && !inline.includes(']') ? joinFlowSequence(lines, index, inline) : inline)
|
|
56
91
|
const items: string[] = []
|
|
57
92
|
// Matched with string ops rather than a regex: the equivalent pattern needs two
|
|
58
93
|
// adjacent whitespace quantifiers, which backtracks super-linearly on long lines.
|
|
@@ -40,7 +40,7 @@ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-a
|
|
|
40
40
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
41
41
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
42
42
|
import { isProjectApprovedSilently } from './internal/project-approval.js'
|
|
43
|
-
import {
|
|
43
|
+
import { claudeSettingsChain } from './internal/settings-chain.js'
|
|
44
44
|
|
|
45
45
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
46
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
@@ -137,12 +137,14 @@ export function sanitizeProjectEnv(env: Record<string, string>, warn: (key: stri
|
|
|
137
137
|
return kept
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/** The project scope's env
|
|
141
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
/** The project scope's env, resolved through the shared settings chain so placement
|
|
141
|
+
* matches every other consumer: the shared settings.json comes from the session's own
|
|
142
|
+
* directory and never an ancestor, settings.local.json from the repository root. Later
|
|
143
|
+
* files win. */
|
|
144
|
+
function projectEnv(cwd: string, home: string): Record<string, string> {
|
|
145
|
+
const merged: Record<string, string> = {}
|
|
146
|
+
for (const file of claudeSettingsChain(cwd, home, true).slice(1)) Object.assign(merged, envFromSettings(readSettingsFile(file)))
|
|
147
|
+
return sanitizeProjectEnv(merged)
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
export default function envSettingsExtension(pi: ExtensionAPI) {
|
|
@@ -157,6 +159,6 @@ export default function envSettingsExtension(pi: ExtensionAPI) {
|
|
|
157
159
|
apply(os.homedir(), {})
|
|
158
160
|
|
|
159
161
|
pi.on('session_start', async (_event, ctx: ExtensionContext) => {
|
|
160
|
-
apply(os.homedir(), isProjectApprovedSilently(ctx) ? projectEnv(ctx.cwd) : {})
|
|
162
|
+
apply(os.homedir(), isProjectApprovedSilently(ctx) ? projectEnv(ctx.cwd, os.homedir()) : {})
|
|
161
163
|
})
|
|
162
164
|
}
|
|
@@ -147,7 +147,19 @@ function surfaceHookFailures(commands: HookCommand[], results: HookRunResult[],
|
|
|
147
147
|
// Claude shows a `<hook> hook error` notice when {..}-shaped stdout cannot be
|
|
148
148
|
// read as JSON output (exit 2 still blocks and reads its own channels).
|
|
149
149
|
const jsonError = result.code === 2 ? undefined : hookJsonError(result.stdout)
|
|
150
|
-
if (jsonError !== undefined)
|
|
150
|
+
if (jsonError !== undefined) {
|
|
151
|
+
notify(`${commands[i].command} hook error: ${jsonError}`)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
// A non-zero exit that is neither a block (2) nor a timeout, with nothing parseable
|
|
155
|
+
// on stdout, is Claude's non-blocking error: the action proceeds and the notice
|
|
156
|
+
// carries the first line of stderr. Without it a mistyped path in settings.json
|
|
157
|
+
// leaves a policy hook silently disabled, since the shell exits 127 and says so only
|
|
158
|
+
// on stderr. A spawn failure is reported above and skipped here.
|
|
159
|
+
if (!result.spawnFailed && !result.timedOut && result.code !== 0 && result.code !== 2) {
|
|
160
|
+
const firstLine = result.stderr.trim().split('\n')[0]
|
|
161
|
+
notify(`${commands[i].command} hook error: Failed with non-blocking status code: ${firstLine || result.code}`)
|
|
162
|
+
}
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace a file through a temp file and a rename, so a crash mid-write cannot leave the
|
|
3
|
+
* target truncated. Used wherever pi-code rewrites a file the user owns and would have to
|
|
4
|
+
* repair by hand: settings and the memory index.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs'
|
|
8
|
+
|
|
9
|
+
/** The tmp name carries the pid so concurrent processes do not collide. */
|
|
10
|
+
export function atomicWriteFile(filePath: string, content: string): void {
|
|
11
|
+
const tmp = `${filePath}.${process.pid}.tmp`
|
|
12
|
+
fs.writeFileSync(tmp, content)
|
|
13
|
+
fs.renameSync(tmp, filePath)
|
|
14
|
+
}
|
|
@@ -244,7 +244,12 @@ export function openBrowser(url: string): void {
|
|
|
244
244
|
else if (process.platform === 'win32') command = 'cmd'
|
|
245
245
|
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
|
|
246
246
|
try {
|
|
247
|
-
spawn(command, args, { stdio: 'ignore', detached: true })
|
|
247
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true })
|
|
248
|
+
// A missing launcher (a container or an SSH session with no xdg-open) reports itself
|
|
249
|
+
// asynchronously through 'error'; with no listener node raises it as an
|
|
250
|
+
// uncaughtException and pi exits. The caller has already shown the URL to open by hand.
|
|
251
|
+
child.on('error', () => {})
|
|
252
|
+
child.unref()
|
|
248
253
|
} catch {
|
|
249
254
|
// the notified URL is the fallback
|
|
250
255
|
}
|
package/extensions/mcp/index.ts
CHANGED
|
@@ -563,8 +563,12 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
563
563
|
// is why serverToolCount reads from `registered` to recover the true count here.
|
|
564
564
|
status.clear()
|
|
565
565
|
// A same-process session switch (/new, /resume) shut the last session down;
|
|
566
|
-
// this one may reconnect again.
|
|
566
|
+
// this one may reconnect again. projectConnected guards against connecting the project
|
|
567
|
+
// scope twice within one session, so it belongs to the session that set it: leaving it
|
|
568
|
+
// set here left every project server disconnected for the rest of the process, since
|
|
569
|
+
// the shutdown had already dropped their clients.
|
|
567
570
|
shuttingDown = false
|
|
571
|
+
projectConnected = false
|
|
568
572
|
// Claude answers roots/list with the session's launch directory and exports the
|
|
569
573
|
// project root as CLAUDE_PROJECT_DIR to stdio servers; both derive from ctx.cwd.
|
|
570
574
|
sessionDirs = { projectDir: repoRoot(ctx.cwd) ?? ctx.cwd, launchDir: ctx.cwd }
|
package/extensions/memory.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'node:path'
|
|
|
14
14
|
import { StringEnum } from '@earendil-works/pi-ai'
|
|
15
15
|
import { type ExtensionAPI, withFileMutationQueue } from '@earendil-works/pi-coding-agent'
|
|
16
16
|
import { Type } from 'typebox'
|
|
17
|
+
import { atomicWriteFile } from './internal/atomic-write.js'
|
|
17
18
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
18
19
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
19
20
|
import { capForContext } from './internal/output-guard.js'
|
|
@@ -322,14 +323,6 @@ function readIndexQuietly(dir: string): string {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
/** Write through a temp file and rename onto the target, so a crash mid-write cannot
|
|
326
|
-
* truncate it. The tmp name carries the pid so concurrent processes do not collide. */
|
|
327
|
-
function atomicWriteFile(filePath: string, content: string): void {
|
|
328
|
-
const tmp = `${filePath}.${process.pid}.tmp`
|
|
329
|
-
fs.writeFileSync(tmp, content)
|
|
330
|
-
fs.renameSync(tmp, filePath)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
326
|
/** Replace the index through a rename so a crash mid-write cannot truncate it. */
|
|
334
327
|
function writeIndex(indexPath: string, content: string): void {
|
|
335
328
|
atomicWriteFile(indexPath, content)
|
|
@@ -23,6 +23,7 @@ import * as fs from 'node:fs'
|
|
|
23
23
|
import * as os from 'node:os'
|
|
24
24
|
import * as path from 'node:path'
|
|
25
25
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
26
|
+
import { atomicWriteFile } from './internal/atomic-write.js'
|
|
26
27
|
|
|
27
28
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
28
29
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
@@ -101,10 +102,10 @@ function isDirectory(target: string): boolean {
|
|
|
101
102
|
*/
|
|
102
103
|
export function styleDirs(cwd: string, home: string, trusted: boolean): string[] {
|
|
103
104
|
const dirs = [path.join(claudeConfigDir(home), 'output-styles')]
|
|
104
|
-
// Claude loads every .claude/output-styles between cwd and the repository root,
|
|
105
|
-
//
|
|
106
|
-
// so the
|
|
107
|
-
if (trusted) dirs.push(...ancestorDirs(cwd, path.join('.claude', 'output-styles')))
|
|
105
|
+
// Claude loads every .claude/output-styles between cwd and the repository root, using
|
|
106
|
+
// the one closest to cwd for a name clash. loadStyles keys by name and later dirs win,
|
|
107
|
+
// so the ancestors go farthest first and the nearest lands last.
|
|
108
|
+
if (trusted) dirs.push(...ancestorDirs(cwd, path.join('.claude', 'output-styles')).reverse())
|
|
108
109
|
return dirs.filter((dir) => isDirectory(dir))
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -173,16 +174,31 @@ export function styleForName(styles: OutputStyle[], name: string | undefined): O
|
|
|
173
174
|
return name ? styles.find((style) => style.name === name) : undefined
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
/** Record the choice in the local settings file, returning a message to show when it
|
|
178
|
+
* cannot be recorded. The file also carries permissions, hooks and env, so a present but
|
|
179
|
+
* unparseable one is left alone rather than replaced by the style choice alone. */
|
|
180
|
+
function persistActiveStyle(file: string, name: string): string | undefined {
|
|
181
|
+
const label = path.basename(file)
|
|
182
|
+
let raw: string | undefined
|
|
178
183
|
try {
|
|
179
|
-
|
|
180
|
-
} catch {
|
|
181
|
-
//
|
|
184
|
+
raw = fs.readFileSync(file, 'utf-8')
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Only a missing file means start fresh; anything else is the user's file to keep.
|
|
187
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return `${label} could not be read; the style applies to this session only`
|
|
188
|
+
}
|
|
189
|
+
let config: Record<string, unknown> = {}
|
|
190
|
+
if (raw !== undefined) {
|
|
191
|
+
try {
|
|
192
|
+
const parsed: unknown = JSON.parse(raw)
|
|
193
|
+
if (parsed !== null && typeof parsed === 'object') config = parsed as Record<string, unknown>
|
|
194
|
+
} catch {
|
|
195
|
+
return `${label} is not valid JSON; the style applies to this session only`
|
|
196
|
+
}
|
|
182
197
|
}
|
|
183
198
|
config.outputStyle = name
|
|
184
199
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
185
|
-
|
|
200
|
+
atomicWriteFile(file, `${JSON.stringify(config, null, 2)}\n`)
|
|
201
|
+
return undefined
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
export default function outputStylesExtension(pi: ExtensionAPI) {
|
|
@@ -236,8 +252,8 @@ export default function outputStylesExtension(pi: ExtensionAPI) {
|
|
|
236
252
|
return
|
|
237
253
|
}
|
|
238
254
|
activeName = picked.name
|
|
239
|
-
persistActiveStyle(localSettingsPath, picked.name)
|
|
240
|
-
ctx.ui.notify(`Output style set to ${picked.name} (applies next turn)`, 'info')
|
|
255
|
+
const failure = persistActiveStyle(localSettingsPath, picked.name)
|
|
256
|
+
ctx.ui.notify(failure ?? `Output style set to ${picked.name} (applies next turn)`, failure ? 'error' : 'info')
|
|
241
257
|
return
|
|
242
258
|
}
|
|
243
259
|
if (!ctx.hasUI) {
|
|
@@ -253,8 +269,8 @@ export default function outputStylesExtension(pi: ExtensionAPI) {
|
|
|
253
269
|
if (!choice) return
|
|
254
270
|
const picked = styles[labels.indexOf(choice)]
|
|
255
271
|
activeName = picked.name
|
|
256
|
-
persistActiveStyle(localSettingsPath, picked.name)
|
|
257
|
-
ctx.ui.notify(`Output style set to ${picked.name} (applies next turn)`, 'info')
|
|
272
|
+
const failure = persistActiveStyle(localSettingsPath, picked.name)
|
|
273
|
+
ctx.ui.notify(failure ?? `Output style set to ${picked.name} (applies next turn)`, failure ? 'error' : 'info')
|
|
258
274
|
},
|
|
259
275
|
})
|
|
260
276
|
}
|
|
@@ -457,14 +457,20 @@ export default function statusLine(pi: ExtensionAPI) {
|
|
|
457
457
|
// Claude's disableAllHooks also turns off the custom statusLine command; the
|
|
458
458
|
// built-in segment still renders as the fallback.
|
|
459
459
|
config = readDisableAllHooks(files) ? undefined : readStatusLineConfig(files)
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
// Re-armed rather than armed once: a refreshInterval added or changed mid-session
|
|
461
|
+
// had no effect until the next session, though Claude applies a settings change as
|
|
462
|
+
// soon as the file is saved.
|
|
463
|
+
const armRefresh = (): void => {
|
|
464
|
+
clearInterval(refreshTimer)
|
|
465
|
+
refreshTimer = config?.refreshInterval ? setInterval(() => scheduleRefresh(), config.refreshInterval * 1000) : undefined
|
|
462
466
|
}
|
|
467
|
+
armRefresh()
|
|
463
468
|
// Claude re-runs the script when the statusLine settings change mid-session; a
|
|
464
469
|
// command change re-resolves and re-runs.
|
|
465
470
|
disposeSettingsWatch()
|
|
466
471
|
disposeSettingsWatch = watchSettingsFiles(files, () => {
|
|
467
472
|
config = readDisableAllHooks(files) ? undefined : readStatusLineConfig(files)
|
|
473
|
+
armRefresh()
|
|
468
474
|
scheduleRefresh()
|
|
469
475
|
})
|
|
470
476
|
show(ctx, segmentText(ctx, ctx.ui.theme.fg('dim', '○')))
|
|
@@ -177,6 +177,13 @@ export function backgroundStatusText(): string {
|
|
|
177
177
|
return formatStatus(runs.values())
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** Every run this process knows about, live and finished. The registry is module state
|
|
181
|
+
* that outlives a session switch; a per-session list is not, because pi re-invokes each
|
|
182
|
+
* extension factory per session. */
|
|
183
|
+
export function allBackgroundRuns(): BackgroundRun[] {
|
|
184
|
+
return [...runs.values()]
|
|
185
|
+
}
|
|
186
|
+
|
|
180
187
|
/** A finished run, so a caller can continue its session with a follow-up task. */
|
|
181
188
|
export function backgroundRun(id: string): BackgroundRun | undefined {
|
|
182
189
|
return runs.get(id)
|
|
@@ -200,7 +207,10 @@ export function resumeBackgroundRun(id: string, task: string, onComplete: (run:
|
|
|
200
207
|
const rebuilt = withRebuiltPrompt(run.spawn)
|
|
201
208
|
run.spawn = { ...run.spawn, args: rebuilt.args }
|
|
202
209
|
if (rebuilt.dir) run.rebuiltPromptDir = rebuilt.dir
|
|
203
|
-
|
|
210
|
+
// The task prompt is always the final argument: both spawn paths push it last and the
|
|
211
|
+
// invocation helper only prepends. Matching a 'Task: ' prefix missed it whenever
|
|
212
|
+
// SubagentStart hooks placed their context ahead of it, so the resume re-ran the old task.
|
|
213
|
+
const args = rebuilt.args.map((arg, index) => (index === rebuilt.args.length - 1 ? `Task: ${task}` : arg))
|
|
204
214
|
run.state = 'running'
|
|
205
215
|
run.task = task
|
|
206
216
|
run.output = undefined
|
|
@@ -34,7 +34,7 @@ import { runSubagentStartHooks } from '../internal/subagent-hooks.js'
|
|
|
34
34
|
import { autoMemoryEnabled, capIndexForPrompt, INDEX_MAX_BYTES, INDEX_MAX_LINES, memorySettingsFiles, readMemorySettings } from '../memory.js'
|
|
35
35
|
import { skillDirs } from '../skills.js'
|
|
36
36
|
import { type AgentConfig, type AgentMemoryScope, type AgentScope, type AgentSource, discoverAgents, expandMcpToolPatterns, resolveModelAlias, withPreloadedSkills } from './agents.js'
|
|
37
|
-
import { activeBackgroundRuns, type BackgroundRun, backgroundRun, backgroundStatusText, cancelAllBackgroundRuns, cancelBackgroundRun, MAX_BACKGROUND_RUNS, resumeBackgroundRun, startBackgroundRun } from './background.js'
|
|
37
|
+
import { activeBackgroundRuns, allBackgroundRuns, type BackgroundRun, backgroundRun, backgroundStatusText, cancelAllBackgroundRuns, cancelBackgroundRun, MAX_BACKGROUND_RUNS, resumeBackgroundRun, startBackgroundRun } from './background.js'
|
|
38
38
|
import { type DisplayItem, formatToolCall, formatUsageStats, getDisplayItems, getFinalOutput } from './render.js'
|
|
39
39
|
import { type AgentWorktree, cleanupAgentWorktree, createAgentWorktree } from './worktree.js'
|
|
40
40
|
|
|
@@ -388,8 +388,11 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
388
388
|
resolve(code ?? 0)
|
|
389
389
|
})
|
|
390
390
|
|
|
391
|
-
proc.on('error', () => {
|
|
391
|
+
proc.on('error', (error: Error) => {
|
|
392
392
|
cleanup()
|
|
393
|
+
// A child that never started leaves no stdout and no stderr, so this message is the
|
|
394
|
+
// only diagnostic; without it the failure reads as "(no output)".
|
|
395
|
+
if (!currentResult.stderr) currentResult.stderr = error.message
|
|
393
396
|
resolve(1)
|
|
394
397
|
})
|
|
395
398
|
|
|
@@ -566,7 +569,7 @@ function runOutputTail(run: BackgroundRunView): string | undefined {
|
|
|
566
569
|
/** The /tasks listing: one line per background run, plus the short output tail the
|
|
567
570
|
* registry's own status lines omit. A pure formatter so it tests against a plain list. */
|
|
568
571
|
export function tasksStatusText(runs: ReadonlyArray<BackgroundRunView>): string {
|
|
569
|
-
if (runs.length === 0) return 'No background subagent runs
|
|
572
|
+
if (runs.length === 0) return 'No background subagent runs.'
|
|
570
573
|
return runs
|
|
571
574
|
.map((run) => {
|
|
572
575
|
const plural = run.turns === 1 ? '' : 's'
|
|
@@ -858,7 +861,7 @@ interface BackgroundContext {
|
|
|
858
861
|
projectApproved: boolean
|
|
859
862
|
}
|
|
860
863
|
|
|
861
|
-
async function runBackgroundMode(params: SubagentParamsStatic, context: BackgroundContext
|
|
864
|
+
async function runBackgroundMode(params: SubagentParamsStatic, context: BackgroundContext): Promise<ToolResult> {
|
|
862
865
|
const { agents, defaultCwd, pi, makeDetails, skillRoots, availableModels, projectApproved } = context
|
|
863
866
|
const task = params.task
|
|
864
867
|
const agentName = params.agent
|
|
@@ -955,7 +958,6 @@ async function runBackgroundMode(params: SubagentParamsStatic, context: Backgrou
|
|
|
955
958
|
removeTmpPrompt(tmpPrompt)
|
|
956
959
|
return backgroundCapResult(makeDetails)
|
|
957
960
|
}
|
|
958
|
-
onStarted?.(id)
|
|
959
961
|
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'start', agentType: agent.name, agentId: id })
|
|
960
962
|
return {
|
|
961
963
|
content: [{ type: 'text', text: `Started background run ${id} (${agent.name}). A notification will arrive on completion; check progress with {status: true}.` }],
|
|
@@ -1437,21 +1439,6 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1437
1439
|
if (isMcpToolAliases(data)) setKnownMcpAliases(data)
|
|
1438
1440
|
})
|
|
1439
1441
|
|
|
1440
|
-
// /tasks resolves these against the registry at print time. background.ts owns the
|
|
1441
|
-
// run records but does not enumerate them, so the ids started here are remembered;
|
|
1442
|
-
// a run the registry has since evicted simply drops out of the listing.
|
|
1443
|
-
const startedBackgroundRuns = new Set<string>()
|
|
1444
|
-
|
|
1445
|
-
// The registry self-caps and evicts old runs, so an id kept here after its record is
|
|
1446
|
-
// gone is dead weight. Drop those on every add, bounding the set to the registry's
|
|
1447
|
-
// live capacity rather than letting it grow for the whole session.
|
|
1448
|
-
const rememberBackgroundRun = (id: string): void => {
|
|
1449
|
-
for (const known of startedBackgroundRuns) {
|
|
1450
|
-
if (!backgroundRun(known)) startedBackgroundRuns.delete(known)
|
|
1451
|
-
}
|
|
1452
|
-
startedBackgroundRuns.add(id)
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
1442
|
const notifyBackgroundCompletion = (run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string }): void => {
|
|
1456
1443
|
// Runs through driveRun's guard, same as the background-mode callback above.
|
|
1457
1444
|
// The stop event fires here too, so SubagentStop hooks see resumed runs end.
|
|
@@ -1585,7 +1572,6 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1585
1572
|
|
|
1586
1573
|
if (params.resume) {
|
|
1587
1574
|
const onResumed = (run: { id: string; agent: string }): void => {
|
|
1588
|
-
rememberBackgroundRun(run.id)
|
|
1589
1575
|
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'start', agentType: run.agent, agentId: run.id })
|
|
1590
1576
|
}
|
|
1591
1577
|
return { content: [{ type: 'text', text: resumeResultText(params.resume, params.task, notifyBackgroundCompletion, onResumed) }], details: makeDetails('single')([]) }
|
|
@@ -1629,7 +1615,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1629
1615
|
// unavailable tier still falls back to the session model.
|
|
1630
1616
|
const availableModels = ctx.modelRegistry?.getAvailable?.() ?? []
|
|
1631
1617
|
|
|
1632
|
-
if (wantsBackground(params, agents)) return runBackgroundMode(params, { agents, defaultCwd: ctx.cwd, pi, makeDetails, skillRoots, availableModels, projectApproved }
|
|
1618
|
+
if (wantsBackground(params, agents)) return runBackgroundMode(params, { agents, defaultCwd: ctx.cwd, pi, makeDetails, skillRoots, availableModels, projectApproved })
|
|
1633
1619
|
|
|
1634
1620
|
const mode: ModeContext = {
|
|
1635
1621
|
agents,
|
|
@@ -1686,8 +1672,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1686
1672
|
pi.registerCommand('tasks', {
|
|
1687
1673
|
description: 'Show background subagent runs',
|
|
1688
1674
|
handler: async (_args, ctx) => {
|
|
1689
|
-
|
|
1690
|
-
ctx.ui.notify(tasksStatusText(runs), 'info')
|
|
1675
|
+
ctx.ui.notify(tasksStatusText(allBackgroundRuns()), 'info')
|
|
1691
1676
|
},
|
|
1692
1677
|
})
|
|
1693
1678
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, subagents, and goals",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|