fix(tools,client): chain-mode resolution and a live preset label

modeFor read only the exact scope's layer, so a `code`-preset session
advertised the native catalog: the mode is declared on the preset's STANDING
scope, and the agent only parents to it. Nearest scope wins along the chain —
the mode decides what the model SEES, which is the class of fact the chain
inherits. Caught live (the model politely computed with bash and said
run_code was not in its list); the chain test pins it.

The client half of the label fix: the create echo and the session-added
frame's agentPreset now reach the session list (newest wins in the upsert —
every producer of the field reports the CURRENT composition), and a confirmed
blank-session switch publishes through the new ISessions.noteAgentPreset, so
the header label moves with the composition instead of waiting for a reload.
This commit is contained in:
Yichen Jiang
2026-08-08 20:19:00 +08:00
parent 3926e95c61
commit 66e9b01fbd
8 changed files with 81 additions and 2 deletions

View File

@@ -62,6 +62,15 @@ export interface ISessions {
* @returns completion of the current or newly started refresh.
*/
refreshSubagents(parentSessionId: SessionId): Promise<void>
/**
* Record the composition one session now runs. The agent-preset seat calls
* this after a successful blank-session switch, so the header label moves
* with the composition instead of waiting for the next full list refresh.
* @param sessionId - the switched session.
* @param agentPreset - the preset id the host confirmed.
*/
noteAgentPreset(sessionId: SessionId, agentPreset: string): void
/** Clear the current selection into the no-session view state. */
clear(): void
/**

View File

@@ -523,6 +523,7 @@ export class SessionManager {
this.recordMutation({ kind: 'upsert', summary: {
sessionId: result.value.sessionId, updatedAt: Date.now(), running: false, blank: true,
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
...(result.value.agentPreset !== undefined ? { agentPreset: result.value.agentPreset } : {}),
} })
} else {
const publishedSessionId = workspaceAttachSessionId(result.error)
@@ -588,6 +589,17 @@ export class SessionManager {
this.recordMutation({ kind: 'upsert', summary })
}
/**
* Record a host-confirmed composition switch (see ISessions.noteAgentPreset).
* @param sessionId - the switched session.
* @param agentPreset - the preset id the host confirmed.
*/
noteAgentPreset(sessionId: SessionId, agentPreset: string): void {
this.recordMutation({ kind: 'upsert', summary: {
sessionId, updatedAt: Date.now(), running: false, blank: true, agentPreset,
} })
}
/** Apply immediately and retain for replay when a list response is in flight. */
private recordMutation(mutation: SessionListMutation): void {
this.listMutations?.push(mutation)
@@ -743,6 +755,7 @@ export class SessionManager {
...(frame.parentSessionId !== undefined ? { parentSessionId: frame.parentSessionId } : {}),
...(frame.origin !== undefined ? { origin: frame.origin } : {}),
...(frame.cwd !== undefined ? { cwd: frame.cwd } : {}),
...(frame.agentPreset !== undefined ? { agentPreset: frame.agentPreset } : {}),
})
this.sessions.get(frame.sessionId)?.handleBlank(frame.blank)
if (frame.origin === 'subagent' && frame.parentSessionId !== undefined) {
@@ -1027,9 +1040,15 @@ function applyMutation(summaries: readonly SessionSummary[], mutation: SessionLi
? { parentSessionId: mutation.summary.parentSessionId } : {}),
...(existing.origin === undefined && mutation.summary.origin !== undefined
? { origin: mutation.summary.origin } : {}),
// Newest wins, not fill-only: a blank-session preset switch replaces
// the creation-time value, and every producer of this field (the
// create echo, the select echo, a list row) reports the CURRENT one.
...(mutation.summary.agentPreset !== undefined
? { agentPreset: mutation.summary.agentPreset } : {}),
}
if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId
&& filled.origin === existing.origin && filled.blank === existing.blank) return [...summaries]
&& filled.origin === existing.origin && filled.blank === existing.blank
&& filled.agentPreset === existing.agentPreset) return [...summaries]
return summaries.map(summary => summary.sessionId === mutation.summary.sessionId ? filled : summary)
}
case 'remove':

View File

@@ -365,6 +365,10 @@ export class SessionsService implements ISessions {
return this.manager.refreshSubagents(parentSessionId)
}
noteAgentPreset(sessionId: SessionId, agentPreset: string): void {
this.manager.noteAgentPreset(sessionId, agentPreset)
}
/**
* Clear the current selection so the layout shows the no-session empty
* state (new-session affordance and the workspace preselection flow).