feat(next-steps): agent-chosen next-step actions - #313
feat(next-steps): agent-chosen next-step actions#313Rinat S (rsolmano) wants to merge 11 commits into
Conversation
🌐 Website preview
This comment updates in place on every push that touches the website or shared build inputs. |
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.
9132577 to
4da74a2
Compare
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
|
Merged 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 (
What I ran (bun 1.3.14, the pinned version) Green: Not green, and not caused by this merge: 6 unit tests fail on port binding ( Gate I could not run here — please let CI have the last word. 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 |
… 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
# Conflicts: # apps/web/src/chat/ChatView.tsx
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 packagePublication-ready:
pi.extensionsmanifest,pi-packagekeyword, peer deps onpi-coding-agent+typeboxonly, no ThinkRail imports. It registers the terminatingoffer_next_stepstool:{ label, prompt }items, trimmed, ≤60 / ≤500 charsdetails: { items }+ a numbered plain-text fallback for hosts with no rendererterminate: true, so the turn ends without paying for an empty follow-up model callNative pi.
execute()never waits for a person — that's the restart invariant. In TUI modeagent_settledre-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-stepsreopens 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 awaitsagent_settledhandlers 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
placementon 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 trailingsystemturn, because only the live path appends the settlement marker, so live settlement and hydration resolve identically with no client-side persistence.NextStepChipsreads 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 idlesession.promptpath via a genericChatActions.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.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-stepsafter a resume); registry placement, row suppression vs. failed fallback, live and hydrated derivation, user/error invalidation, the details reader, and chip markup; ande2e/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-stepsappearing in a real session's command list.writeFixtureSessionnow expresses tool calls and tool results, pinned against pi's realSessionManager. Pere2e/SPEC.md, the send scenario answers that onesession.promptframe on the socket so both outcomes are deterministic and no provider is reached; every other frame still travels to the real host. The@agentspec 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 outgoingsession.prompt, and the followingagent_settledevent.Verification
On the final branch after syncing current
main:bun run check:deps·bun run check:seams·bun run lint·bun run typecheckbun run test— 13 packages, including 36pi-next-stepstestsbun run e2e— 306 passed across 8 shardsTHINKRAIL_E2E_MODEL=ollama/gemma4:31b-coding-mtp-bf16 THINKRAIL_E2E_SKIP_BUILD=1 bun run e2e:agent -- e2e/next-steps.live.spec.ts— 1 passedThe 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_validatereports only the two pre-existing danglingtask-*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
Phone · 390 × 844