Skip to content

feat: add pinned messages - #117

Open
jjjhenriksen wants to merge 6 commits into
openclaw:mainfrom
jjjhenriksen:codex/pinned-messages
Open

feat: add pinned messages#117
jjjhenriksen wants to merge 6 commits into
openclaw:mainfrom
jjjhenriksen:codex/pinned-messages

Conversation

@jjjhenriksen

@jjjhenriksen jjjhenriksen commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add shared channel message pins with SQLite and PostgreSQL persistence
  • expose typed pin, unpin, and list APIs plus pin.added and pin.removed realtime events
  • add timeline, touch-sheet, thread-reply, and pinned-panel controls with realtime refresh
  • publish the OpenAPI/TypeScript SDK contract, documentation, and refreshed embedded web client

Real behavior proof

The focused Playwright scenario runs against the real ClickClack Go server and embedded web app. It:

  1. creates a real workspace and channel, then sends a message through the ClickClack composer;
  2. pins the message through its More actions menu and verifies the real 201 response and pin.added event;
  3. opens Pinned items and observes the pinned message;
  4. edits that message through the real API while the app remains open and observes the panel update through realtime message.updated handling;
  5. reloads and verifies the message action is already Unpin, proving persisted pin state loads on startup;
  6. opens the pinned message's thread, sends a real thread reply, pins that reply from its thread action, and observes both messages in Pinned items;
  7. returns to and reloads the canonical channel URL, proving persisted root and reply pins remain visible without restoring the closed thread;
  8. unpins both messages, verifies the pin.removed response and empty UI state, then verifies the durable API list is empty.

Command and result:

pnpm exec playwright test tests/e2e/pinned-messages.spec.ts --reporter=line
1 passed (7.7s)

pnpm test:e2e
133 passed (45.8s)

The SQLite boundary regression starts with 99 pins, races two requests for the final slot ten times, and proves each run yields exactly one success, one ErrPinnedMessageLimit, and 100 stored pins. HTTP authorization coverage proves a workspace-bound bot token receives 403 for pin, unpin, and list operations in another workspace.

Validation

pnpm generate:sqlc
passed

go test ./internal/store/sqlite -run 'TestPinnedMessage(LimitIsAtomic|Limit|Lifecycle)$' -count=10
passed

go test ./internal/httpapi -run TestHTTPBotTokenWorkspaceIsolation -count=1
passed

bash scripts/check-go-coverage.sh
85.2% aggregate statement coverage (85.0% required)

pnpm check
passed

The HTTP behavior suite also proves malformed and missing pin payloads return 400, a repeated unpin returns 404, and listing after the final unpin serializes a non-null empty messages array. Hosted ChatGPT Codex findings addressed: bot-token workspace guards, refresh on message edits, authoritative resync refresh, thread-reply pin controls, and batched typed PostgreSQL pin listing. ClawSweeper's SQLite concurrency finding is covered by a single conditional write plus the repeated boundary race above. The branch is rebased onto current main.

Branch head d8591ed81925f9115ef19e983d0612348fce3f2c is one commit directly on ae666669f5ae08e09431d88eded7f012498f12bb (main).

@jjjhenriksen

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@jjjhenriksen
jjjhenriksen marked this pull request as ready for review July 26, 2026 03:38
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. labels Jul 26, 2026
@clawsweeper

clawsweeper Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed August 1, 2026, 7:47 PM ET / 23:47 UTC.

ClawSweeper review

What this changes

Adds shared channel-message pins with SQLite and PostgreSQL persistence, three HTTP operations, SDK/OpenAPI types, durable realtime events, and web controls for listing, pinning, and unpinning messages.

Merge readiness

⚠️ Ready for maintainer review - 4 items remain

Keep this PR open for maintainer review. Current main still exposes pinned items only as a stub and has no pin persistence or API routes, while this branch adds a broad but coherently validated feature that creates lasting storage, REST, SDK, and realtime-event contracts.

Priority: P2
Reviewed head: d26c11abba773132c51defb6e647db3a69610003
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Strong real-behavior proof and focused regression coverage support a good feature patch; the remaining gate is maintainer acceptance of its permanent public contract.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (live_output): The PR body provides detailed after-fix evidence from Playwright against the real Go server and embedded web app, including persisted reloads, realtime refresh, thread-reply pins, authorization, and SQLite boundary races.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (live_output): The PR body provides detailed after-fix evidence from Playwright against the real Go server and embedded web app, including persisted reloads, realtime refresh, thread-reply pins, authorization, and SQLite boundary races.
Evidence reviewed 6 items Current main does not implement pins: The checked-out default branch has no pin routes or durable pin events; its web client still treats pinned items as a stub and reports “no pinned items.”.
Clean feature branch against current main: Current main is the merge base of the PR head and is an ancestor of it; the branch changes 70 files, so the merge is not obsolete main drift.
Storage lifecycle covers deletion and realtime consistency: The branch removes an associated pin inside the message-deletion transaction, emits pin.removed before message.deleted, and refreshes pin state during authoritative resyncs and relevant message events.
Findings None None.
Security None None.

How this fits together

ClickClack persists channel messages in SQLite or PostgreSQL, exposes them through HTTP and the TypeScript SDK, and updates connected web clients through durable realtime events. This PR adds a shared per-channel pin record and a pinned-message panel to that existing message lifecycle.

flowchart LR
  A[Channel messages] --> B[Pin and unpin API]
  B --> C[SQLite or PostgreSQL pin records]
  C --> D[Durable pin events]
  D --> E[Realtime client refresh]
  E --> F[Pinned messages panel]
  C --> G[OpenAPI and SDK clients]
Loading

Decision needed

Question Recommendation
Should ClickClack adopt shared persisted channel pins—with a 100-pin limit, public REST/SDK methods, and durable pin.added/pin.removed events—as a supported product contract? Sponsor the shared-pin contract: Accept the persisted channel-pin feature and its documented HTTP, SDK, export, and realtime guarantees as proposed.

Why: The branch is coherent and proof-backed, but choosing to expose and support a persisted collaboration feature and its external contracts is a product decision, not a mechanical correctness judgment.

Before merge

  • Resolve merge risk (P1) - Merging establishes permanent SQLite and PostgreSQL migration state, three public REST operations, generated SDK methods, two durable event names, and a 100-pin-per-channel policy that external clients may depend on.
  • Resolve merge risk (P1) - The implementation is large for a feature PR—70 files across storage, APIs, generated contracts, embedded web assets, and UI—so maintainer acceptance should cover the intended product and compatibility contract, not only test status.
  • Complete next step (P2) - No discrete patch defect was found, but a maintainer must decide whether to support the new persistent public pin contract before merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Review surface 70 files; 3,019 additions; 97 deletions The feature crosses persistence, HTTP, SDK, realtime, generated web assets, docs, and browser coverage.
Public contract 2 database migrations, 3 REST operations, 2 durable events These interfaces and stored-state changes are the central compatibility commitment before merge.

Merge-risk options

Maintainer options:

  1. Sponsor the complete contract (recommended)
    Approve the migration, API, SDK, durable-event, export, and 100-pin policy together so clients receive one stable behavior.
  2. Pause for product narrowing
    Hold the PR if maintainers do not want shared pins or any part of the permanent public contract in the current release direction.

Technical review

Best possible solution:

Sponsor shared channel pins as a documented public contract, retaining the migration, API, SDK, event, export, and realtime behavior together; otherwise pause rather than partially merging parallel pin surfaces.

Do we have a high-confidence way to reproduce the issue?

Not applicable: this PR proposes a new capability rather than repairing a defined broken behavior. Its body nevertheless supplies a detailed real-server Playwright scenario covering persistence, realtime refresh, thread replies, reloads, and unpinning.

Is this the best way to solve the issue?

Unclear: the implementation is internally consistent and current main lacks an equivalent capability, but whether shared pins should become a supported persisted API and event contract requires maintainer product direction.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4843afe2e8eb.

Labels

Label justifications:

  • P2: This is a substantial but non-urgent user-facing collaboration feature with bounded, reviewed scope.
  • merge-risk: 🚨 compatibility: The PR adds persistent schemas plus REST, SDK, export, event, and documented limit contracts that existing integrations may adopt.
  • merge-risk: 🚨 session-state: Pins are shared durable channel state and must remain synchronized with message deletion, edits, resyncs, and reloads.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body provides detailed after-fix evidence from Playwright against the real Go server and embedded web app, including persisted reloads, realtime refresh, thread-reply pins, authorization, and SQLite boundary races.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides detailed after-fix evidence from Playwright against the real Go server and embedded web app, including persisted reloads, realtime refresh, thread-reply pins, authorization, and SQLite boundary races.

Evidence

What I checked:

  • Current main does not implement pins: The checked-out default branch has no pin routes or durable pin events; its web client still treats pinned items as a stub and reports “no pinned items.”. (apps/web/src/ChatApp.svelte:3535, 4843afe2e8eb)
  • Clean feature branch against current main: Current main is the merge base of the PR head and is an ancestor of it; the branch changes 70 files, so the merge is not obsolete main drift. (apps/api/internal/httpapi/pins.go:1, d26c11abba77)
  • Storage lifecycle covers deletion and realtime consistency: The branch removes an associated pin inside the message-deletion transaction, emits pin.removed before message.deleted, and refreshes pin state during authoritative resyncs and relevant message events. (apps/api/internal/store/postgres/mutations.go:370, d26c11abba77)
  • SQL policy is followed in the proposed diff: Both stores add schema/query inputs and corresponding generated storedb output; the root policy requires sqlc rather than hand-maintained generated store code. (sqlc.yaml:1, d26c11abba77)
  • Feature history and review follow-up: The PR branch contains the original pin feature plus focused lifecycle and PostgreSQL isolated-migration follow-ups. Current-main history identifies recent realtime and ChatApp work by Peter Steinberger and Shakker as adjacent ownership signals. (apps/web/src/ChatApp.svelte:2701, bfc8fc6230e4)
  • History-inspection limitation: Targeted blame and pickaxe history reads attempted to hydrate unavailable promisor objects, but network DNS resolution was unavailable. The decision therefore relies on locally available current-main source, the PR range, and available log records rather than claiming a complete blame trail.

Likely related people:

  • Peter Steinberger: Recent current-main commits cover realtime recovery and channel display behavior adjacent to this PR’s client synchronization and channel controls. (role: recent realtime and channel-work contributor; confidence: medium; commits: bfc8fc6230e4, e455d43809a0; files: apps/web/src/ChatApp.svelte, apps/api/internal/httpapi/mutations.go)
  • Shakker: Recent current-main work on web-client state and preferences makes this person a plausible reviewer for the ChatApp panel and UI-state integration. (role: recent web-client contributor; confidence: medium; commits: f8215a37d22d, 4e144f880bf6; files: apps/web/src/ChatApp.svelte)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Obtain maintainer sponsorship for the shared-pin API, durable-event, migration, and 100-pin policy as one supported contract.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (29 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-31T23:35:30.591Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T04:05:27.750Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T09:35:03.193Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T11:27:27.719Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T12:27:01.852Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T14:52:41.312Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T17:34:06.847Z sha d26c11a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T20:14:42.424Z sha d26c11a :: needs maintainer review before merge. :: none

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea11592ede

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return
}

pin, event, err := s.store.PinMessage(r.Context(), channelID, body.MessageID, act.user.ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce the bot token's workspace before accessing pins

When a bot identity belongs to multiple workspaces, a token issued for workspace A can reach this store call with a channel from workspace B and read or mutate B's shared pins, because membership checks use only act.user.ID. The existing bot-token isolation test models this exact multi-workspace identity, and other channel handlers call requireBotChannelWorkspace; apply the same guard in all three pin handlers before invoking the store.

Useful? React with 👍 / 👎.

Comment thread apps/web/src/ChatApp.svelte Outdated
Comment on lines +2824 to +2825
if (event.type === "message.deleted" && pinnedMessageIDs.has(event.payload.message_id || "")) {
await loadPinnedMessages();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh pins after pinned messages are edited

When a pinned message is edited while the pin panel is open, message.updated reloads the timeline but this invalidation only refreshes pins for message.deleted. Because pinnedMessages contains separately fetched message objects, the panel continues displaying the old body and attachments until another pin event, channel navigation, or manual panel reload; refresh or update the pin cache for message.updated as well.

Useful? React with 👍 / 👎.

Comment on lines +2755 to +2759
(event.type === "pin.added" || event.type === "pin.removed") &&
event.channel_id === selectedChannelID &&
!selectedDirectID
) {
await loadPinnedMessages();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reload pins during authoritative realtime resyncs

When the realtime wrapper forces an authoritative resync after an unusable cursor, it advances to the current tail instead of replaying missed events, but the resync reloads messages and channel data without reloading pinnedMessages. Consequently pin changes made during the gap remain stale in an open panel and in message-menu pin labels; the pin-event handler cannot repair this because those events were intentionally skipped, so include the active channel's pins in reconcileRealtimeState.

Useful? React with 👍 / 👎.

Comment on lines +3641 to +3643
channelID={selectedChannelID}
{pinnedMessageIDs}
onTogglePin={toggleMessagePin}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose pin actions for thread replies

The pin state and toggle callback are wired only into MessageList, while channel thread replies are rendered separately by ThreadPanel, which receives neither prop and offers no pin control. The store accepts replies because they retain the channel ID, and the new pinned-panel navigation explicitly handles parent_message_id, so users can view API-created reply pins but cannot create or toggle them through the app; pass the same pin state and callback into the thread surface.

Useful? React with 👍 / 👎.

Comment on lines +154 to +155
for _, row := range rows {
message, err := getMessage(ctx, s.db, row.MessageID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Batch pinned-message retrieval

For a channel at the supported 100-pin limit, every list request performs the initial pin query and then up to 100 sequential getMessage queries; on PostgreSQL this adds a database round trip per pin before attachment and reaction hydration and can make opening the panel disproportionately slow. Return the complete message rows with one typed sqlc query, or batch-fetch all selected IDs, while preserving the pin ordering.

AGENTS.md reference: AGENTS.md:L3-L3

Useful? React with 👍 / 👎.

@jjjhenriksen
jjjhenriksen force-pushed the codex/pinned-messages branch from ea11592 to 7d352a1 Compare July 26, 2026 05:38
@jjjhenriksen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 7d352a14e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 26, 2026
@jjjhenriksen
jjjhenriksen force-pushed the codex/pinned-messages branch from 7d352a1 to fe4e99c Compare July 26, 2026 05:58
@jjjhenriksen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe4e99c6d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
defer tx.Rollback()

msg, err := getMessageTx(ctx, tx, messageID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Lock the message before validating its deletion state

In PostgreSQL, when DeleteMessage commits after this unlocked getMessageTx read but before the pin insert, this transaction can still insert a pin and emit pin.added for the now-deleted message. The new pin is then immediately absent from ListPinnedMessages because that query filters m.deleted_at IS NULL, despite the POST having returned 201. Lock the message row through the validation/insert or make the insert conditional on the message remaining undeleted.

Useful? React with 👍 / 👎.

@@ -0,0 +1,12 @@
CREATE TABLE pinned_messages (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include pinned messages in JSON exports

When an operator runs clickclack export, both the SQLite and PostgreSQL ExportJSON implementations read from explicit table allowlists that do not include this new pinned_messages table. Every JSON export therefore silently drops all channel pin state, including exports used for migrations or audits; add the table to both export allowlists and cover it in the export tests.

Useful? React with 👍 / 👎.

LEFT JOIN bot_tombstones quoted_tombstone ON quoted_tombstone.bot_user_id = qu.id
WHERE p.workspace_id = sqlc.arg(workspace_id)
AND p.channel_id = sqlc.arg(channel_id)
AND m.deleted_at IS NULL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit a pin removal when deleting a pinned message

When a pinned message is soft-deleted, this filter makes it disappear from the authoritative pin list, but DeleteMessage neither deletes its pinned_messages row nor emits pin.removed; it emits only message.deleted. An outgoing subscription configured for exactly pin.added and pin.removed—which the new event registry explicitly supports—will consequently retain the deleted message forever in its replicated pin state. Remove the pin and emit the corresponding event as part of the deletion transaction.

Useful? React with 👍 / 👎.

Comment on lines +306 to +307
try {
await onTogglePin(message, pinnedMessageIDs.has(message.id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle rejected pin mutations in the thread panel

When pinning from a thread fails—for example because the channel has reached 100 pins, another client already changed the pin, or the request loses connectivity—onTogglePin rejects and this try only resets the saving flag. The toolbar invokes togglePin with void, and the action-sheet callback also has no error boundary, so users receive an unhandled promise rejection and no visible failure feedback; catch the error here and expose it as the timeline and pinned-panel actions already do.

Useful? React with 👍 / 👎.

<button type="submit" class="search-submit">Search</button>
</form>

<button type="button" class="desktop-titlebar-pin" title="Pinned items" aria-label="Pinned items" onclick={onPinnedItems}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide the pin control for direct messages

When a direct conversation is selected, this title-bar button is still rendered and enabled even though direct messages do not support pins. Its handler immediately returns because selectedDirectID is set, so activating the visible “Pinned items” control does nothing; the regular top bar has the same behavior after it is wired to togglePinnedPanel. Render or enable the control only when a channel is active.

Useful? React with 👍 / 👎.

@jjjhenriksen
jjjhenriksen force-pushed the codex/pinned-messages branch from fe4e99c to d8591ed Compare July 26, 2026 06:13
@jjjhenriksen

Copy link
Copy Markdown
Contributor Author

@codex review

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8591ed819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!channelID || !onTogglePin || pinSaving) return;
pinSaving = true;
try {
await onTogglePin(message, pinnedMessageIDs.has(message.id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle pin failures in the thread panel

When the pin request is rejected—for example, with the expected 409 after the channel reaches 100 pins—or the network fails, onTogglePin rejects and this try/finally propagates the error. The toolbar explicitly discards that promise, so users receive an unhandled rejection with no visible explanation, while the action-sheet path remains open without reporting why the operation failed. Catch the error and expose a status as MessageRow does for the same action.

Useful? React with 👍 / 👎.

pinnedPanelOpen = false;
return;
}
if (!selectedChannelID || selectedDirectID) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable pinned-items controls outside channels

When a direct message or an empty workspace is selected, both top-bar variants still present an enabled “Pinned items” button, but this branch silently returns because pins are channel-only. The previous top-bar stub at least reported that there were no pinned items, while the new desktop control also routes here, so these contexts now expose a control that appears operable but has no effect. Hide or disable the control when there is no selected channel, or provide explicit unsupported-state feedback.

Useful? React with 👍 / 👎.

Comment on lines +2827 to +2830
(event.type === "message.updated" || event.type === "message.deleted") &&
pinnedMessageIDs.has(event.payload.message_id || "")
) {
await loadPinnedMessages();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Synchronize pins after local message mutations

If this client successfully edits or deletes a pinned message over HTTP while its WebSocket is disconnected or blocked, the local mutation handlers update messages, replies, and selectedThread but never update pinnedMessages. This realtime-only refresh therefore never runs, leaving edited text stale and deleted messages visible in the pinned panel indefinitely until a later reconnect or reload. Update or remove the corresponding pinned entry as part of the successful local mutation path rather than relying exclusively on the echoed event.

Useful? React with 👍 / 👎.

…s-lifecycle

# Conflicts:
#	apps/api/internal/webassets/dist/200.html
#	apps/api/internal/webassets/dist/_app/immutable/entry/app.C0jTAm08.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/0.erneJbxK.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/11.BPwHOykE.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/11.COwgNK6M.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/11.vvKR-XrD.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/12.D2jzbKVo.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/13.0lkaA9sE.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/3.BoPssj0u.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/4.B1Q5W4BL.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/4.CV83zLxh.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/4.D81lYOij.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/5.C04PopFT.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/5.DW2e0mkr.js
#	apps/api/internal/webassets/dist/_app/immutable/nodes/5.gnDXXEBJ.js
#	apps/api/internal/webassets/dist/index.html
@clawsweeper clawsweeper Bot added status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Jul 29, 2026
@PollyBot13

Copy link
Copy Markdown
Contributor

The lifecycle assistance branch has been merged into this PR and the latest ClawSweeper review has no actionable code findings. I opened a narrow follow-up for the current PostgreSQL CI failure: jjjhenriksen#2. It adds the missing per-test migrations for the three new isolated-schema tests; once Jacqueline merges it, the upstream CI can rerun on the same feature head.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 29, 2026
test: migrate isolated PostgreSQL pin stores
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. labels Jul 29, 2026
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. label Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants