Skip to content

feat(task): auto-flatten subtasks inline when the nesting limit is reached - #12

Merged
easonLiangWorldedtech merged 1 commit into
main-task-treefrom
pr-3-auto-flatten-inline
Aug 16, 2026
Merged

feat(task): auto-flatten subtasks inline when the nesting limit is reached#12
easonLiangWorldedtech merged 1 commit into
main-task-treefrom
pr-3-auto-flatten-inline

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Owner

feat(task): auto-flatten subtasks inline when the nesting limit is reached

Part of the Task Tree unified subtask management plan (issue #9, P1.3). When a new_task call would exceed maxNestingDepth, it no longer opens a child tab — it runs inline in the current conversation as an execution phase of the same Task instance.

Design: inline = same Task instance + in-memory phase marker (no virtual child)

The repo has three hard constraints that rule out a "virtual child Task":

  1. single-open invariant (removeClineFromStack) — parent leaves the stack before a child tab opens
  2. handleModeSwitch is provider-global state; two Tasks can't each hold a mode
  3. an unopened real Task instance violates both of the above

So an inline subtask is the parent's own execution phase: no approval popup, no child Task, no new tab — the tool_result doubles as the inline prompt.

model calls new_task(message, mode)
        │
NewTaskTool.execute():
  decision = decideInlineFlatten({childDepth: task.depth + 1, maxNestingDepth, autoFlattenOnLimit, ...})
  ├─ reject-nested   → nested new_task while an inline phase is active → error tool_result
  ├─ flatten         → task.inlineSubtask = {message, todos}; pushToolResult(directive); return (no askApproval)
  ├─ reject-limit    → autoFlattenOnLimit=false → error tool_result "continue working directly"
  └─ delegate        → existing flow unchanged (askApproval → delegateParentAndOpenChild)

attempt_completion during an inline phase clears the marker and pushes a [inline subtask completed] tool_result — no askFinishSubTaskApproval popup (the user is already watching this same conversation; a second approval would be double friction). The loop naturally continues to the next API call. Cancel/abort clears the marker so the task resumes as an ordinary parent.

Changes

File Change
src/core/tools/inlineSubtask.ts (new) Pure decision function decideInlineFlatten() + directive builder — unit-testable without Task/vscode
src/core/task/Task.ts InlineSubtask interface + non-persisted inlineSubtask? phase marker; cleared in abortTask()
src/core/tools/NewTaskTool.ts Depth check before askApproval; flatten/reject branches per the decision; delegate path unchanged
src/core/tools/AttemptCompletionTool.ts Inline-phase completion branch (clear marker, no approval, loop continues) before the delegation flow

Behavior guarantees

  • Within limit → existing tab-based delegation is completely unchanged (approval + child open)
  • Over limit + autoFlattenOnLimit: true → inline phase; zero synthetic messages (the parent's own conversation IS the bounded context); no resumeAfterDelegation() needed since the parent never left its task loop
  • Over limit + autoFlattenOnLimit: false → rejected with an error result so work continues directly
  • Nested new_task during an inline phase → rejected (P1 forbids recursion into a second inline subtask)
  • Cancel mid-inline-phase → marker cleared, conversation continues as ordinary parent; no orphan child, no cancellation cascade
  • Inline state is not persisted — it's a transient execution phase, not lineage

Tests (~450 lines per plan)

  • core/tools/__tests__/inlineSubtask.spec.ts (unit, 11 cases): delegate within/at limit; flatten over-limit with directive content + todos; reject-limit when auto-flatten disabled; maxNestingDepth=0 → always inline; nested rejection precedence (active phase wins even when over limit); todo rendering in/out
  • core/tools/__tests__/newTaskInlineFlatten.spec.ts (integration, 6 cases): delegate path unchanged (approval + child opened); flatten path (no approval, no child, marker set, directive as tool_result); reject-limit error; nested rejection preserves existing marker; inline attempt_completion clears marker with no askFinishSubTaskApproval call and pushes the continue result; non-inline completion does not take the inline branch
  • Regression: new-task-delegation, provider-delegation, nested-delegation-resume, Task.spec.ts all pass unchanged

Verification

  • pnpm run check-types — 14/14 packages pass
  • New suites: 17/17 (unit 11 + integration 6); regression suites: 26/26 incl. Task.spec 96/96
  • Full turbo lint (--max-warnings=0) clean; zero eslint suppression count increases (test doubles use a single documented as unknown as Task cast, matching the existing new-task-delegation.spec.ts pattern)

Out of scope (later PRs per issue #9)

Webview inline-phase indicator + cancel cascade (PR-4); manual conversation checkpoints (PR-5). Escalation stays user-manual in P2; no AI-driven heuristics.

@easonLiangWorldedtech
easonLiangWorldedtech merged commit f2e7003 into main-task-tree Aug 16, 2026
6 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants