diff --git a/CHANGELOG.md b/CHANGELOG.md index 909fb3be..3d881e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,19 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- **Linear integration:** connect a Linear workspace in Settings → Integrations, browse its issues in the context rail with status, priority, assignee, and team filters, and start a session or worktree straight from an issue. Sessions started that way post started, completed, and failed comments on the issue, each linking back to the session; chat can also attach an issue to the next send (thanks to @AlexKutas). -- **Voice: the voice follows the language of the text.** With "Match the voice to the language of the text" (Settings → Voice, on by default) the local provider switches to a model for the reply's language — Kokoro for Chinese/English and Piper models for Ukrainian, German, French, Spanish, Italian, Portuguese, Polish, Russian, Dutch, Czech, Turkish, and Swedish, downloaded on first use — and macOS say switches to an installed voice of that language. The local voice picker lists every installed model's voices. -- **Git: projects made of several repositories.** A project whose root is not itself a Git repository — a folder of plugins, a workspace of services — now works with the repositories inside it. The Git tab opens the first one it finds, a picker next to the branch dropdown switches between them, and the diff, pull request, walkthrough, and mobile Changes views follow the same choice; the work status card shows the chosen repository's branch and changes with the folder named under the branch (thanks to @jaygupta17). -- **Chat:** sessions opened from the sidebar stay at the latest message, and switching between sessions no longer causes conversation jumps, partial rendering, crossfades, or tab-title shifts. -- Chat: command and skill autocomplete in a Chat (a session that belongs to no project) lists that chat's own commands and skills instead of the project last selected in the sidebar, and file mentions in a new chat draft no longer search the previous project. -- Files: Ctrl/Cmd+F opens the find bar in the Markdown preview even when nothing inside the preview has focus. -- GitHub: account connection has moved to Settings → Integrations. The pull-request panel now includes account controls, and its context-rail icon appears only when GitHub is connected. -- Chat: a turn that OpenCode stopped no longer ends with nothing on screen — what OpenCode reported shows under the last message, and a message an idle session has left unanswered is named as such. The status report (Ctrl/Cmd+Shift+L) now lists the last session errors, rejected sends, the managed OpenCode process's last error, and where the log files are. -- Git: the commit graph no longer leaves a gap in a lane when the same branch is merged twice (thanks to @Naputt1). -- Settings: the theme is now remembered per OpenChamber instance. Two windows connected to different instances no longer swap themes with each other or overwrite each other's choice on every settings sync; each window boots with the theme of the instance it points at (thanks to @kydorn). -- Scheduled tasks: a task's Goal and Auto-accept settings no longer disappear after a run when another OpenChamber process — an older desktop, CLI, or VS Code build — shares the same project config. Every process now rewrites only what it changed and leaves the rest of each task exactly as stored. -- Desktop: on Windows and Linux the close button sits flush against the window edge, so the exact top-right corner closes the window, and its hover color follows the theme (thanks to @kydorn). +- **Linear integration:** connect a workspace in Settings → Integrations, browse and filter issues, and start a session or worktree from an issue. OpenChamber reports session progress back to Linear and can attach an issue to the next chat message (thanks to @AlexKutas). +- **Voice:** local text-to-speech and macOS say now choose a voice that matches the reply's language. Additional local models download on first use, and the voice picker lists voices from every installed model. +- **Git:** projects containing several repositories can now switch between them from the Git tab. Diff, pull request, walkthrough, mobile Changes, and work status follow the selected repository (thanks to @jaygupta17). +- **Chat:** sessions opened from the sidebar stay at the latest message, and switching sessions no longer causes jumps, partial rendering, crossfades, or tab-title shifts. +- Chat: command, skill, and file autocomplete in projectless chats no longer uses the previously selected project. +- Chat: reverting to a message, or forking from one, now brings its attached context back to the composer — review comments, chat and file quotes, terminal selections, and browser annotations are no longer lost. +- Chat: stopped and unanswered turns now explain what happened. The status report includes recent session, send, and managed OpenCode errors, plus log locations. +- Files: Ctrl/Cmd+F opens search in the Markdown preview even when the preview is not focused. +- GitHub: account connection has moved to Settings → Integrations. The pull-request panel includes account controls, and its context-rail icon appears only when connected. +- Git: the commit graph no longer leaves a lane gap when the same branch is merged twice (thanks to @Naputt1). +- Settings: themes are now remembered per OpenChamber instance, so windows connected to different instances keep their own theme (thanks to @kydorn). +- Scheduled tasks: Goal, Auto-accept, and other task settings are preserved when older OpenChamber builds share the same project config. +- Desktop: on Windows and Linux, the close button reaches the top-right corner and follows the theme on hover (thanks to @kydorn). ## [1.21.1] - 2026-08-29 diff --git a/packages/ui/src/lib/messages/contextParts.ts b/packages/ui/src/lib/messages/contextParts.ts index 8ec46a2c..4b6b78a9 100644 --- a/packages/ui/src/lib/messages/contextParts.ts +++ b/packages/ui/src/lib/messages/contextParts.ts @@ -333,3 +333,78 @@ export function readContextPart(part: ContextCarrierPart): ContextPartPayload | export function hasContextParts(parts: ContextCarrierPart[]): boolean { return parts.some((part) => readContextPart(part) !== null); } + +/** + * The composer draft a context payload came from, so reverting or forking a + * message can put its attached context back on the chips instead of dropping + * it. Linked issues/PRs have no draft form — they are owned by their own + * pickers — so they map to null. + */ +export function draftFromContextPayload( + payload: ContextPartPayload, +): Omit | null { + switch (payload.kind) { + case 'code-comment': { + const draft: Omit = { + source: payload.source, + fileLabel: payload.fileLabel, + startLine: payload.startLine, + endLine: payload.endLine, + code: payload.code, + language: payload.language, + text: payload.text, + }; + if (payload.side) draft.side = payload.side; + return draft; + } + case 'terminal': + return { + source: 'terminal', + fileLabel: payload.terminalLabel, + startLine: payload.startLine, + endLine: payload.endLine, + code: payload.output, + language: '', + text: '', + terminalId: payload.terminalId, + }; + case 'browser-annotation': + return { + source: 'preview-annotation', + fileLabel: payload.pageUrl, + startLine: 0, + endLine: 0, + code: payload.prompt, + language: '', + text: payload.text, + }; + case 'pr-comment': + return { source: 'pr-comment', fileLabel: payload.label, startLine: 0, endLine: 0, code: payload.body, language: '', text: payload.text }; + case 'pr-check': + return { source: 'pr-check', fileLabel: payload.label, startLine: 0, endLine: 0, code: payload.output, language: '', text: payload.text }; + case 'file-quote': + return { + source: 'file-quote', + fileLabel: payload.fileLabel, + startLine: payload.startLine ?? 0, + endLine: payload.endLine ?? 0, + code: payload.quote, + language: '', + text: payload.text, + }; + case 'chat-quote': + return { + source: 'chat-quote', + fileLabel: payload.messageId ?? '', + startLine: 0, + endLine: 0, + code: payload.quote, + language: '', + text: payload.text, + }; + case 'github-issue': + case 'github-pr': + case 'linear-issue': + return null; + } +} diff --git a/packages/ui/src/sync/session-actions.ts b/packages/ui/src/sync/session-actions.ts index b1e5aa2f..c208e0cc 100644 --- a/packages/ui/src/sync/session-actions.ts +++ b/packages/ui/src/sync/session-actions.ts @@ -16,6 +16,8 @@ import { registerSessionDirectory } from "./sync-refs" import { useGlobalSessionStatusStore } from "./global-session-status" import { recordSendFailure } from "./send-failure-log" import { isSyntheticPart } from "@/lib/messages/synthetic" +import { draftFromContextPayload, readContextPart, type ContextCarrierPart } from "@/lib/messages/contextParts" +import { useInlineCommentDraftStore, type InlineCommentDraftTarget } from "@/stores/useInlineCommentDraftStore" import { materializeSessionSnapshots } from "./materialization" import { stripMessageDiffSnapshots, stripSessionDiffSnapshots } from "./sanitize" import { sessionEvents } from "@/lib/sessionEvents" @@ -598,6 +600,32 @@ function restoreFilePartsToInput(fileParts: Array>): voi } } +/** + * Put a message's attached context (review comments, quotes, terminal + * selections, annotations) back on the composer chips. + * + * Context rides out as synthetic parts carrying structured metadata, so a + * reverted or forked message can be rebuilt into the drafts it came from. + * Without this the context is simply gone: the message is pulled back into the + * composer with its text and files, but the comments attached to it are not. + * + * The target's existing drafts are replaced, matching how text and file + * attachments are restored — the composer ends up as the message was sent. + */ +function restoreContextPartsToInput( + parts: readonly ContextCarrierPart[], + target: InlineCommentDraftTarget, +): void { + const store = useInlineCommentDraftStore.getState() + store.clearDrafts(target) + for (const part of parts) { + const payload = readContextPart(part) + if (!payload) continue + const draft = draftFromContextPayload(payload) + if (draft) store.addDraft(target, draft) + } +} + /** * Server-confirmed directory that owns a session, from the session record * (`directory`, then `project.worktree`). Mirrors the authoritative source in @@ -1985,6 +2013,7 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro const targetMsg = messages.find((m) => m.id === messageId) let messageText = "" let submittedFileParts: Array> = [] + let submittedContextParts: readonly ContextCarrierPart[] = [] if (targetMsg && targetMsg.role === "user") { const parts = state.part[messageId] ?? [] const textParts = parts.filter((p) => p.type === "text" && !isSyntheticPart(p)) @@ -1996,6 +2025,9 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro // Exclude synthetic file parts (server-generated file content that should // not be restored to the composer). submittedFileParts = parts.filter((p) => p.type === "file" && !isSyntheticPart(p)) as Array> + // Attached context (review comments, quotes, terminal selections) rides in + // synthetic text parts and belongs back on the composer chips. + submittedContextParts = parts } // Optimistically set only the revert marker. Keep messages and parts in the @@ -2023,6 +2055,10 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro const prevInputAttachments = [...useInputStore.getState().attachedFiles] const prevInputText = useInputStore.getState().pendingInputText const prevInputMode = useInputStore.getState().pendingInputMode + const draftTarget: InlineCommentDraftTarget | null = directory + ? { directory, sessionKey: sessionId } + : null + const prevDrafts = draftTarget ? useInlineCommentDraftStore.getState().getDrafts(draftTarget) : [] // Restore reverted message text and file attachments to input if (messageText) { @@ -2036,6 +2072,7 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro // Clear existing attachments first — previous revert's attachments // must not carry over, even when the current message has no files. restoreFilePartsToInput(submittedFileParts) + if (draftTarget) restoreContextPartsToInput(submittedContextParts, draftTarget) // Call SDK and merge authoritative result into store try { @@ -2070,6 +2107,10 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro pendingInputMode: prevInputMode, attachedFiles: prevInputAttachments, }) + if (draftTarget) { + useInlineCommentDraftStore.getState().clearDrafts(draftTarget) + useInlineCommentDraftStore.getState().restoreDrafts(draftTarget, prevDrafts) + } throw err } } @@ -2192,6 +2233,11 @@ export async function forkFromMessage(sessionId: string, messageId: string): Pro } // Clear existing attachments and restore file parts from the forked message. restoreFilePartsToInput(fileParts) + // The forked session is a fresh draft target, so the attached context of the + // forked message follows the text into its composer. + if (directory) { + restoreContextPartsToInput(parts, { directory, sessionKey: forkedSession.id }) + } } export async function fetchMessagesForSession(sessionID: string, directory?: string | null): Promise {