@herbertgao/pi-subagents 0.16.0 → 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/src/index.ts CHANGED
@@ -77,7 +77,11 @@ import {
77
77
  resolveAgentInvocationConfig,
78
78
  resolveJoinMode,
79
79
  } from "./invocation-config.js"
80
- import { type ModelRegistry, resolveModel } from "./model-resolver.js"
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
@@ -890,6 +912,20 @@ export default function (pi: ExtensionAPI) {
890
912
  const record = manager.getRecord(id)
891
913
  return !record?.parentAgentId && manager.abort(id)
892
914
  },
915
+ consumeResult: (id) => {
916
+ const record = manager.getRecord(id)
917
+ if (
918
+ !record ||
919
+ record.parentAgentId ||
920
+ record.status === "running" ||
921
+ record.status === "queued"
922
+ ) {
923
+ return false
924
+ }
925
+ record.resultConsumed = true
926
+ cancelNudge(record.id)
927
+ return true
928
+ },
893
929
  },
894
930
  })
895
931
  // Broadcast readiness so extensions loaded alongside us can discover us.
@@ -911,6 +947,7 @@ export default function (pi: ExtensionAPI) {
911
947
  rpcHandle?.unsubSpawn()
912
948
  rpcHandle?.unsubStop()
913
949
  rpcHandle?.unsubPing()
950
+ rpcHandle?.unsubConsume()
914
951
  rpcHandle = undefined
915
952
  currentCtx = undefined
916
953
  // Only release the global slot if this activation claimed it — a child
@@ -943,6 +980,7 @@ export default function (pi: ExtensionAPI) {
943
980
  agentActivity,
944
981
  getWidgetMode,
945
982
  isShowCostEnabled,
983
+ isShowModelEnabled,
946
984
  )
947
985
  function setWidgetMode(m: WidgetMode): void {
948
986
  widgetMode = m
@@ -1068,6 +1106,7 @@ export default function (pi: ExtensionAPI) {
1068
1106
  // Grab UI context from first tool execution + clear lingering widget on new turn
1069
1107
  pi.on("tool_execution_start", async (_event, ctx) => {
1070
1108
  widget.setUICtx(ctx.ui as UICtx)
1109
+ // SAFETY: both UI adapters receive the same Pi ExtensionContext UI surface.
1071
1110
  fleet.setUICtx(ctx.ui as unknown as FleetUICtx)
1072
1111
  widget.onTurnStart()
1073
1112
  })
@@ -1117,6 +1156,7 @@ export default function (pi: ExtensionAPI) {
1117
1156
  applyAndEmitLoaded(
1118
1157
  {
1119
1158
  setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
1159
+ setMaxConcurrentForeground: (n) => manager.setMaxConcurrentForeground(n),
1120
1160
  setDefaultMaxTurns,
1121
1161
  setGraceTurns,
1122
1162
  setDefaultJoinMode,
@@ -1136,6 +1176,8 @@ export default function (pi: ExtensionAPI) {
1136
1176
  setFallbackSubagent: setFallbackSubagent,
1137
1177
  setReportUsage,
1138
1178
  setShowCost,
1179
+ setShowModel,
1180
+ setViewerMarkdown,
1139
1181
  },
1140
1182
  (event, payload) => pi.events.emit(event, payload),
1141
1183
  )
@@ -1621,20 +1663,27 @@ Terse command-style prompts produce shallow, generic work.
1621
1663
  writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd)
1622
1664
  }
1623
1665
 
1624
- const parentModelId = ctx.model?.id
1625
- const effectiveModelId = model?.id
1626
- const modelName =
1627
- effectiveModelId && effectiveModelId !== parentModelId
1628
- ? (model?.name ?? effectiveModelId)
1629
- .replace(/^Claude\s+/i, "")
1630
- .toLowerCase()
1631
- : undefined
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)
1632
1678
  const effectiveMaxTurns = normalizeMaxTurns(
1633
1679
  resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
1634
1680
  )
1635
1681
  const agentInvocation: AgentInvocation = {
1636
1682
  modelName,
1683
+ modelId,
1637
1684
  thinking,
1685
+ requestedThinking: resolvedConfig.overridden?.thinking,
1686
+ requestedModel: askedModel,
1638
1687
  // Explicit value only — the default fallback would just add noise.
1639
1688
  // Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
1640
1689
  maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
@@ -1656,6 +1705,23 @@ Terse command-style prompts produce shallow, generic work.
1656
1705
  modelName,
1657
1706
  tags: agentTags.length > 0 ? agentTags : undefined,
1658
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
+ }
1659
1725
 
1660
1726
  // ---- Schedule: register a job, don't spawn now ----
1661
1727
  if (params.schedule) {
@@ -1849,9 +1915,7 @@ Terse command-style prompts produce shallow, generic work.
1849
1915
  `\nYou will be notified when this agent completes.\n` +
1850
1916
  `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
1851
1917
  {
1852
- ...detailBase,
1853
- subagentType: existing.type,
1854
- displayName: existing.type,
1918
+ ...detailBaseFor(record),
1855
1919
  toolUses: record.toolUses,
1856
1920
  tokens: "",
1857
1921
  durationMs: 0,
@@ -1874,12 +1938,12 @@ Terse command-style prompts produce shallow, generic work.
1874
1938
  if (record.status === "error") {
1875
1939
  return textResult(
1876
1940
  `Agent failed: ${record.error}${partialOutputSuffix(record)}`,
1877
- buildDetails(detailBase, record),
1941
+ buildDetails(detailBaseFor(record), record),
1878
1942
  )
1879
1943
  }
1880
1944
  return textResult(
1881
1945
  record.result?.trim() || "No output.",
1882
- buildDetails(detailBase, record),
1946
+ buildDetails(detailBaseFor(record), record),
1883
1947
  )
1884
1948
  }
1885
1949
 
@@ -1974,7 +2038,7 @@ Terse command-style prompts produce shallow, generic work.
1974
2038
  `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
1975
2039
  `Do not duplicate this agent's work.`,
1976
2040
  {
1977
- ...detailBase,
2041
+ ...detailBaseFor(record),
1978
2042
  toolUses: 0,
1979
2043
  tokens: "",
1980
2044
  durationMs: 0,
@@ -1988,10 +2052,11 @@ Terse command-style prompts produce shallow, generic work.
1988
2052
  let spinnerFrame = 0
1989
2053
  const startedAt = Date.now()
1990
2054
  let fgId: string | undefined
2055
+ let queuedAhead: number | undefined
1991
2056
 
1992
2057
  const streamUpdate = () => {
1993
2058
  const details: AgentDetails = {
1994
- ...detailBase,
2059
+ ...detailBaseFor(fgId ? manager.getRecord(fgId) : undefined),
1995
2060
  toolUses: fgState.toolUses,
1996
2061
  tokens: fgId ? formatLifetimeTokens(manager.getRecord(fgId)!) : "",
1997
2062
  cost: fgId
@@ -2001,10 +2066,10 @@ Terse command-style prompts produce shallow, generic work.
2001
2066
  maxTurns: fgState.maxTurns,
2002
2067
  durationMs: Date.now() - startedAt,
2003
2068
  status: "running",
2004
- activity: describeActivity(
2005
- fgState.activeTools,
2006
- fgState.responseText,
2007
- ),
2069
+ activity:
2070
+ queuedAhead === undefined
2071
+ ? describeActivity(fgState.activeTools, fgState.responseText)
2072
+ : `queued — waiting for a foreground slot${queuedAhead > 0 ? ` (${queuedAhead} ahead)` : ""}`,
2008
2073
  spinnerFrame: spinnerFrame % SPINNER.length,
2009
2074
  }
2010
2075
  onUpdate?.({
@@ -2024,6 +2089,10 @@ Terse command-style prompts produce shallow, generic work.
2024
2089
  const origOnSession = fgCallbacks.onSessionCreated
2025
2090
  fgCallbacks.onSessionCreated = (session: any) => {
2026
2091
  origOnSession(session)
2092
+ if (queuedAhead !== undefined) {
2093
+ queuedAhead = undefined
2094
+ streamUpdate()
2095
+ }
2027
2096
  for (const a of manager.listAgents()) {
2028
2097
  if (a.session === session) {
2029
2098
  fgId = a.id
@@ -2074,6 +2143,10 @@ Terse command-style prompts produce shallow, generic work.
2074
2143
  invocation: agentInvocation,
2075
2144
  signal,
2076
2145
  rootSessionId: ctx.sessionManager.getSessionId(),
2146
+ onQueued: (_id, ahead) => {
2147
+ queuedAhead = ahead
2148
+ streamUpdate()
2149
+ },
2077
2150
  ...fgCallbacks,
2078
2151
  },
2079
2152
  (fgAgentId) => {
@@ -2098,7 +2171,7 @@ Terse command-style prompts produce shallow, generic work.
2098
2171
  // Get final token count
2099
2172
  const tokenText = formatLifetimeTokens(record)
2100
2173
 
2101
- const details = buildDetails(detailBase, record, fgState, {
2174
+ const details = buildDetails(detailBaseFor(record), record, fgState, {
2102
2175
  tokens: tokenText,
2103
2176
  })
2104
2177
 
@@ -2537,6 +2610,11 @@ Terse command-style prompts produce shallow, generic work.
2537
2610
  keybindings,
2538
2611
  (message: string) => manager.steer(record.id, message),
2539
2612
  showCost,
2613
+ getViewerMarkdown,
2614
+ (mode) => {
2615
+ setViewerMarkdown(mode)
2616
+ persistSettings(ctx, `Viewer markdown set to ${mode}`)
2617
+ },
2540
2618
  )
2541
2619
  },
2542
2620
  {
@@ -2828,6 +2906,7 @@ Write the file using the write tool. Only write the file, nothing else.`
2828
2906
  {
2829
2907
  description: `Generate ${name} agent`,
2830
2908
  maxTurns: 5,
2909
+ bypassQueue: true,
2831
2910
  },
2832
2911
  )
2833
2912
 
@@ -2952,6 +3031,7 @@ Write the file using the write tool. Only write the file, nothing else.`
2952
3031
  function snapshotSettings() {
2953
3032
  return {
2954
3033
  maxConcurrent: manager.getMaxConcurrent(),
3034
+ maxConcurrentForeground: manager.getMaxConcurrentForeground(),
2955
3035
  // 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
2956
3036
  // normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
2957
3037
  defaultMaxTurns: getDefaultMaxTurns() ?? 0,
@@ -2975,6 +3055,8 @@ Write the file using the write tool. Only write the file, nothing else.`
2975
3055
  fallbackSubagent: getFallbackSubagent(),
2976
3056
  reportUsage: isReportUsageEnabled(),
2977
3057
  showCost: isShowCostEnabled(),
3058
+ showModel: isShowModelEnabled(),
3059
+ viewerMarkdown: getViewerMarkdown(),
2978
3060
  } satisfies SubagentsSettings
2979
3061
  }
2980
3062
 
@@ -2994,6 +3076,7 @@ Write the file using the write tool. Only write the file, nothing else.`
2994
3076
 
2995
3077
  const NUMERIC_IDS = new Set([
2996
3078
  "maxConcurrent",
3079
+ "maxConcurrentForeground",
2997
3080
  "defaultMaxTurns",
2998
3081
  "graceTurns",
2999
3082
  "maxSubagentDepth",
@@ -3002,6 +3085,7 @@ Write the file using the write tool. Only write the file, nothing else.`
3002
3085
  async function showSettings(ctx: ExtensionCommandContext) {
3003
3086
  function buildItems(): SettingItem[] {
3004
3087
  const mc = manager.getMaxConcurrent()
3088
+ const mcf = manager.getMaxConcurrentForeground()
3005
3089
  const dmt = getDefaultMaxTurns() ?? 0
3006
3090
  const gt = getGraceTurns()
3007
3091
  const msd = getMaxSubagentDepth()
@@ -3021,6 +3105,14 @@ Write the file using the write tool. Only write the file, nothing else.`
3021
3105
  currentValue: String(mc),
3022
3106
  values: [String(mc)],
3023
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
+ },
3024
3116
  {
3025
3117
  id: "defaultMaxTurns",
3026
3118
  label: "Default max turns",
@@ -3128,6 +3220,21 @@ Write the file using the write tool. Only write the file, nothing else.`
3128
3220
  currentValue: isShowCostEnabled() ? "on" : "off",
3129
3221
  values: ["on", "off"],
3130
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
+ },
3131
3238
  {
3132
3239
  id: "fleetView",
3133
3240
  label: "Fleet view",
@@ -3162,6 +3269,17 @@ Write the file using the write tool. Only write the file, nothing else.`
3162
3269
  manager.setMaxConcurrent(n)
3163
3270
  notifyApplied(ctx, `Max concurrency set to ${n}`)
3164
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
+ }
3165
3283
  } else if (id === "defaultMaxTurns") {
3166
3284
  const n = parseInt(value, 10)
3167
3285
  if (n === 0) {
@@ -3266,6 +3384,13 @@ Write the file using the write tool. Only write the file, nothing else.`
3266
3384
  const enabled = value === "on"
3267
3385
  setShowCost(enabled)
3268
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}`)
3269
3394
  } else if (id === "toolDescriptionMode") {
3270
3395
  setToolDescriptionMode(value as ToolDescriptionMode)
3271
3396
  notifyApplied(
@@ -3336,20 +3461,24 @@ Write the file using the write tool. Only write the file, nothing else.`
3336
3461
  const current =
3337
3462
  result === "maxConcurrent"
3338
3463
  ? String(manager.getMaxConcurrent())
3339
- : result === "defaultMaxTurns"
3340
- ? String(getDefaultMaxTurns() ?? 0)
3341
- : result === "maxSubagentDepth"
3342
- ? String(getMaxSubagentDepth())
3343
- : String(getGraceTurns())
3464
+ : result === "maxConcurrentForeground"
3465
+ ? String(manager.getMaxConcurrentForeground())
3466
+ : result === "defaultMaxTurns"
3467
+ ? String(getDefaultMaxTurns() ?? 0)
3468
+ : result === "maxSubagentDepth"
3469
+ ? String(getMaxSubagentDepth())
3470
+ : String(getGraceTurns())
3344
3471
 
3345
3472
  const label =
3346
3473
  result === "maxConcurrent"
3347
3474
  ? "Max concurrency (1+)"
3348
- : result === "defaultMaxTurns"
3349
- ? "Default max turns (0 = unlimited)"
3350
- : result === "maxSubagentDepth"
3351
- ? "Nested depth (0/1 = nesting off)"
3352
- : "Grace turns (1+)"
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+)"
3353
3482
 
3354
3483
  // Loop until user enters a valid integer or cancels (Esc / null).
3355
3484
  // Silently trims whitespace; rejects non-numeric input by re-prompting.
@@ -3372,6 +3501,18 @@ Write the file using the write tool. Only write the file, nothing else.`
3372
3501
  // the right toast. Successful saves show info; persistence failures downgrade
3373
3502
  // to warning so users aren't silently reverted on restart. Event fires regardless
3374
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
+
3375
3516
  function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
3376
3517
  const { message, level } = saveAndEmitChanged(
3377
3518
  snapshotSettings(),
@@ -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
 
@@ -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 display name, e.g. "haiku" only set when different from parent. */
190
+ /** Short model label for tight UI rows. */
181
191
  modelName?: string
182
- thinking?: ThinkingLevel
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