Skip to content

Commit 4fecd09

Browse files
AbhiShake1claude
andcommitted
feat(api): phase(name, { title }, body) overload for static titles
When the title is known at phase-creation time (which is most of the time — harness code almost always knows what a phase does upfront), the runtime setTitle path from 0.1.12 is overkill. New overload: await phase('pipeline', { title: 'Ingest & transform' }, async () => { … }); Engine's stage() signature accepts either (id, body) or (id, opts, body), opts = { title?: string }. The stage-enter event carries the static title, so the TUI renders it on the node's very first frame without waiting for a follow-up stage-title event. Runtime ctx.setTitle() still works — if both are used, the dynamic override wins. Backward compat: every existing `phase(name, body)` keeps working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 513ee46 commit 4fecd09

6 files changed

Lines changed: 55 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.13] - 2026-04-19
9+
10+
### Added
11+
- `phase(name, { title }, body)` overload — static title at phase-creation time. Use when you know the title upfront; cleaner than the runtime `setTitle` path. Runtime `ctx.setTitle(...)` still works and overrides the static title. `stage-enter` event now carries an optional `title` field so the TUI sees the title on its first render.
12+
813
## [0.1.12] - 2026-04-19
914

1015
### Added

api/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ export interface PhaseCtx {
117117
setTitle: (title: string) => void;
118118
}
119119

120+
export interface PhaseOpts {
121+
/**
122+
* Display title known at phase-creation time. Rendered in the TUI instead
123+
* of the stripped id. Runtime `setTitle(...)` from the body overrides this.
124+
*/
125+
title?: string;
126+
}
127+
120128
export interface RunCtx {
121129
/**
122130
* Wraps `engineStage()` around the provided async body. The body's return
@@ -125,6 +133,10 @@ export interface RunCtx {
125133
* use to call `setTitle(...)` after some setup work.
126134
*/
127135
phase<T>(name: string, body: (ctx: PhaseCtx) => Promise<T>): Promise<T>;
136+
/**
137+
* Overload: static title passed at phase-creation time.
138+
*/
139+
phase<T>(name: string, opts: PhaseOpts, body: (ctx: PhaseCtx) => Promise<T>): Promise<T>;
128140

129141
/**
130142
* Invoke one agent session. Returns a promise that resolves to the LLM's
@@ -271,9 +283,15 @@ export function taskflow(name: string): TaskflowBuilder {
271283

272284
return engineHarness(name, harnessOpts, async (h) => {
273285
const ctx: RunCtx = {
274-
async phase<T>(phaseName: string, phaseBody: (ctx: PhaseCtx) => Promise<T>): Promise<T> {
286+
async phase<T>(
287+
phaseName: string,
288+
optsOrBody: PhaseOpts | ((ctx: PhaseCtx) => Promise<T>),
289+
maybeBody?: (ctx: PhaseCtx) => Promise<T>,
290+
): Promise<T> {
291+
const phaseBody = typeof optsOrBody === 'function' ? optsOrBody : maybeBody!;
292+
const opts: PhaseOpts = typeof optsOrBody === 'function' ? {} : optsOrBody;
275293
let result!: T;
276-
await engineStage(h, phaseName, async (stageCtx) => {
294+
await engineStage(h, phaseName, opts, async (stageCtx) => {
277295
result = await phaseBody({ setTitle: stageCtx.setTitle });
278296
});
279297
return result;

core/index.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,24 @@ export interface StageCtx {
227227
setTitle: (title: string) => void;
228228
}
229229

230-
export async function stage(h: Ctx, id: string, body: (ctx: StageCtx) => Promise<void> | (() => Promise<void>)): Promise<void> {
230+
export interface StageOpts {
231+
/**
232+
* Display title for the TUI, known at phase-creation time. When provided,
233+
* rendered in place of the stripped id. Runtime-set `setTitle(...)` calls
234+
* from the body override this if both are used.
235+
*/
236+
title?: string;
237+
}
238+
239+
export async function stage(
240+
h: Ctx,
241+
id: string,
242+
optsOrBody: StageOpts | ((ctx: StageCtx) => Promise<void>),
243+
maybeBody?: (ctx: StageCtx) => Promise<void>,
244+
): Promise<void> {
245+
const body = typeof optsOrBody === 'function' ? optsOrBody : maybeBody!;
246+
const opts: StageOpts = typeof optsOrBody === 'function' ? {} : optsOrBody;
247+
231248
const parentId = h.stageStack[h.stageStack.length - 1];
232249
h.stageStack.push(id);
233250
h._stageOrder.push(id);
@@ -239,7 +256,14 @@ export async function stage(h: Ctx, id: string, body: (ctx: StageCtx) => Promise
239256
if (fireBefore) {
240257
await hooks!.fire('beforePhase', buildHookCtx(h, { hookName: 'beforePhase', phaseScope: { id, stack: h.stageStack.slice() } }), { phaseId: id, parentId });
241258
}
242-
h.bus.publish({ t: 'stage-enter', stageId: id, parentId, ts: Date.now() });
259+
const enterEvent: { t: 'stage-enter'; stageId: string; parentId?: string; title?: string; ts: number } = {
260+
t: 'stage-enter',
261+
stageId: id,
262+
parentId,
263+
ts: Date.now(),
264+
};
265+
if (opts.title !== undefined) enterEvent.title = opts.title;
266+
h.bus.publish(enterEvent);
243267

244268
const stageCtx: StageCtx = {
245269
setTitle: (title: string) => {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "taskflow-sdk",
3-
"version": "0.1.12",
3+
"version": "0.1.13",
44
"description": "Multi-agent orchestration harness for AI coding agents (claude-code, codex, cursor, opencode, pi). Async-await TypeScript API; lifecycle hooks; auto-todos with verify-loop.",
55
"keywords": [
66
"ai",

tui/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function createTuiStore(
108108
startedAt: ev.ts,
109109
children: [],
110110
leafEvents: [],
111+
...(ev.title !== undefined ? { title: ev.title } : {}),
111112
};
112113
nodes[ev.stageId] = node;
113114
if (parentId && nodes[parentId]) {

0 commit comments

Comments
 (0)