Skip to content

proposal(desktop): inline continuation of a partly typed composer draft, built on next-prompt suggestions #5703

Description

@liugddx

Problem

#5629 added opt-in next-prompt suggestions, which fill an empty composer after a reply. The other half of what a user types is a draft they have started but not finished: "帮我把这个函数" → "改成异步的", "Let's add" → "tests first". Today that draft gets no help, even though its most likely ending is usually implied by the last reply.

The pieces #5629 landed already cover most of what a continuation needs:

  • the opt-in preference, synchronised across renderers;
  • the Host-owned, tool-free, metered prompt_suggestion call path, including the off-reasoning gate and maxRetries: 0;
  • the shared generated-text cleanup;
  • the overlay that sits beside ChatComposerInput without measuring anything;
  • Tab to accept through native insertText (undoable), and Esc to dismiss.

What is missing is a trigger for a non-empty draft, a way to place the ghost text after that draft, and a Host operation that accepts a prefix.

Desired outcome

With suggestions enabled, when the user pauses on a draft of at least 4 characters, the composer shows a short continuation in ghost text directly after the caret. Tab appends it, Esc dismisses it, and anything else the user types discards it. Nothing is sent and nothing reaches the transcript.

The proposed design keeps the #4117 invariant of no layout-driven state and stays inside the constraints #5629 established:

1. Placing the ghost without measuring anything.

  • The existing .maka-composer-next-prompt overlay already shares the editor's grid cell, typography, padding and pre-wrap wrapping.
  • For a continuation, the overlay renders the current draft as an invisible span, followed by the visible continuation. The browser's own line breaking puts the ghost exactly where the caret is.
  • No getBoundingClientRect, no scroll sync, no state written from layout.
  • This only holds when the overlay's text matches what the editor renders. So a continuation is offered only when:
    • the draft is plain text: no skill, mention or paste tokens (checked with a DOM query at request time, never stored in state);
    • the caret is collapsed at the end of the draft;
    • the draft does not overflow maxRows.
  • Everything else simply gets no offer.

2. Trigger (renderer).

  • The feature is enabled and the Session is not streaming.
  • There is no IME composition and no open trigger menu (aria-expanded).
  • There are no pending attachments or quotes, and none of the other blocked conditions feat(desktop): add opt-in next prompt suggestions #5629 already uses.
  • The draft is at least 4 characters and does not end in whitespace.
  • A 350 ms debounce; any edit bumps the existing epoch and drops the pending result.
  • Accepting re-checks that the draft still equals the value the request was made for.

Streaming is deliberately excluded in v1. While a turn is running, Esc stops the model, and an offer on a queued message would compete with that.

3. Host operation.

  • Either a new session.prompt-continuation.generate { sessionId, prefix } (prefix capped, e.g. 2,000 code points), or an optional prefix on the existing operation. Either way this needs a compatibility-epoch bump.
  • It reuses readSource eligibility, the prompt_suggestion call path and the off gate, with its own continuation prompt: append-only, no answering, no invented facts, empty when the draft is already complete.
  • Output budget and cleanup are shared with feat(desktop): add opt-in next prompt suggestions #5629. Cleanup additionally strips a repeated prefix.
  • Cancellation without a protocol abort frame. The Host keeps one request in flight per Session and aborts the previous one when a newer prefix arrives. This bounds both the paid calls and the shared connection's in-flight slots to one per Session, and debouncing keeps the rate low.
  • A simple per-client rate cap as a backstop, e.g. 30 per minute.

4. Accounting. Either a distinct callKind (prompt_continuation) so the two features can be measured apart, or reuse prompt_suggestion. I lean towards a distinct kind; #5691 would then let the Session views separate both from main.

5. Tests.

  • Unit tests: the trigger predicate; stale-prefix discard; cleanup, including prefix echo.
  • A linkedom test: Tab with a menu open, and Tab during composition.
  • A Chromium Storybook play asserting that the ghost's first glyph rectangle starts where the draft's last glyph ends, in both light and dark themes and at a narrow width, plus the wrap-at-line-end case.

Alternatives or workarounds

  • Measuring the caret (Range.getBoundingClientRect) and positioning an absolute ghost. This is more flexible (tokens, mid-text caret), but it reintroduces the layout-to-state path that Bug:alert error dialog #4117 warns about. The invisible-mirror approach avoids that at the cost of skipping drafts that contain tokens.
  • Inserting a non-editable ghost node inside the contenteditable. Rejected: serialize would read its text, controlled writes would wipe it, slash-query detection would break, and it cannot be undone cleanly.
  • Allowing continuation while streaming. Possible later, once Esc priority between "dismiss offer" and "stop turn" is decided.

Note: with the off gate from #5629, continuation is unavailable on models that cannot disable reasoning (GPT-5, o-series, GPT-6, Gemini 3.x), the same as next-prompt suggestions. #5690 tracks lifting that.

Happy to implement this as a single PR if maintainers agree with the direction, especially the mirror placement and the one-in-flight-per-Session cancellation.

AI disclosure: Claude Code (Claude Opus) studied the merged #5629 code and drafted this proposal; liugddx reviewed it and is the human contributor of record.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions