Skip to content

feat(next-steps): agent-chosen next-step actions - #313

Draft
Rinat S (rsolmano) wants to merge 11 commits into
mainfrom
agent-driven-next-steps
Draft

feat(next-steps): agent-chosen next-step actions#313
Rinat S (rsolmano) wants to merge 11 commits into
mainfrom
agent-driven-next-steps

Conversation

@rsolmano

@rsolmano Rinat S (rsolmano) commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

The agent — not the browser — decides whether to offer optional ways to continue, and authors each one's short label plus the complete prompt it sends. Implements the design promoted in 874023a5.

packages/pi-next-steps — a standalone pi package

Publication-ready: pi.extensions manifest, pi-package keyword, peer deps on pi-coding-agent + typebox only, no ThinkRail imports. It registers the terminating offer_next_steps tool:

  • 1–3 { label, prompt } items, trimmed, ≤60 / ≤500 chars
  • blanks and case-insensitive duplicate labels or prompts rejected, with every message naming the tool so the model can correct itself
  • normalized details: { items } + a numbered plain-text fallback for hosts with no renderer
  • terminate: true, so the turn ends without paying for an empty follow-up model call
  • prompt metadata makes explicit requests for follow-up actions mandatory while ordinary turns stay omit-by-default; after any other tool result, a non-displayed ephemeral reminder renews that semantic check without parsing user prose or synthesizing options

Native pi. execute() never waits for a person — that's the restart invariant. In TUI mode agent_settled re-reads the branch, confirms a successful result is still the latest message, then opens pi's native selector. Choosing sends the prompt as a real user message, switching to a follow-up when another extension already started work; Escape leaves the offer intact. /next-steps reopens it, including after a resume — currency is always re-read from the session branch and never cached, so there is no reconstruction step and no stale in-memory offer to invalidate. The settle handler is detached on purpose: pi awaits agent_settled handlers before reporting idle, so a human-length selector inside one would stall the host. Non-TUI modes keep only the durable fallback.

Bundling

Added to the server's dependencies and source-mode resolution, and to the compiled-binary generator.

The generator now derives both the factory array and the skill roots from one declaration list where each entry states whether it ships skills. The previous [entryPaths[2], entryPaths[3], entryPaths[4]] indexing would have silently shifted every skill package when a sixth entry landed — and a wrong skill index fails either loudly (ENOENT) or invisibly.

Web

  • placement on the tool registry — "transcript" (default) or "composer".
  • deriveComposerTool (rows.ts, pure) — exposes the renderer only when the session is idle, the final meaningful assistant content ends with its successful call, and no later user/error turn exists. It reads past a trailing system turn, because only the live path appends the settlement marker, so live settlement and hydration resolve identically with no client-side persistence.
  • Success is the pivot on both sides. A successful composer call is omitted from historical rows and from the round divider's tool count — otherwise the divider said "1 tool call" with nothing rendered (caught while reviewing the screenshots). Any other outcome keeps the ordinary row, so a failed offer is never silently invisible.
  • NextStepChips reads only the validated result details — never the raw arguments — renders 1–3 wrapping, viewport-bounded chips, and sends the whole prompt immediately through the ordinary idle session.prompt path via a generic ChatActions.sendPrompt. No draft insertion, no tool-specific wire method, no new wire method at all. The optimistic user turn makes the offer stale instantly; a rejected send appends an error after it, so old chips cannot revive.
  • Its one-shot latch is a ref, not state: two activations dispatched in one task share a render's closure, so a state flag would let the second send through — the exact double-send the latch exists to stop. Pinned by a browser test that fires both clicks in one task.

From #295 I took only the chip concept. Its keyword heuristic, transcript scanning, gradient/scrim experiments, draft-append behaviour and composer-border change are deliberately not carried over.

Tests

36 package tests (contract, currency against hand-built branches, selector lifecycle incl. cancellation, stale offers, the busy follow-up path, non-TUI silence, /next-steps after a resume); registry placement, row suppression vs. failed fallback, live and hydrated derivation, user/error invalidation, the details reader, and chip markup; and e2e/next-steps.spec.ts (no-agent) covering the row, the empty-draft gate, an immediate send, a rejected send, duplicate-click prevention, phone-width wrapping/bounds, and the source and compiled-binary extension wiring via /next-steps appearing in a real session's command list.

writeFixtureSession now expresses tool calls and tool results, pinned against pi's real SessionManager. Per e2e/SPEC.md, the send scenario answers that one session.prompt frame on the socket so both outcomes are deterministic and no provider is reached; every other frame still travels to the real host. The @agent spec uses the natural request “Explain what a mutex is in one sentence, then suggest two useful ways I could explore the topic further.” — it never names the tool — then verifies the chips, the selected item's complete outgoing session.prompt, and the following agent_settled event.

Verification

On the final branch after syncing current main:

  • bun run check:deps · bun run check:seams · bun run lint · bun run typecheck
  • bun run test — 13 packages, including 36 pi-next-steps tests
  • bun run e2e306 passed across 8 shards
  • THINKRAIL_E2E_MODEL=ollama/gemma4:31b-coding-mtp-bf16 THINKRAIL_E2E_SKIP_BUILD=1 bun run e2e:agent -- e2e/next-steps.live.spec.ts1 passed

The live test now formally passes with an isolated local Ollama model. Manual browser verification with GPT-5.6 Sol/xhigh also produced two agent-authored chips from the same natural request; selecting one sent its complete prompt, and the ordinary follow-up did not force another offer.

spec_validate reports only the two pre-existing dangling task-* links, untouched.

Screenshots

Same seeded conversation and layout in each before/after pair. The successful tool call moves from a transcript activity row into agent-authored action chips above the composer; the hidden call also disappears from the round's tool count.

Desktop · 1280 × 800

Before After
Before: offer_next_steps appears as a transcript activity row After: three next-step chips appear above the composer

Phone · 390 × 844

Before After
Before on phone: offer_next_steps appears in the transcript After on phone: next-step chips wrap to two lines without horizontal overflow

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

🌐 Website preview

Preview https://pr-313.thinkrail-website.pages.dev
Routes Landing · Blog · Vibecoding
Commit f121aea

This comment updates in place on every push that touches the website or shared build inputs.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread packages/pi-next-steps/index.ts Outdated
ThinkRail-Todo: 01a03e8d-c551-7ef9-b8e4-8e769d603965/t_e71bda93be4e
The agent, not the browser, decides whether to offer optional ways to
continue and authors each one's label plus the complete prompt it sends.

packages/pi-next-steps — a standalone, publication-ready pi package
(pi.extensions manifest, pi-package keyword, peer deps on
pi-coding-agent + typebox only, no ThinkRail imports). Registers the
terminating `offer_next_steps` tool: 1-3 { label, prompt } items,
trimmed, <=60/<=500 chars, blanks and case-insensitive duplicate labels
or prompts rejected, normalized `details: { items }`, a numbered text
fallback for generic hosts, and `terminate: true` so the turn ends
without an empty follow-up model call. Prompt metadata states the four
rules the tool depends on and names the tool in every guideline.

Native pi: execute() never waits for a person. In TUI mode
`agent_settled` re-reads the branch, confirms a successful result is
still the latest message, then opens pi's native selector; choosing
sends the prompt as a real user message, switching to a follow-up when
another extension already started work. Escape leaves the offer intact.
`/next-steps` reopens it, including after a resume — currency is always
re-read from the session branch, never cached, so there is no
reconstruction step and no stale in-memory offer. The settle handler is
detached on purpose: pi awaits those handlers before reporting idle.
Non-TUI modes keep only the durable fallback.

Bundling: added to the server's dependencies and source-mode extension
resolution, and to the compiled-binary generator. The generator now
derives both the factory array and the skill roots from one declaration
list where each entry states whether it ships skills — the previous
`entryPaths[2..4]` indexing would have silently shifted a skill package.

Web: the tool registry gains a generic `placement` (`transcript` default,
`composer` opt-in), `rows.ts` gains the pure `deriveComposerTool` seam,
`ChatActions` gains `sendPrompt`, and `ComposerToolSlot` renders the
current composer-placed tool above the composer. A successful composer
call is the pivot on both sides: it is omitted from historical rows and
from the round divider's tool count, so the summary can never name a
call the round does not show; anything else keeps the ordinary row, so a
failed offer is never invisible. NextStepChips reads only the validated
result details, renders 1-3 wrapping viewport-bounded chips, and sends
the whole prompt immediately through the ordinary idle prompt path — no
draft insertion, no new wire method. Its one-shot latch is a ref, not
state: two activations dispatched in one task share a render's closure,
so a state flag would let the second send through.

Only the chip concept is taken from #295; its keyword heuristic,
transcript scanning, gradient/scrim experiments, draft-append behavior
and composer-border change are deliberately not carried over.

Tests: 35 package tests (contract, currency, selector lifecycle), the
registry/rows/derivation/reader/markup unit tests, and a no-agent
browser spec covering the row, the empty-draft gate, an immediate send,
a rejected send, duplicate-click prevention, phone-width wrapping and
the source/compiled-binary extension wiring. `writeFixtureSession` now
expresses tool calls and tool results, pinned against pi's real
SessionManager.

Verified: check:deps, check:seams, lint, typecheck, unit tests, the
complete `bun run e2e` gate, build:binary, smoke:binary, and the full
`e2e:binary` suite.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

jetbrains-air[bot]
jetbrains-air Bot previously approved these changes Aug 26, 2026

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

Resolves three adjacency conflicts as a lossless union of both sides —
no behavioural choice was made on either author's behalf:

- apps/web/src/chat/ChatView.tsx: #292's `restoreQueueContentToDraft` /
  `drainQueueToDraft` / `performCompact` sit alongside this branch's
  `useCallback`-wrapped `performSend` (needed by `ChatActions.sendPrompt`).
  `performSend`'s body is byte-identical on both sides.
- apps/web/src/chat/SPEC.md: this branch's `placement` bullet keeps main's
  rewritten `prominence` bullet (#312's nested Thinking disclosure), and the
  next-step-chips bullet follows main's rewritten queue paragraph — the
  lossless `session.abort { restoreQueue: true }` contract that #292's review
  mandated over the older text-only `clearQueue` restore.

rows.ts auto-merged correctly: #312's nested-thinking restructure and this
branch's composer-placement skip touch disjoint lines.

Produced by AIR Automations. Name: Resolve Merge Conflicts / Run: https://air.jetbrains.cloud/org/05cf1a7f-6ab5-713b-abd3-29d0c8a05e2d/automations/6019d7e8-25e2-42ee-9589-26da37839744?run=b3373ee3-4132-4ebe-a63b-7c2d46dbb909
@jetbrains-air

jetbrains-air Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Merged main (61307e8) into agent-driven-next-steps — merge commit cf849b0, no rebase, no force-push. Three conflicts, all adjacency collisions; each resolved as a lossless union, so no decision was taken on either author's behalf.

What I resolved

Where the authors had already decided it

That last one is the only place the two sides said different things about the same behaviour, and it was already settled on #292: three separate blocking review findings rejected the older lossy path (session.clearQueue returning text only, dropping queued image bytes on /compact and letting a queued continuation survive Stop), and #292 replaced it with the lossless atomic drain in f72818eb. Restating the superseded prose here would have re-opened a closed review decision, so main's wording stands unchanged.

rows.ts auto-merged and I left it alone — #312's nested-thinking restructure (nestRoutineRun, ThinkingStep.tools) and this branch's composer-placement skip (isComposerPlaced in deriveRows + turnDivider) touch disjoint lines. Nothing outside the conflict hunks was touched: relative to origin/main the tree contains exactly this branch's authored delta, and relative to the pre-merge branch tip exactly main's — verified line-by-line both ways.

What I ran (bun 1.3.14, the pinned version)

Green: check:deps · check:seams · lint · typecheck (12 packages) · build · build:binary · unit tests — @thinkrail/web 745/745, which covers every resolved line, plus pi-next-steps 35/35, contracts 11/11, cli 58/58, website 79/79, website-analytics 3/3.

Not green, and not caused by this merge: 6 unit tests fail on port binding (shared/freePort.test.ts ×2, server/host/boot.test.ts ×4 — "Unable to connect", stop() releases the port). I reproduced the identical set on a pristine origin/main checkout before concluding that; this sandbox lets a second bind succeed on a listening port and refuses loopback connections. I did not touch those tests.

Gate I could not run here — please let CI have the last word. bun run e2e and e2e:binary are unrunnable in this environment: the Playwright CDN is blocked by network policy (403 Blocked by network policy on cdn.playwright.dev), so chromium cannot be installed, and smoke:binary fails at "WebSocket connection failed" — also reproduced identically on pristine main, since the host binds fine (listening on port 24312) but loopback WS is refused. So the merge is proven by build + static gates + the full unit suite, not by the browser suite. Worth a CI run before merging.

Produced by AIR Automations. Name: Resolve Merge Conflicts / Run: https://air.jetbrains.cloud/org/05cf1a7f-6ab5-713b-abd3-29d0c8a05e2d/automations/6019d7e8-25e2-42ee-9589-26da37839744?run=b3373ee3-4132-4ebe-a63b-7c2d46dbb909

jetbrains-air[bot]
jetbrains-air Bot previously approved these changes Aug 27, 2026

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun marked this pull request as draft August 27, 2026 12:19
… spec

ThinkRail-Todo: 01a03f58-99ab-75aa-8a45-c86d94045849/t_90e315530b3c
ThinkRail-Todo: 01a03f58-99ab-75aa-8a45-c86d94045849/t_f97c8b0ab590
ThinkRail-Todo: 01a03f58-99ab-75aa-8a45-c86d94045849/t_7d53d26da91c
…l behavior

ThinkRail-Todo: 01a03f58-99ab-75aa-8a45-c86d94045849/t_7ca14bde0262
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.

1 participant