Skip to content

Commit 33749fb

Browse files
committed
Merge main into loop/starter-suggestions
Resolve merge conflict in docs/loop/feature-ledger.md: adopt main's canonical ledger and prepend this PR's starter-suggestions (#73) row. No code changes.
2 parents 358e92f + 3a4f438 commit 33749fb

10 files changed

Lines changed: 227 additions & 15 deletions

File tree

apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,8 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
585585
const [visibleTurnCount, setVisibleTurnCount] = React.useState(TURNS_PER_PAGE)
586586
// Sticky-bottom: When true, auto-scroll on content changes. Toggled by user scroll behavior.
587587
const isStickToBottomRef = React.useRef(true)
588+
// Show a floating "jump to latest" button when the user has scrolled far from the bottom.
589+
const [showScrollToBottom, setShowScrollToBottom] = React.useState(false)
588590
// Mirror isFocusedPanel into a ref so the ResizeObserver closure reads the latest value
589591
const isFocusedPanelRef = React.useRef(isFocusedPanel)
590592
isFocusedPanelRef.current = isFocusedPanel
@@ -1176,6 +1178,9 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
11761178
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
11771179
// 20px threshold for "at bottom" detection
11781180
isStickToBottomRef.current = distanceFromBottom < 20
1181+
// Reveal the jump-to-latest button once the user is well away from the bottom
1182+
// (200px hysteresis so it doesn't flicker while reading near the end).
1183+
setShowScrollToBottom(distanceFromBottom > 200)
11791184

11801185
// Load more turns when scrolling near top (within 100px)
11811186
if (scrollTop < 100) {
@@ -1206,6 +1211,13 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
12061211
return () => viewport.removeEventListener('scroll', handleScroll)
12071212
}, [handleScroll])
12081213

1214+
// Jump back to the latest message and resume sticky-bottom auto-scroll.
1215+
const scrollToBottom = React.useCallback(() => {
1216+
isStickToBottomRef.current = true
1217+
setShowScrollToBottom(false)
1218+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
1219+
}, [])
1220+
12091221
// Auto-scroll using ResizeObserver for streaming content
12101222
// Initial scroll is handled by ScrollOnMount (useLayoutEffect, before paint)
12111223
React.useEffect(() => {
@@ -1219,6 +1231,7 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
12191231
if (isSessionSwitch) {
12201232
isStickToBottomRef.current = true
12211233
setVisibleTurnCount(TURNS_PER_PAGE)
1234+
setShowScrollToBottom(false)
12221235
}
12231236

12241237
// Debounced scroll for streaming - waits for layout to settle
@@ -1709,7 +1722,7 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
17091722
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 32px, black calc(100% - 32px), transparent 100%)'
17101723
}}
17111724
>
1712-
<ScrollArea className="h-full min-w-0" viewportRef={scrollViewportRef}>
1725+
<ScrollArea className="h-full min-w-0" viewportRef={scrollViewportRef} data-testid="chat-transcript">
17131726
<div className={cn(
17141727
CHAT_LAYOUT.maxWidth,
17151728
"mx-auto min-w-0",
@@ -2084,6 +2097,32 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
20842097
</div>
20852098
</ScrollArea>
20862099
</div>
2100+
2101+
{/* Jump-to-latest: floating button shown when scrolled up from the bottom */}
2102+
<AnimatePresence>
2103+
{showScrollToBottom && (
2104+
<motion.button
2105+
type="button"
2106+
onClick={scrollToBottom}
2107+
aria-label={t('chat.scrollToBottom')}
2108+
title={t('chat.scrollToBottom')}
2109+
data-testid="scroll-to-bottom"
2110+
initial={{ opacity: 0, y: 8 }}
2111+
animate={{ opacity: 1, y: 0 }}
2112+
exit={{ opacity: 0, y: 8 }}
2113+
transition={{ duration: 0.12, ease: 'easeOut' }}
2114+
className={cn(
2115+
"absolute bottom-4 left-1/2 -translate-x-1/2 z-10",
2116+
"flex items-center justify-center h-9 w-9 rounded-full",
2117+
"bg-background/90 backdrop-blur border border-border shadow-md",
2118+
"text-muted-foreground hover:text-foreground hover:bg-accent",
2119+
"transition-colors"
2120+
)}
2121+
>
2122+
<ChevronDown className="h-5 w-5" />
2123+
</motion.button>
2124+
)}
2125+
</AnimatePresence>
20872126
</div>
20882127

20892128
{/* === INPUT CONTAINER: FreeForm or Structured Input === */}

docs/loop/feature-ledger.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ log, not the system of record.
1212

1313
| status | meaning |
1414
| --- | --- |
15+
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). |
1516
| `proposed` | Identified as a gap, not started. |
1617
| `in-progress` | Issue filed, branch open, implementation underway. |
1718
| `pr-open` | PR submitted, awaiting human review/merge. |
@@ -33,20 +34,8 @@ log, not the system of record.
3334
| slug | title | source | feasibility | status | issue | pr | branch | updated | notes |
3435
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
3536
| starter-suggestions | Starter prompt suggestions on the empty conversation | Claude Code Desktop "prompt suggestions" / ChatGPT & Codex example prompts | frontend-only | pr-open | [#72](https://github.com/modelstudioai/openwork/issues/72) | [#73](https://github.com/modelstudioai/openwork/pull/73) | loop/starter-suggestions | 2026-07-09 | New `EmptyStateSuggestions` chip row rendered under the centered empty-state composer in `ChatDisplay` (only while composer is empty). Clicking a chip seeds the composer via the existing controlled draft channel (`onInputChange`) + focuses it — does NOT auto-send. 4 suggestions, 8 new i18n keys ×7 locales. typecheck:all / `bun test` zero-delta vs main (11 electron-typecheck + 56-test pre-existing baseline byte-identical); renderer build ✅; i18n parity ✅; lint 0 errors on touched files. CDP assertion `e2e/assertions/starter-suggestions.assert.ts` included; **could not execute locally** (Electron binary egress-blocked: `github.com` releases 403). |
36-
| copy-message | "Copy" (copy-as-Markdown) action on assistant/agent responses | Codex desktop "Copy as Markdown" (openai/codex #2880, #17241) + Claude/ChatGPT desktop per-message copy | frontend-only | pr-open | [#70](https://github.com/modelstudioai/openwork/issues/70) | [#71](https://github.com/modelstudioai/openwork/pull/71) | loop/copy-message | 2026-07-08 | User messages + code blocks were already copyable, but full assistant responses had only a pop-out button — no copy. Extracted `AssistantMessage` component in `ChatDisplay.tsx` (owns `copied` state, mirrors the existing `ErrorMessage` extraction) with a hover copy button next to pop-out; copies raw `message.content` via `navigator.clipboard.writeText`, 2s "copied" check state, `toast.copyFailed` on error. **Zero new i18n keys** (reuses `common.copy`/`common.copied`/`toast.copyFailed`). Added a general `seed` hook to the e2e harness (`app.ts`/`runner.ts`) so assertions can pre-seed an on-disk session the embedded `SessionManager` loads on boot (no backend). typecheck:all zero-delta; `bun test` 56-failure set byte-identical to main; renderer build ✅; i18n parity ✅ (1544 keys); eslint 0 errors. CDP assertion (`copy-message.assert.ts`) seeds a user+assistant session, opens it, clicks the copy button, and asserts the exact response markdown reaches the (stubbed) clipboard + the button enters its copied state; **could not run locally** (org egress policy 403s the Electron binary download — same block as prior rounds). |
37-
| interface-zoom | Interface zoom (⌘+ / ⌘- / ⌘0) to scale the whole app | Claude/VS Code/Codex desktop zoom | frontend-only | pr-open | [#68](https://github.com/modelstudioai/openwork/issues/68) | [#69](https://github.com/modelstudioai/openwork/pull/69) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
38-
| increase-contrast | "Increase contrast" accessibility setting in Appearance | macOS / Windows / Claude desktop increase-contrast | frontend-only | pr-open | [#66](https://github.com/modelstudioai/openwork/issues/66) | [#67](https://github.com/modelstudioai/openwork/pull/67) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
39-
| chat-text-size | "Chat text size" (Small / Default / Large) in Appearance | ChatGPT / Claude desktop text-size | frontend-only | pr-open | [#64](https://github.com/modelstudioai/openwork/issues/64) | [#65](https://github.com/modelstudioai/openwork/pull/65) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
40-
| conversation-width | "Conversation width" (Comfortable / Wide / Full) in Appearance | ChatGPT / Claude desktop conversation width | frontend-only | pr-open | [#62](https://github.com/modelstudioai/openwork/issues/62) | [#63](https://github.com/modelstudioai/openwork/pull/63) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
41-
| composer-word-count | Live word / character count in the chat composer | ChatGPT / editors composer counters | frontend-only | pr-open | [#60](https://github.com/modelstudioai/openwork/issues/60) | [#61](https://github.com/modelstudioai/openwork/pull/61) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
42-
| shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | VS Code / Codex desktop shortcuts search | frontend-only | pr-open | [#58](https://github.com/modelstudioai/openwork/issues/58) | [#59](https://github.com/modelstudioai/openwork/pull/59) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
43-
| recent-commands | Surface recently-used commands in the Command Palette (⌘K) | Claude Code Desktop / VS Code recent commands | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
44-
| thinking-menu-shortcut | Keyboard shortcut (⌘⇧E) to open the composer thinking menu | Claude Code Desktop effort menu ⌘⇧E | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-menu-shortcut | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
45-
| prompt-history | Recall previously-sent prompts with Up / Down in the composer | Claude Code / ChatGPT / shell history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) || 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
46-
| session-search-shortcut | Cmd+F / `app.search` should activate in-conversation session search | OpenWork internal bug report | frontend-only | proposed | [#43](https://github.com/modelstudioai/openwork/issues/43) ||| 2026-07-09 | Open loop-bot issue (bug), no PR yet. Not selected this round. |
47-
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | merged | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-08 | **Merged** into `main` (2026-07-06). Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. |
37+
| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code Desktop / ChatGPT / Codex desktop jump-to-latest affordance | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Reuses existing `ChatDisplay` scroll state (`distanceFromBottom`, `isStickToBottomRef`, `messagesEndRef`); floating `AnimatePresence` button shown when >200px from bottom. One new i18n key `chat.scrollToBottom` across all 6 locales. Added a reusable `seed(profileDirs)` hook to the e2e harness (`app.ts`/`runner.ts`) so assertions can pre-seed an on-disk session (backend-independent) — the scroll assertion seeds a 40-message session, opens it, scrolls up, asserts the button appears, clicks it, asserts return-to-bottom + hide. typecheck/`bun test` zero-delta vs main (11 pre-existing tsc errors, 56 pre-existing test fails, identical sets); renderer build ✅. **CDP could not run locally**: the Electron binary download is 403'd by the sandbox egress policy (github release host), so the app can't launch here — assertion transpiles and is included for CI/reviewer. |
38+
| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-02 | Merged into `main`. `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). typecheck/`bun test` zero-delta vs main. |
4839
| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. |
49-
| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. |
50-
| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |
5140
| command-palette | Global command palette (⌘K/Ctrl+K) to search & run any action | Claude Code Desktop ⌘K / VS Code & Codex ⌘⇧P / Linear ⌘K | frontend-only | merged | [#41](https://github.com/modelstudioai/openwork/issues/41) | [#42](https://github.com/modelstudioai/openwork/pull/42) | loop/command-palette | 2026-07-02 | Merged into `main`. Reuses action registry `execute()` + cmdk primitives; zero new i18n keys. CDP e2e 2/2 pass. typecheck/test +0 vs main. |
5241
| settings-search | Searchable/filterable settings navigation | Claude Code Desktop / VS Code / Codex desktop settings search | frontend-only | merged | [#39](https://github.com/modelstudioai/openwork/issues/39) | [#40](https://github.com/modelstudioai/openwork/pull/40) | loop/settings-search | 2026-07-01 | Merged into `main`. Filters `SettingsNavigator` by title+description; reuses `common.search`/`common.noResultsFound` (no new locale keys). Also hardened `e2e/app.ts` teardown (per-launch profile dir + setsid process-group kill) so multiple CDP assertions run under headless xvfb. |

0 commit comments

Comments
 (0)