Skip to content

Ability to pin specific messages or AI responses within a conversation #7708

Open

Description

@stenza

What problem are you trying to solve?

In longer working sessions, certain messages become reference points that get revisited repeatedly — a generated to-do list, a config snippet, a decision summary, a list of steps. Right now the only way to find them again is scrolling back or re-asking the model to repeat itself, which breaks flow and wastes context.

Proposed solution

Let users pin any message (their own or the assistant's) within a conversation. Pinned items would:

  • Be accessible from a persistent sidebar or a collapsible "Pinned" panel, separate from the main scroll
  • Stay visible/reachable regardless of how far the conversation has scrolled
  • Support multiple pins per conversation, each with a short auto-generated or user-editable label (e.g. "Deploy checklist", "API keys format")
  • Be easy to unpin
  • Ideally, be quick-referenceable back into the chat (e.g. a small "insert into prompt" or "quote" action) so a pinned to-do list or spec can be reused as context in a later message without retyping or scrolling

Use case example:

Working through a multi-step task, the assistant generates a to-do list partway through the conversation. As the session continues, I need to keep checking that list against my progress — pinning it would let me glance at it without losing my place in the ongoing thread.

Why it matters:

This is a common pattern in longer technical or planning sessions — certain outputs function as working reference material rather than one-off answers, and the UI doesn't currently have a way to distinguish or surface them.

Workarounds you've tried

none

Activity

  1. github-actions commented on Sep 21, 2026

    @github-actions
    Contributor

    Summary

    This is a feature request (correctly labeled enhancement), not a bug — there is no "pin" concept anywhere in the chat UI today, so nothing is misbehaving. What's being asked for is: a per-message pin toggle, a separate panel listing the pins with editable labels, a way to jump back to the pinned message, and a "quote/insert into prompt" action. Below is what I found about where this would land and the four constraints that will actually shape the implementation.

    Which surface this lands on

    The structured-message chat is ChatV3Pane, and it is behind the PostHog flag chat-v3 (packages/shared/src/constants.ts:178-183). The default agent surface in a v2 workspace is still TerminalPane, which renders raw PTY output and has no message model at all — pinning is not implementable there without reimplementing it on top of chat-v3. Mobile is in the same position (apps/mobile/screens/(authenticated)/workspace/[id]/components/TerminalWebView/). apps/web/.../SessionChat/SessionChat.tsx renders mock data on a design page, so it isn't a real target either.

    So: this feature is effectively a chat-v3 feature. Worth deciding explicitly before any work starts, because it changes the size of the request considerably.

    What already exists and makes this cheap

    • Stable item ids. Every transcript item carries an id (packages/chat/src/protocol/items.ts:3-8), derived in the Claude adapter as `${messageId}#${index}` from the API message id (packages/chat-runtime/src/harness/claude/translateStream/translateStream.ts:182-190), and persisted in the event journal (packages/chat-runtime/src/db/schema/schema.ts:11-23). Ids survive replay.
    • Item ids are already a durable client-facing reference. forkSessionInputSchema.fromItemId (packages/chat/src/protocol/commands.ts:67-72) is precedent for a client pinning a command to a specific item id — a pinItem/unpinItem command would be architecturally consistent.
    • Scroll-to-message already works. Rows carry data-item-id (.../Transcript/components/TurnGroupSection/TurnGroupSection.tsx:79 and :102), and Transcript.tsx:99-112 already does querySelector('[data-item-id=...]') + scrollIntoView for the latest user message and the first pending approval. "Jump to pin" reuses that exact mechanism.
    • An unused selection popover exists. packages/ui/src/components/ai-elements/text-selection-popover.tsx implements a primary/secondary action popover over a text selection — exactly the shape of a "quote" affordance. I found no importers of it anywhere in the repo, so it's available.
    • MessageActions/MessageAction (packages/ui/src/components/ai-elements/message.tsx:75-121) is the existing hover-action primitive, though the chat-v3 rows don't currently use it — AgentMessageRow is a bare MarkdownView (.../AgentMessageRow/AgentMessageRow.tsx:13).

    The four real design decisions

    1. Where pins are stored — not localStorage. Pins are entity-scoped (session × item) with unbounded cardinality, which apps/desktop/AGENTS.md explicitly routes to host-side SQLite rather than the renderer's shared ~10 MB localStorage quota. The natural home is the chat runtime DB alongside chat_journal / chat_sessions_local (packages/chat-runtime/src/db/schema/schema.ts) — e.g. a chat_pins(session_id, item_id, label, created_at) table served over the existing /chat-v3/trpc mount (packages/host-service/src/chat-v3/mount.ts:24). That also makes pins survive across machines/clients the way session state does. Note packages/db isn't the right place either — that's the cloud Drizzle schema, and the chat DB has its own migrations.

    2. A pin panel cannot resolve content from item id alone. The transcript is paginated (getItemsInputSchema defaults to 200 items, packages/chat/src/protocol/commands.ts:83-88) with hasOlder/loadOlder (packages/chat/src/react/useChatSession/useChatSession.ts:218-238), and a stream reset triggers resync() which throws away everything and re-seeds only the latest page (useChatSession.ts:125-147). Meanwhile displayText returns "" for an item that isn't in the current snapshot (packages/chat/src/core/reducer/reducer.ts:156-161). Concretely: pin a message, scroll far enough that it pages out or the socket resets, and a naive panel renders a blank pin. Either the pin row stores a content snapshot at pin time, or "jump to pin" needs to drive loadOlder() in a loop until the id appears.

    3. The composer has no insert API. Composer owns its textarea state internally and exposes only draftKey for localStorage persistence (.../Composer/Composer.tsx:20-52). There is no controlled value prop and no imperative handle, so "insert into prompt" / "quote" needs a small API addition there — and whatever it writes must stay consistent with the debounced draft write at Composer.tsx:38-44. The chat-v3-draft:* key is already registered at apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts:61-62; any new renderer key would need the same registration (CI fails otherwise).

    4. content-visibility: auto on rows. Every entry is rendered with [content-visibility:auto] [contain-intrinsic-size:auto_240px] (TurnGroupSection.tsx:21-22). scrollIntoView against an offscreen, size-estimated row can land at the wrong offset before the real content lays out — the existing anchoring code lives with this, but a user-triggered "jump to pin" will make it much more visible. Expect to need a post-layout correction.

    Repro / verification sketch

    There's no bug to reproduce, but to see the current gap: enable the chat-v3 flag, open the chat pane in a v2 workspace, run a task long enough for the agent to emit a plan, then scroll — the plan (.../Transcript/components/PlanRow/PlanRow.tsx) is an ordinary inline row with no way to keep it in view. That row is probably the single highest-value pin target given the use case in the issue, and a cheaper first increment might be "keep the latest plan docked" rather than general-purpose pinning.

    For tests, the pieces are unit-testable without the UI: a bun:test case over deriveTimeline + a pin store asserting that a pinned id still resolves after loadOlder()/resync() drops it from the snapshot would pin down constraint #2 directly, using the existing fixtures in packages/chat-runtime/src/testing/fixtures/.

    Suggested direction

    1. Pins in the chat runtime DB, exposed as list/add/remove/rename over the existing chat-v3 tRPC mount; key on (session_id, item_id) and store a label plus a text snapshot of the pinned content.
    2. Hover MessageAction on AgentMessageRow/UserMessageRow to toggle, auto-labelling from the first heading or line of the item text.
    3. A collapsible panel in SessionView (sibling to Transcript, .../SessionView/SessionView.tsx:48-73), rendering from the stored snapshot so it never blanks, with "jump" using the existing data-item-id + scrollIntoView path.
    4. Add a controlled-value or imperative-insert prop to Composer for the quote action, and wire TextSelectionPopover to it — that also delivers the "quote a selection" half of the request independently of pinning, and is probably the smallest shippable slice.

    All new strings need Lingui macros and catalog updates per AGENTS.md (bun run check:i18n).

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