@herbertgao/pi-subagents 0.16.1 → 0.17.0
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/CHANGELOG.md +10 -0
- package/README.md +21 -15
- package/package.json +3 -3
- package/src/agent-file-toggle.ts +11 -8
- package/src/agent-manager.ts +184 -106
- package/src/cross-extension-rpc.ts +31 -11
- package/src/custom-agents.ts +9 -1
- package/src/index.ts +156 -31
- package/src/invocation-config.ts +17 -0
- package/src/model-resolver.ts +14 -0
- package/src/schedule.ts +8 -0
- package/src/settings.ts +32 -1
- package/src/types.ts +17 -2
- package/src/ui/agent-widget.ts +23 -2
- package/src/ui/conversation-viewer.ts +259 -24
package/src/index.ts
CHANGED
|
@@ -77,7 +77,11 @@ import {
|
|
|
77
77
|
resolveAgentInvocationConfig,
|
|
78
78
|
resolveJoinMode,
|
|
79
79
|
} from "./invocation-config.js"
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
describeModel,
|
|
82
|
+
type ModelRegistry,
|
|
83
|
+
resolveModel,
|
|
84
|
+
} from "./model-resolver.js"
|
|
81
85
|
import {
|
|
82
86
|
checkModelScope,
|
|
83
87
|
isScopeModelsEnabled,
|
|
@@ -113,6 +117,7 @@ import {
|
|
|
113
117
|
type JoinMode,
|
|
114
118
|
type NotificationDetails,
|
|
115
119
|
type SubagentType,
|
|
120
|
+
type ViewerMarkdownMode,
|
|
116
121
|
type WidgetMode,
|
|
117
122
|
} from "./types.js"
|
|
118
123
|
import {
|
|
@@ -541,6 +546,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
541
546
|
widget.update()
|
|
542
547
|
fleet.update()
|
|
543
548
|
}
|
|
549
|
+
let showModel = false
|
|
550
|
+
function isShowModelEnabled(): boolean {
|
|
551
|
+
return showModel
|
|
552
|
+
}
|
|
553
|
+
function setShowModel(enabled: boolean): void {
|
|
554
|
+
showModel = enabled
|
|
555
|
+
widget.update()
|
|
556
|
+
}
|
|
557
|
+
let viewerMarkdown: ViewerMarkdownMode = "assistant"
|
|
558
|
+
function getViewerMarkdown(): ViewerMarkdownMode {
|
|
559
|
+
return viewerMarkdown
|
|
560
|
+
}
|
|
561
|
+
function setViewerMarkdown(mode: ViewerMarkdownMode): void {
|
|
562
|
+
viewerMarkdown = mode
|
|
563
|
+
}
|
|
544
564
|
|
|
545
565
|
// ---- Cancellable pending notifications ----
|
|
546
566
|
// Holds notifications briefly so get_subagent_result can cancel them
|
|
@@ -800,6 +820,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
800
820
|
// Also internal: it names a transcript directory, so a forged value would
|
|
801
821
|
// be a path-traversal primitive.
|
|
802
822
|
delete safeOptions.rootSessionId
|
|
823
|
+
// Every call through this registry is detached, never a blocking tool call.
|
|
824
|
+
delete safeOptions.blocking
|
|
803
825
|
// Cross-extension callers get the same dispatch contract as the LLM (#183).
|
|
804
826
|
// The RPC layer already throws for an unresolvable model rather than falling
|
|
805
827
|
// back silently; a bad agent type should not be quieter. Throws become error
|
|
@@ -958,6 +980,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
958
980
|
agentActivity,
|
|
959
981
|
getWidgetMode,
|
|
960
982
|
isShowCostEnabled,
|
|
983
|
+
isShowModelEnabled,
|
|
961
984
|
)
|
|
962
985
|
function setWidgetMode(m: WidgetMode): void {
|
|
963
986
|
widgetMode = m
|
|
@@ -1133,6 +1156,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1156
|
applyAndEmitLoaded(
|
|
1134
1157
|
{
|
|
1135
1158
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
1159
|
+
setMaxConcurrentForeground: (n) => manager.setMaxConcurrentForeground(n),
|
|
1136
1160
|
setDefaultMaxTurns,
|
|
1137
1161
|
setGraceTurns,
|
|
1138
1162
|
setDefaultJoinMode,
|
|
@@ -1152,6 +1176,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1152
1176
|
setFallbackSubagent: setFallbackSubagent,
|
|
1153
1177
|
setReportUsage,
|
|
1154
1178
|
setShowCost,
|
|
1179
|
+
setShowModel,
|
|
1180
|
+
setViewerMarkdown,
|
|
1155
1181
|
},
|
|
1156
1182
|
(event, payload) => pi.events.emit(event, payload),
|
|
1157
1183
|
)
|
|
@@ -1637,20 +1663,27 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1637
1663
|
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd)
|
|
1638
1664
|
}
|
|
1639
1665
|
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1666
|
+
const { modelName, modelId } = model
|
|
1667
|
+
? describeModel(model)
|
|
1668
|
+
: { modelName: undefined, modelId: undefined }
|
|
1669
|
+
const askedModel = ((asked: string | undefined) => {
|
|
1670
|
+
if (!asked) return undefined
|
|
1671
|
+
const resolvedAsked = resolveModel(asked, ctx.modelRegistry)
|
|
1672
|
+
if (typeof resolvedAsked === "string") return asked
|
|
1673
|
+
return resolvedAsked.provider === model?.provider &&
|
|
1674
|
+
resolvedAsked.id === model?.id
|
|
1675
|
+
? undefined
|
|
1676
|
+
: asked
|
|
1677
|
+
})(resolvedConfig.overridden?.model)
|
|
1648
1678
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
1649
1679
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
1650
1680
|
)
|
|
1651
1681
|
const agentInvocation: AgentInvocation = {
|
|
1652
1682
|
modelName,
|
|
1683
|
+
modelId,
|
|
1653
1684
|
thinking,
|
|
1685
|
+
requestedThinking: resolvedConfig.overridden?.thinking,
|
|
1686
|
+
requestedModel: askedModel,
|
|
1654
1687
|
// Explicit value only — the default fallback would just add noise.
|
|
1655
1688
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
1656
1689
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
@@ -1672,6 +1705,23 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1672
1705
|
modelName,
|
|
1673
1706
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
1674
1707
|
}
|
|
1708
|
+
const detailBaseFor = (
|
|
1709
|
+
rec: AgentRecord | undefined,
|
|
1710
|
+
): typeof detailBase => {
|
|
1711
|
+
if (!rec?.invocation) return detailBase
|
|
1712
|
+
const { modelName: recModelName, tags } = buildInvocationTags(
|
|
1713
|
+
rec.invocation,
|
|
1714
|
+
)
|
|
1715
|
+
const recModeLabel = getPromptModeLabel(rec.type)
|
|
1716
|
+
const recTags = recModeLabel ? [recModeLabel, ...tags] : tags
|
|
1717
|
+
return {
|
|
1718
|
+
displayName: getDisplayName(rec.type),
|
|
1719
|
+
description: rec.description,
|
|
1720
|
+
subagentType: rec.type,
|
|
1721
|
+
modelName: recModelName,
|
|
1722
|
+
tags: recTags.length > 0 ? recTags : undefined,
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1675
1725
|
|
|
1676
1726
|
// ---- Schedule: register a job, don't spawn now ----
|
|
1677
1727
|
if (params.schedule) {
|
|
@@ -1865,9 +1915,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1865
1915
|
`\nYou will be notified when this agent completes.\n` +
|
|
1866
1916
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
|
|
1867
1917
|
{
|
|
1868
|
-
...
|
|
1869
|
-
subagentType: existing.type,
|
|
1870
|
-
displayName: existing.type,
|
|
1918
|
+
...detailBaseFor(record),
|
|
1871
1919
|
toolUses: record.toolUses,
|
|
1872
1920
|
tokens: "",
|
|
1873
1921
|
durationMs: 0,
|
|
@@ -1890,12 +1938,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1890
1938
|
if (record.status === "error") {
|
|
1891
1939
|
return textResult(
|
|
1892
1940
|
`Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
1893
|
-
buildDetails(
|
|
1941
|
+
buildDetails(detailBaseFor(record), record),
|
|
1894
1942
|
)
|
|
1895
1943
|
}
|
|
1896
1944
|
return textResult(
|
|
1897
1945
|
record.result?.trim() || "No output.",
|
|
1898
|
-
buildDetails(
|
|
1946
|
+
buildDetails(detailBaseFor(record), record),
|
|
1899
1947
|
)
|
|
1900
1948
|
}
|
|
1901
1949
|
|
|
@@ -1990,7 +2038,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1990
2038
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
1991
2039
|
`Do not duplicate this agent's work.`,
|
|
1992
2040
|
{
|
|
1993
|
-
...
|
|
2041
|
+
...detailBaseFor(record),
|
|
1994
2042
|
toolUses: 0,
|
|
1995
2043
|
tokens: "",
|
|
1996
2044
|
durationMs: 0,
|
|
@@ -2004,10 +2052,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2004
2052
|
let spinnerFrame = 0
|
|
2005
2053
|
const startedAt = Date.now()
|
|
2006
2054
|
let fgId: string | undefined
|
|
2055
|
+
let queuedAhead: number | undefined
|
|
2007
2056
|
|
|
2008
2057
|
const streamUpdate = () => {
|
|
2009
2058
|
const details: AgentDetails = {
|
|
2010
|
-
...
|
|
2059
|
+
...detailBaseFor(fgId ? manager.getRecord(fgId) : undefined),
|
|
2011
2060
|
toolUses: fgState.toolUses,
|
|
2012
2061
|
tokens: fgId ? formatLifetimeTokens(manager.getRecord(fgId)!) : "",
|
|
2013
2062
|
cost: fgId
|
|
@@ -2017,10 +2066,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2017
2066
|
maxTurns: fgState.maxTurns,
|
|
2018
2067
|
durationMs: Date.now() - startedAt,
|
|
2019
2068
|
status: "running",
|
|
2020
|
-
activity:
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2069
|
+
activity:
|
|
2070
|
+
queuedAhead === undefined
|
|
2071
|
+
? describeActivity(fgState.activeTools, fgState.responseText)
|
|
2072
|
+
: `queued — waiting for a foreground slot${queuedAhead > 0 ? ` (${queuedAhead} ahead)` : ""}`,
|
|
2024
2073
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
2025
2074
|
}
|
|
2026
2075
|
onUpdate?.({
|
|
@@ -2040,6 +2089,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2040
2089
|
const origOnSession = fgCallbacks.onSessionCreated
|
|
2041
2090
|
fgCallbacks.onSessionCreated = (session: any) => {
|
|
2042
2091
|
origOnSession(session)
|
|
2092
|
+
if (queuedAhead !== undefined) {
|
|
2093
|
+
queuedAhead = undefined
|
|
2094
|
+
streamUpdate()
|
|
2095
|
+
}
|
|
2043
2096
|
for (const a of manager.listAgents()) {
|
|
2044
2097
|
if (a.session === session) {
|
|
2045
2098
|
fgId = a.id
|
|
@@ -2090,6 +2143,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2090
2143
|
invocation: agentInvocation,
|
|
2091
2144
|
signal,
|
|
2092
2145
|
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2146
|
+
onQueued: (_id, ahead) => {
|
|
2147
|
+
queuedAhead = ahead
|
|
2148
|
+
streamUpdate()
|
|
2149
|
+
},
|
|
2093
2150
|
...fgCallbacks,
|
|
2094
2151
|
},
|
|
2095
2152
|
(fgAgentId) => {
|
|
@@ -2114,7 +2171,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2114
2171
|
// Get final token count
|
|
2115
2172
|
const tokenText = formatLifetimeTokens(record)
|
|
2116
2173
|
|
|
2117
|
-
const details = buildDetails(
|
|
2174
|
+
const details = buildDetails(detailBaseFor(record), record, fgState, {
|
|
2118
2175
|
tokens: tokenText,
|
|
2119
2176
|
})
|
|
2120
2177
|
|
|
@@ -2553,6 +2610,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2553
2610
|
keybindings,
|
|
2554
2611
|
(message: string) => manager.steer(record.id, message),
|
|
2555
2612
|
showCost,
|
|
2613
|
+
getViewerMarkdown,
|
|
2614
|
+
(mode) => {
|
|
2615
|
+
setViewerMarkdown(mode)
|
|
2616
|
+
persistSettings(ctx, `Viewer markdown set to ${mode}`)
|
|
2617
|
+
},
|
|
2556
2618
|
)
|
|
2557
2619
|
},
|
|
2558
2620
|
{
|
|
@@ -2844,6 +2906,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2844
2906
|
{
|
|
2845
2907
|
description: `Generate ${name} agent`,
|
|
2846
2908
|
maxTurns: 5,
|
|
2909
|
+
bypassQueue: true,
|
|
2847
2910
|
},
|
|
2848
2911
|
)
|
|
2849
2912
|
|
|
@@ -2968,6 +3031,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2968
3031
|
function snapshotSettings() {
|
|
2969
3032
|
return {
|
|
2970
3033
|
maxConcurrent: manager.getMaxConcurrent(),
|
|
3034
|
+
maxConcurrentForeground: manager.getMaxConcurrentForeground(),
|
|
2971
3035
|
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
2972
3036
|
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
2973
3037
|
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
@@ -2991,6 +3055,8 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2991
3055
|
fallbackSubagent: getFallbackSubagent(),
|
|
2992
3056
|
reportUsage: isReportUsageEnabled(),
|
|
2993
3057
|
showCost: isShowCostEnabled(),
|
|
3058
|
+
showModel: isShowModelEnabled(),
|
|
3059
|
+
viewerMarkdown: getViewerMarkdown(),
|
|
2994
3060
|
} satisfies SubagentsSettings
|
|
2995
3061
|
}
|
|
2996
3062
|
|
|
@@ -3010,6 +3076,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3010
3076
|
|
|
3011
3077
|
const NUMERIC_IDS = new Set([
|
|
3012
3078
|
"maxConcurrent",
|
|
3079
|
+
"maxConcurrentForeground",
|
|
3013
3080
|
"defaultMaxTurns",
|
|
3014
3081
|
"graceTurns",
|
|
3015
3082
|
"maxSubagentDepth",
|
|
@@ -3018,6 +3085,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3018
3085
|
async function showSettings(ctx: ExtensionCommandContext) {
|
|
3019
3086
|
function buildItems(): SettingItem[] {
|
|
3020
3087
|
const mc = manager.getMaxConcurrent()
|
|
3088
|
+
const mcf = manager.getMaxConcurrentForeground()
|
|
3021
3089
|
const dmt = getDefaultMaxTurns() ?? 0
|
|
3022
3090
|
const gt = getGraceTurns()
|
|
3023
3091
|
const msd = getMaxSubagentDepth()
|
|
@@ -3037,6 +3105,14 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3037
3105
|
currentValue: String(mc),
|
|
3038
3106
|
values: [String(mc)],
|
|
3039
3107
|
},
|
|
3108
|
+
{
|
|
3109
|
+
id: "maxConcurrentForeground",
|
|
3110
|
+
label: "Max foreground concurrency",
|
|
3111
|
+
description:
|
|
3112
|
+
"Max concurrent blocking agents (0 = unlimited, Enter to type)",
|
|
3113
|
+
currentValue: String(mcf),
|
|
3114
|
+
values: [String(mcf)],
|
|
3115
|
+
},
|
|
3040
3116
|
{
|
|
3041
3117
|
id: "defaultMaxTurns",
|
|
3042
3118
|
label: "Default max turns",
|
|
@@ -3144,6 +3220,21 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3144
3220
|
currentValue: isShowCostEnabled() ? "on" : "off",
|
|
3145
3221
|
values: ["on", "off"],
|
|
3146
3222
|
},
|
|
3223
|
+
{
|
|
3224
|
+
id: "showModel",
|
|
3225
|
+
label: "Show model",
|
|
3226
|
+
description: "Show model and thinking level in running widget rows",
|
|
3227
|
+
currentValue: isShowModelEnabled() ? "on" : "off",
|
|
3228
|
+
values: ["on", "off"],
|
|
3229
|
+
},
|
|
3230
|
+
{
|
|
3231
|
+
id: "viewerMarkdown",
|
|
3232
|
+
label: "Viewer markdown",
|
|
3233
|
+
description:
|
|
3234
|
+
"Conversation Markdown: assistant (default), all tool results, or off; press m in the viewer to cycle",
|
|
3235
|
+
currentValue: getViewerMarkdown(),
|
|
3236
|
+
values: ["off", "assistant", "all"],
|
|
3237
|
+
},
|
|
3147
3238
|
{
|
|
3148
3239
|
id: "fleetView",
|
|
3149
3240
|
label: "Fleet view",
|
|
@@ -3178,6 +3269,17 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3178
3269
|
manager.setMaxConcurrent(n)
|
|
3179
3270
|
notifyApplied(ctx, `Max concurrency set to ${n}`)
|
|
3180
3271
|
}
|
|
3272
|
+
} else if (id === "maxConcurrentForeground") {
|
|
3273
|
+
const n = parseInt(value, 10)
|
|
3274
|
+
if (n >= 0) {
|
|
3275
|
+
manager.setMaxConcurrentForeground(n)
|
|
3276
|
+
notifyApplied(
|
|
3277
|
+
ctx,
|
|
3278
|
+
n === 0
|
|
3279
|
+
? "Max foreground concurrency set to unlimited"
|
|
3280
|
+
: `Max foreground concurrency set to ${n}`,
|
|
3281
|
+
)
|
|
3282
|
+
}
|
|
3181
3283
|
} else if (id === "defaultMaxTurns") {
|
|
3182
3284
|
const n = parseInt(value, 10)
|
|
3183
3285
|
if (n === 0) {
|
|
@@ -3282,6 +3384,13 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3282
3384
|
const enabled = value === "on"
|
|
3283
3385
|
setShowCost(enabled)
|
|
3284
3386
|
notifyApplied(ctx, `Cost display ${enabled ? "enabled" : "disabled"}`)
|
|
3387
|
+
} else if (id === "showModel") {
|
|
3388
|
+
const enabled = value === "on"
|
|
3389
|
+
setShowModel(enabled)
|
|
3390
|
+
notifyApplied(ctx, `Model display ${enabled ? "enabled" : "disabled"}`)
|
|
3391
|
+
} else if (id === "viewerMarkdown") {
|
|
3392
|
+
setViewerMarkdown(value as ViewerMarkdownMode)
|
|
3393
|
+
notifyApplied(ctx, `Viewer markdown set to ${value}`)
|
|
3285
3394
|
} else if (id === "toolDescriptionMode") {
|
|
3286
3395
|
setToolDescriptionMode(value as ToolDescriptionMode)
|
|
3287
3396
|
notifyApplied(
|
|
@@ -3352,20 +3461,24 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3352
3461
|
const current =
|
|
3353
3462
|
result === "maxConcurrent"
|
|
3354
3463
|
? String(manager.getMaxConcurrent())
|
|
3355
|
-
: result === "
|
|
3356
|
-
? String(
|
|
3357
|
-
: result === "
|
|
3358
|
-
? String(
|
|
3359
|
-
:
|
|
3464
|
+
: result === "maxConcurrentForeground"
|
|
3465
|
+
? String(manager.getMaxConcurrentForeground())
|
|
3466
|
+
: result === "defaultMaxTurns"
|
|
3467
|
+
? String(getDefaultMaxTurns() ?? 0)
|
|
3468
|
+
: result === "maxSubagentDepth"
|
|
3469
|
+
? String(getMaxSubagentDepth())
|
|
3470
|
+
: String(getGraceTurns())
|
|
3360
3471
|
|
|
3361
3472
|
const label =
|
|
3362
3473
|
result === "maxConcurrent"
|
|
3363
3474
|
? "Max concurrency (1+)"
|
|
3364
|
-
: result === "
|
|
3365
|
-
? "
|
|
3366
|
-
: result === "
|
|
3367
|
-
? "
|
|
3368
|
-
:
|
|
3475
|
+
: result === "maxConcurrentForeground"
|
|
3476
|
+
? "Max foreground concurrency (0 = unlimited)"
|
|
3477
|
+
: result === "defaultMaxTurns"
|
|
3478
|
+
? "Default max turns (0 = unlimited)"
|
|
3479
|
+
: result === "maxSubagentDepth"
|
|
3480
|
+
? "Nested depth (0/1 = nesting off)"
|
|
3481
|
+
: "Grace turns (1+)"
|
|
3369
3482
|
|
|
3370
3483
|
// Loop until user enters a valid integer or cancels (Esc / null).
|
|
3371
3484
|
// Silently trims whitespace; rejects non-numeric input by re-prompting.
|
|
@@ -3388,6 +3501,18 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3388
3501
|
// the right toast. Successful saves show info; persistence failures downgrade
|
|
3389
3502
|
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
3390
3503
|
// of outcome so listeners see the in-memory change.
|
|
3504
|
+
function persistSettings(
|
|
3505
|
+
ctx: ExtensionCommandContext,
|
|
3506
|
+
successMsg: string,
|
|
3507
|
+
): void {
|
|
3508
|
+
const { message, level } = saveAndEmitChanged(
|
|
3509
|
+
snapshotSettings(),
|
|
3510
|
+
successMsg,
|
|
3511
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
3512
|
+
)
|
|
3513
|
+
if (level === "warning") ctx.ui.notify(message, level)
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3391
3516
|
function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
|
|
3392
3517
|
const { message, level } = saveAndEmitChanged(
|
|
3393
3518
|
snapshotSettings(),
|
package/src/invocation-config.ts
CHANGED
|
@@ -113,6 +113,7 @@ export function resolveAgentInvocationConfig(
|
|
|
113
113
|
runInBackground: boolean
|
|
114
114
|
isolated: boolean
|
|
115
115
|
isolation?: IsolationMode
|
|
116
|
+
overridden?: { thinking?: ThinkingLevel; model?: string }
|
|
116
117
|
} {
|
|
117
118
|
// Precedence first, collapse second — reversing these loses the veto, since
|
|
118
119
|
// an agent file's "off" only outranks a caller's "worktree" while it is still
|
|
@@ -122,6 +123,18 @@ export function resolveAgentInvocationConfig(
|
|
|
122
123
|
requested === "worktree" && opts?.worktreeAllowed !== false
|
|
123
124
|
? "worktree"
|
|
124
125
|
: undefined
|
|
126
|
+
const overriddenThinking =
|
|
127
|
+
agentConfig?.thinking != null &&
|
|
128
|
+
params.thinking != null &&
|
|
129
|
+
agentConfig.thinking !== params.thinking
|
|
130
|
+
? (params.thinking as ThinkingLevel)
|
|
131
|
+
: undefined
|
|
132
|
+
const overriddenModel =
|
|
133
|
+
agentConfig?.model != null &&
|
|
134
|
+
params.model != null &&
|
|
135
|
+
agentConfig.model !== params.model
|
|
136
|
+
? params.model
|
|
137
|
+
: undefined
|
|
125
138
|
|
|
126
139
|
return {
|
|
127
140
|
modelInput: agentConfig?.model ?? params.model,
|
|
@@ -139,6 +152,10 @@ export function resolveAgentInvocationConfig(
|
|
|
139
152
|
false,
|
|
140
153
|
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
141
154
|
isolation,
|
|
155
|
+
overridden:
|
|
156
|
+
overriddenThinking || overriddenModel
|
|
157
|
+
? { thinking: overriddenThinking, model: overriddenModel }
|
|
158
|
+
: undefined,
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
|
package/src/model-resolver.ts
CHANGED
|
@@ -14,6 +14,20 @@ export interface ModelRegistry {
|
|
|
14
14
|
getAvailable?(): any[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Both display forms of a resolved model. */
|
|
18
|
+
export function describeModel(model: {
|
|
19
|
+
provider: string
|
|
20
|
+
id: string
|
|
21
|
+
name?: string
|
|
22
|
+
}): { modelName: string; modelId: string } {
|
|
23
|
+
return {
|
|
24
|
+
modelName: (model.name ?? model.id)
|
|
25
|
+
.replace(/^Claude\s+/i, "")
|
|
26
|
+
.toLowerCase(),
|
|
27
|
+
modelId: `${model.provider}/${model.id}`,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Resolve a model string to a Model instance.
|
|
19
33
|
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
package/src/schedule.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import { Cron } from "croner"
|
|
23
23
|
import { nanoid } from "nanoid"
|
|
24
24
|
import type { AgentManager } from "./agent-manager.js"
|
|
25
|
+
import { normalizeMaxTurns } from "./agent-runner.js"
|
|
25
26
|
import { resolveSpawnType } from "./agent-types.js"
|
|
26
27
|
import { resolveModel } from "./model-resolver.js"
|
|
27
28
|
import type { ScheduleStore } from "./schedule-store.js"
|
|
@@ -280,6 +281,13 @@ export class SubagentScheduler {
|
|
|
280
281
|
isolated: job.isolated,
|
|
281
282
|
thinkingLevel: job.thinking,
|
|
282
283
|
isolation: job.isolation,
|
|
284
|
+
invocation: {
|
|
285
|
+
thinking: job.thinking,
|
|
286
|
+
maxTurns: normalizeMaxTurns(job.max_turns),
|
|
287
|
+
isolated: job.isolated,
|
|
288
|
+
runInBackground: true,
|
|
289
|
+
isolation: job.isolation,
|
|
290
|
+
},
|
|
283
291
|
})
|
|
284
292
|
} catch (err) {
|
|
285
293
|
const error = err instanceof Error ? err.message : String(err)
|
package/src/settings.ts
CHANGED
|
@@ -6,10 +6,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
6
6
|
import { dirname, join } from "node:path"
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent"
|
|
8
8
|
import { NO_FALLBACK } from "./agent-types.js"
|
|
9
|
-
import type { JoinMode, WidgetMode } from "./types.js"
|
|
9
|
+
import type { JoinMode, ViewerMarkdownMode, WidgetMode } from "./types.js"
|
|
10
10
|
|
|
11
11
|
export interface SubagentsSettings {
|
|
12
12
|
maxConcurrent?: number
|
|
13
|
+
/** Max concurrent blocking agents. 0 = unlimited (default). */
|
|
14
|
+
maxConcurrentForeground?: number
|
|
13
15
|
/**
|
|
14
16
|
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
15
17
|
* `normalizeMaxTurns()` in agent-runner.ts treats 0 → `undefined`, and the
|
|
@@ -200,6 +202,10 @@ export interface SubagentsSettings {
|
|
|
200
202
|
* what the parent session counts.
|
|
201
203
|
*/
|
|
202
204
|
showCost?: boolean
|
|
205
|
+
/** Show effective model and thinking level in running widget rows. */
|
|
206
|
+
showModel?: boolean
|
|
207
|
+
/** Markdown scope in the conversation viewer. Defaults to assistant. */
|
|
208
|
+
viewerMarkdown?: ViewerMarkdownMode
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export type ToolDescriptionMode = "full" | "compact" | "custom"
|
|
@@ -207,6 +213,7 @@ export type ToolDescriptionMode = "full" | "compact" | "custom"
|
|
|
207
213
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
208
214
|
export interface SettingsAppliers {
|
|
209
215
|
setMaxConcurrent: (n: number) => void
|
|
216
|
+
setMaxConcurrentForeground: (n: number) => void
|
|
210
217
|
setDefaultMaxTurns: (n: number) => void
|
|
211
218
|
setGraceTurns: (n: number) => void
|
|
212
219
|
setDefaultJoinMode: (mode: JoinMode) => void
|
|
@@ -224,6 +231,8 @@ export interface SettingsAppliers {
|
|
|
224
231
|
setFallbackSubagent: (v: string | undefined) => void
|
|
225
232
|
setReportUsage: (b: boolean) => void
|
|
226
233
|
setShowCost: (b: boolean) => void
|
|
234
|
+
setShowModel: (b: boolean) => void
|
|
235
|
+
setViewerMarkdown: (mode: ViewerMarkdownMode) => void
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
@@ -241,6 +250,8 @@ const VALID_WIDGET_MODES: ReadonlySet<string> = new Set<WidgetMode>([
|
|
|
241
250
|
"background",
|
|
242
251
|
"off",
|
|
243
252
|
])
|
|
253
|
+
const VALID_VIEWER_MARKDOWN_MODES: ReadonlySet<string> =
|
|
254
|
+
new Set<ViewerMarkdownMode>(["off", "assistant", "all"])
|
|
244
255
|
|
|
245
256
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
246
257
|
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
@@ -262,6 +273,13 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
262
273
|
) {
|
|
263
274
|
out.maxConcurrent = r.maxConcurrent as number
|
|
264
275
|
}
|
|
276
|
+
if (
|
|
277
|
+
Number.isInteger(r.maxConcurrentForeground) &&
|
|
278
|
+
(r.maxConcurrentForeground as number) >= 0 &&
|
|
279
|
+
(r.maxConcurrentForeground as number) <= MAX_CONCURRENT_CEILING
|
|
280
|
+
) {
|
|
281
|
+
out.maxConcurrentForeground = r.maxConcurrentForeground as number
|
|
282
|
+
}
|
|
265
283
|
if (
|
|
266
284
|
Number.isInteger(r.defaultMaxTurns) &&
|
|
267
285
|
(r.defaultMaxTurns as number) >= 0 &&
|
|
@@ -331,6 +349,15 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
331
349
|
if (typeof r.showCost === "boolean") {
|
|
332
350
|
out.showCost = r.showCost
|
|
333
351
|
}
|
|
352
|
+
if (typeof r.showModel === "boolean") {
|
|
353
|
+
out.showModel = r.showModel
|
|
354
|
+
}
|
|
355
|
+
if (
|
|
356
|
+
typeof r.viewerMarkdown === "string" &&
|
|
357
|
+
VALID_VIEWER_MARKDOWN_MODES.has(r.viewerMarkdown)
|
|
358
|
+
) {
|
|
359
|
+
out.viewerMarkdown = r.viewerMarkdown as ViewerMarkdownMode
|
|
360
|
+
}
|
|
334
361
|
if (r.fallbackSubagent === false) {
|
|
335
362
|
// The only non-string spelling worth accepting: a boolean would otherwise be
|
|
336
363
|
// dropped, silently leaving the PERMISSIVE default in place. Every string is
|
|
@@ -407,6 +434,8 @@ export function applySettings(
|
|
|
407
434
|
): void {
|
|
408
435
|
if (typeof s.maxConcurrent === "number")
|
|
409
436
|
appliers.setMaxConcurrent(s.maxConcurrent)
|
|
437
|
+
if (typeof s.maxConcurrentForeground === "number")
|
|
438
|
+
appliers.setMaxConcurrentForeground(s.maxConcurrentForeground)
|
|
410
439
|
if (typeof s.defaultMaxTurns === "number")
|
|
411
440
|
appliers.setDefaultMaxTurns(s.defaultMaxTurns)
|
|
412
441
|
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns)
|
|
@@ -434,6 +463,8 @@ export function applySettings(
|
|
|
434
463
|
appliers.setWorktreeIsolation(s.worktreeIsolation)
|
|
435
464
|
if (typeof s.reportUsage === "boolean") appliers.setReportUsage(s.reportUsage)
|
|
436
465
|
if (typeof s.showCost === "boolean") appliers.setShowCost(s.showCost)
|
|
466
|
+
if (typeof s.showModel === "boolean") appliers.setShowModel(s.showModel)
|
|
467
|
+
if (s.viewerMarkdown) appliers.setViewerMarkdown(s.viewerMarkdown)
|
|
437
468
|
}
|
|
438
469
|
|
|
439
470
|
/**
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,9 @@ export type JoinMode = "async" | "group" | "smart"
|
|
|
105
105
|
*/
|
|
106
106
|
export type WidgetMode = "all" | "background" | "off"
|
|
107
107
|
|
|
108
|
+
/** How much of the conversation viewer renders as Markdown. */
|
|
109
|
+
export type ViewerMarkdownMode = "off" | "assistant" | "all"
|
|
110
|
+
|
|
108
111
|
export interface AgentRecord {
|
|
109
112
|
id: string
|
|
110
113
|
type: SubagentType
|
|
@@ -125,6 +128,10 @@ export interface AgentRecord {
|
|
|
125
128
|
session?: AgentSession
|
|
126
129
|
abortController?: AbortController
|
|
127
130
|
promise?: Promise<string>
|
|
131
|
+
/** Whether a caller is awaiting this agent inline. */
|
|
132
|
+
blocking?: boolean
|
|
133
|
+
/** Resolves when a queued blocking agent starts or is removed from the queue. */
|
|
134
|
+
startGate?: Promise<void>
|
|
128
135
|
groupId?: string
|
|
129
136
|
joinMode?: JoinMode
|
|
130
137
|
/** Set when result was already consumed via get_subagent_result — suppresses completion notification. */
|
|
@@ -176,10 +183,18 @@ export interface AgentRecord {
|
|
|
176
183
|
rootSessionId?: string
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/** Effective session level, including pi's display-only `off` value. */
|
|
187
|
+
export type EffectiveThinkingLevel = ThinkingLevel | "off"
|
|
188
|
+
|
|
179
189
|
export interface AgentInvocation {
|
|
180
|
-
/** Short
|
|
190
|
+
/** Short model label for tight UI rows. */
|
|
181
191
|
modelName?: string
|
|
182
|
-
|
|
192
|
+
/** Canonical provider/model id. */
|
|
193
|
+
modelId?: string
|
|
194
|
+
thinking?: EffectiveThinkingLevel
|
|
195
|
+
/** Requested values retained only when the run used something else. */
|
|
196
|
+
requestedThinking?: EffectiveThinkingLevel
|
|
197
|
+
requestedModel?: string
|
|
183
198
|
maxTurns?: number
|
|
184
199
|
isolated?: boolean
|
|
185
200
|
inheritContext?: boolean
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -226,18 +226,31 @@ export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
|
226
226
|
/** Mode label is not included — callers add it where they want it. */
|
|
227
227
|
export function buildInvocationTags(invocation: AgentInvocation | undefined): {
|
|
228
228
|
modelName?: string
|
|
229
|
+
modelId?: string
|
|
229
230
|
tags: string[]
|
|
230
231
|
} {
|
|
231
232
|
const tags: string[] = []
|
|
232
233
|
if (!invocation) return { tags }
|
|
233
|
-
|
|
234
|
+
const asked = (
|
|
235
|
+
value: string | undefined,
|
|
236
|
+
requested: string | undefined,
|
|
237
|
+
): string | undefined =>
|
|
238
|
+
value && requested && requested !== value
|
|
239
|
+
? `${value} (asked ${requested})`
|
|
240
|
+
: value
|
|
241
|
+
const thinking = asked(invocation.thinking, invocation.requestedThinking)
|
|
242
|
+
if (thinking) tags.push(`thinking: ${thinking}`)
|
|
234
243
|
if (invocation.isolated) tags.push("isolated")
|
|
235
244
|
if (invocation.isolation === "worktree") tags.push("worktree")
|
|
236
245
|
if (invocation.inheritContext) tags.push("inherit context")
|
|
237
246
|
if (invocation.runInBackground) tags.push("background")
|
|
238
247
|
if (invocation.maxTurns != null)
|
|
239
248
|
tags.push(`max turns: ${invocation.maxTurns}`)
|
|
240
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
modelName: asked(invocation.modelName, invocation.requestedModel),
|
|
251
|
+
modelId: asked(invocation.modelId, invocation.requestedModel),
|
|
252
|
+
tags,
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
/** Truncate text to a single line, max `len` chars. */
|
|
@@ -317,6 +330,8 @@ export class AgentWidget {
|
|
|
317
330
|
* supplies the user's `showCost` setting.
|
|
318
331
|
*/
|
|
319
332
|
private showCost: () => boolean = () => false,
|
|
333
|
+
/** Whether running rows show the model and thinking level. */
|
|
334
|
+
private showModel: () => boolean = () => false,
|
|
320
335
|
) {}
|
|
321
336
|
|
|
322
337
|
/**
|
|
@@ -526,6 +541,12 @@ export class AgentWidget {
|
|
|
526
541
|
: ""
|
|
527
542
|
|
|
528
543
|
const parts: string[] = []
|
|
544
|
+
if (this.showModel()) {
|
|
545
|
+
const { modelName, tags } = buildInvocationTags(a.invocation)
|
|
546
|
+
if (modelName) parts.push(modelName)
|
|
547
|
+
const thinkingTag = tags.find((tag) => tag.startsWith("thinking: "))
|
|
548
|
+
if (thinkingTag) parts.push(thinkingTag)
|
|
549
|
+
}
|
|
529
550
|
if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns))
|
|
530
551
|
if (toolUses > 0)
|
|
531
552
|
parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`)
|