Merge remote-tracking branch 'origin/master' into feat/scrollbar-tokens

This commit is contained in:
Chinesezjc
2026-07-28 18:15:00 +08:00
176 changed files with 4427 additions and 842 deletions

View File

@@ -227,7 +227,15 @@ export class CommandService extends Service implements CommandServiceContract {
}
}
/** The command.execute transaction, addressed to the session's agent. */
/**
* The command.execute transaction, addressed to the session's agent — pure
* admission semantics. An unmatched line reports an error outcome (the
* composer's immediate admission feedback); an admitted command reports
* plain success regardless of its handler outcome, because the host
* executor durably logged the lifecycle (`command/run`/`command/done`) and
* the outcome renders as a persistent flow node — the composer never
* echoes it. Transport failures throw.
*/
private async execute(
session: ClientSessionContext,
line: string,
@@ -236,25 +244,25 @@ export class CommandService extends Service implements CommandServiceContract {
const { result } = await connection.api.commands.execute({ sessionId: session.sessionId, line })
if (!result.ok) throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`)
if (!result.value.matched) return { kind: 'error', text: `unknown or malformed command: ${line}` }
const detached = result.value.result
return detached === undefined
? { kind: 'success' }
: { kind: detached.kind, ...(detached.text !== undefined ? { text: detached.text } : {}) }
return { kind: 'success' }
}
/**
* Fire-and-forget execute for the internal ('handled') paths. The detached
* result surfaces as a notice routed to the triggering session's composer,
* so a late result lands on its own session after a switch.
* Fire-and-forget execute for the internal ('handled') paths. Outcomes are
* NOT surfaced here: the host executor durably logs the command lifecycle
* (`command/run`/`command/done`), and the mux-broadcast events render as a
* persistent flow node on every tab. Only a transport/admission failure —
* which never entered a handler and therefore never logged — falls back to
* the composer notice as immediate feedback.
*/
private runDetached(desc: CommandDescriptor, session: ClientSessionContext, line: string): void {
void this.execute(session, line).then(
(outcome) => {
if (outcome.kind === 'error') this.noticeFor(session.sessionId, desc.name, 'error', outcome.text ?? `/${desc.name} failed`)
else if (outcome.text !== undefined) this.noticeFor(session.sessionId, desc.name, 'info', outcome.text)
// matched:false maps to an error outcome with no logged lifecycle.
if (outcome.kind === 'error') this.noticeFor(session.sessionId, 'error', outcome.text ?? `/${desc.name} failed`)
},
(error: unknown) => {
this.noticeFor(session.sessionId, desc.name, 'error', error instanceof Error ? error.message : String(error))
this.noticeFor(session.sessionId, 'error', error instanceof Error ? error.message : String(error))
},
)
}
@@ -270,8 +278,8 @@ export class CommandService extends Service implements CommandServiceContract {
})
}
/** Route a detached result to the session's composer notice channel (scope gone = attempt died with it). */
private noticeFor(id: SessionId, _name: string, level: 'info' | 'error', text: string): void {
/** Route an admission/transport failure to the session's composer notice channel (scope gone = attempt died with it). */
private noticeFor(id: SessionId, level: 'info' | 'error', text: string): void {
const actx = this.scopeFor(id)
if (actx === undefined) return
const conversation = actx.get('conversation')