Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [1.13.7] - 2026-06-28

- Chat: with tool calls (such as Bash and Edit) shown expanded by default, scrolling no longer twitches, and slow scrolling no longer jumps past several messages.
- Mobile: in long conversations, older messages now load before you reach the very top, and fast scrolling no longer leaves blank gaps where messages briefly disappear until you scroll back.
- Mobile: the model and agent buttons in the composer are now borderless and cleaner, show the provider logo next to the model name, and shorten long names with an ellipsis; in the model picker the thinking-variant control is plain text with a chevron and each row's controls line up.
- Mobile: interface labels (the model and agent selectors and other small labels) are back to their previous size after 1.13.6 shrank them too much.
- Providers: the Add provider form stays open while provider data refreshes or a model is picked in the background, instead of snapping back to an existing provider.
- CLI: `openchamber update` works again after a missing helper broke the command.

## [1.13.6] - 2026-06-28

- Chat: scrolling in conversations now stays steady while sending, queueing, streaming, switching sessions, and loading older messages.
- Chat: selecting a user-installed skill from the slash command menu now invokes the skill and injects its content, instead of inserting the skill name as plain text.
- Context Panel: chat tabs now use the session title and mark the open chat as seen while you are viewing it.
- Desktop/macOS: the Dock icon can now show a badge count for chats with unseen activity, with a new Appearance setting to turn it off.
- Context Panel: Browser and Preview tabs no longer accumulate duplicate auth tokens in their URLs after reloads or navigation.
- UI: typography classes (ui-header, ui-label, meta, micro) now actually shrink on mobile viewports — they previously rendered at the same size as desktop despite the mobile clamp rules (thanks to @foundryseven).

## [1.13.5] - 2026-06-27

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openchamber-monorepo",
"version": "1.13.6",
"version": "1.13.7",
"description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openchamber/electron",
"version": "1.13.6",
"version": "1.13.7",
"private": true,
"description": "Electron desktop runtime for OpenChamber",
"author": "OpenChamber",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openchamber/ui",
"version": "1.13.6",
"version": "1.13.7",
"private": true,
"type": "module",
"main": "src/main.tsx",
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4514,7 +4514,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
{isMobile ? (
<>
<div className="flex w-full items-center justify-between gap-x-1.5">
<div className="flex items-center gap-x-1.5">
<div className="composer-mobile-actions flex items-center gap-x-2 pl-1">
<MobileSessionPanelTrigger
footerIconButtonClass={footerIconButtonClass}
iconSizeClass={iconSizeClass}
Expand All @@ -4539,7 +4539,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
/>
</div>
<div className="flex items-center min-w-0 gap-x-1 justify-end">
<div className="flex items-center gap-x-1 min-w-0 max-w-[60vw] flex-shrink">
<div className="flex items-center gap-x-2 min-w-0 max-w-[60vw] flex-shrink">
<MemoMobileModelButton onOpenModel={() => handleOpenMobilePanel('model')} className="min-w-0 flex-shrink" />
<MemoMobileAgentButton
onOpenAgentPanel={handleOpenAgentPanel}
Expand Down
24 changes: 10 additions & 14 deletions packages/ui/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,22 @@ import type { StreamPhase } from './message/types';
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
import { useSessionParts } from '@/sync/sync-context';
import type { ReviewTransferDirection } from '@/lib/reviewFlow';
import { isMobileSurfaceRuntime } from '@/lib/runtimeSurface';

const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = 5;
const EMPTY_STATIC_ENTRY_MESSAGES: ChatMessageEntry[] = [];
const EMPTY_UNGROUPED_MESSAGE_IDS = new Set<string>();
const MESSAGE_LIST_BUFFER_SIZE = 900;
// Touch surfaces fling-scroll natively and dispatch scroll events less often
// than the virtualizer can repaint, so a desktop-sized buffer leaves blank gaps
// during momentum that only fill once measurement catches up. A larger overscan
// keeps more rows mounted around the viewport so fast flings stay populated.
const MOBILE_MESSAGE_LIST_BUFFER_SIZE = 2400;
const resolveMessageListBufferSize = (): number => (
isMobileSurfaceRuntime() ? MOBILE_MESSAGE_LIST_BUFFER_SIZE : MESSAGE_LIST_BUFFER_SIZE
);
const TIMELINE_CACHE_LIMIT = 16;

const estimateHistoryEntryHeight = (entry: RenderEntry | undefined): number => {
if (!entry) {
return 160;
}

if (entry.kind === 'turn') {
return 180 + Math.min(entry.turn.assistantMessages.length, 4) * 100;
}

return 140;
};

const sameKeys = (a: readonly string[] | undefined, b: readonly string[] | undefined): boolean => {
if (a === b) return true;
if (!a || !b) return false;
Expand Down Expand Up @@ -982,8 +979,7 @@ const StaticHistoryList = React.memo(({ entries, shouldVirtualize, contentRef, s
ref={virtualizerRef}
data={entries}
cache={virtualCache}
itemSize={virtualCache ? undefined : estimateHistoryEntryHeight(undefined)}
bufferSize={MESSAGE_LIST_BUFFER_SIZE}
bufferSize={resolveMessageListBufferSize()}
shift={shift}
scrollRef={scrollRef}
>
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/src/components/chat/MobileAgentButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export const MobileAgentButton: React.FC<MobileAgentButtonProps> = ({ onCycleAge
onPointerLeave={handlePointerLeave}
onContextMenu={(e) => e.preventDefault()}
className={cn(
'inline-flex min-w-0 items-center select-none',
'rounded-lg border border-border/50 px-1.5',
'inline-flex min-w-0 items-stretch select-none',
'rounded-lg',
'typography-micro font-medium',
'focus:outline-none hover:bg-[var(--interactive-hover)]',
'touch-none',
Expand All @@ -88,7 +88,9 @@ export const MobileAgentButton: React.FC<MobileAgentButtonProps> = ({ onCycleAge
}}
title={agentLabel}
>
<span className="truncate">{agentLabel}</span>
<span className="flex h-full w-full min-w-0 items-center">
<span className="truncate">{agentLabel}</span>
</span>
</button>
);
};
13 changes: 9 additions & 4 deletions packages/ui/src/components/chat/MobileModelButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { cn } from '@/lib/utils';
import { useConfigStore } from '@/stores/useConfigStore';
import { getModelDisplayName } from './mobileControlsUtils';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { useI18n } from '@/lib/i18n';

interface MobileModelButtonProps {
Expand All @@ -12,6 +13,7 @@ interface MobileModelButtonProps {
export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenModel, className }) => {
const { t } = useI18n();
const currentModelId = useConfigStore((state) => state.currentModelId);
const currentProviderId = useConfigStore((state) => state.currentProviderId);
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
const currentProvider = getCurrentProvider();
const modelLabel = getModelDisplayName(currentProvider, currentModelId, t('chat.modelControls.selectModel'));
Expand All @@ -21,17 +23,20 @@ export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenMode
type="button"
onClick={onOpenModel}
className={cn(
'inline-flex min-w-0 items-center justify-center',
'rounded-lg border border-border/50 px-1.5',
'inline-flex min-w-0 items-stretch',
'rounded-lg',
'typography-micro font-medium text-foreground/80',
'focus:outline-none hover:bg-[var(--interactive-hover)]',
className
)}
style={{ height: '26px', maxHeight: '26px', minHeight: '26px' }}
title={modelLabel}
>
<span className="min-w-0 max-w-full overflow-x-auto whitespace-nowrap scrollbar-hidden">
{modelLabel}
<span className="flex h-full w-full min-w-0 items-center gap-1">
{currentProviderId ? (
<ProviderLogo providerId={currentProviderId} className="size-4 flex-shrink-0" />
) : null}
<span className="truncate">{modelLabel}</span>
</span>
</button>
);
Expand Down
16 changes: 8 additions & 8 deletions packages/ui/src/components/chat/ModelControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1671,7 +1671,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
isSelected && 'bg-interactive-selection/15 text-interactive-selection-foreground'
)}
>
<div className="flex items-start gap-2 px-2 py-1.5">
<div className="flex items-center gap-2 px-2 py-1.5">
<button
type="button"
onClick={() => handleMobileModelApply(providerId, modelId, resolvedVariant)}
Expand All @@ -1680,15 +1680,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary rounded-lg'
)}
>
{showProviderLogo ? (
<ProviderLogo providerId={providerId} className="mt-0.5 size-3.5 flex-shrink-0" />
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-start gap-2">
<div className="flex min-w-0 items-center gap-1.5">
{showProviderLogo ? (
<ProviderLogo providerId={providerId} className="size-3.5 flex-shrink-0" />
) : null}
<span className="typography-meta font-medium text-foreground truncate">
{getModelDisplayName(model)}
</span>
{isSelected ? <Icon name="check" className="mt-0.5 size-4 flex-shrink-0 text-primary" /> : null}
{isSelected ? <Icon name="check" className="size-4 flex-shrink-0 text-primary" /> : null}
</div>
{contextText || indicatorIcons.length > 0 ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
Expand Down Expand Up @@ -1722,15 +1722,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<button
type="button"
onClick={() => setExpandedMobileModelKey((prev) => prev === rowKey ? null : rowKey)}
className="flex items-center gap-1 rounded-lg border border-border/40 px-2 py-1 typography-micro font-medium text-muted-foreground hover:bg-interactive-hover/50 flex-shrink-0"
className="flex items-center gap-0.5 typography-micro font-medium text-muted-foreground hover:text-foreground flex-shrink-0"
aria-expanded={isExpanded}
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
>
<span className="whitespace-nowrap">{variantLabel}</span>
{isExpanded ? <Icon name="arrow-down-s" className="size-3.5" /> : <Icon name="arrow-right-s" className="size-3.5" />}
</button>
) : null}
<div className="flex flex-shrink-0 items-start gap-1.5">
<div className="flex flex-shrink-0 items-center gap-1.5">
<button
type="button"
onClick={(event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ export interface UseChatTimelineControllerResult {

const TURN_MODEL_CACHE_MAX = 30
const HISTORY_SCROLL_THRESHOLD = 200
// On touch surfaces the user can drag continuously toward the top, and
// loadEarlier is an async (network) fetch. A 200px lead is enough on desktop
// (wheel + fast render) but the finger can outrun an in-flight fetch on mobile
// and hit the very top before history lands. Give touch a much larger,
// viewport-relative head start so the fetch completes before the top is
// reached, regardless of how fast the user drags.
const MOBILE_HISTORY_SCROLL_THRESHOLD_MIN = 1200
const MOBILE_HISTORY_SCROLL_VIEWPORT_FACTOR = 2

const resolveHistoryScrollThreshold = (clientHeight: number): number => {
if (!isMobileSurfaceRuntime()) {
return HISTORY_SCROLL_THRESHOLD
}
return Math.max(
MOBILE_HISTORY_SCROLL_THRESHOLD_MIN,
clientHeight * MOBILE_HISTORY_SCROLL_VIEWPORT_FACTOR,
)
}
const VSCODE_TURN_MODEL_CACHE_MAX = 4
const VSCODE_TURN_MODEL_CACHE_MAX_MESSAGES = 30
const MOBILE_TURN_MODEL_CACHE_MAX = 4
Expand Down Expand Up @@ -521,7 +539,7 @@ export const useChatTimelineController = ({
const container = scrollRef.current;
if (!container) return;
if (isPinnedRef.current) return;
if (container.scrollTop >= HISTORY_SCROLL_THRESHOLD) return;
if (container.scrollTop >= resolveHistoryScrollThreshold(container.clientHeight)) return;
if (!historySignalsRef.current.canLoadEarlier) return;
if (isLoadingOlderRef.current || pendingRevealWorkRef.current) return;

Expand Down
18 changes: 17 additions & 1 deletion packages/ui/src/components/chat/message/parts/ToolPart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,30 @@ const scheduleDeferredToolBodyMount = (fn: () => void) => {
};

const useDeferredExpandedContent = (isExpanded: boolean) => {
const [shouldRender, setShouldRender] = React.useState(false);
// If the tool is expanded when the row first mounts (e.g. "show tools open
// by default", or scrolling a default-open tool back into a virtualized
// view), render the body SYNCHRONOUSLY so the virtualizer measures the real
// height immediately. Deferring it would let the row mount short and grow a
// frame later, which makes the virtualizer compensate scroll and lurch the
// viewport past several messages on slow scroll. Only defer LATER
// user-initiated expansions, where instant single-item feedback isn't worth
// blocking the click on a heavy body render.
const [shouldRender, setShouldRender] = React.useState(isExpanded);
const mountedRef = React.useRef(false);

React.useEffect(() => {
if (!isExpanded) {
mountedRef.current = true;
setShouldRender(false);
return;
}

if (!mountedRef.current) {
mountedRef.current = true;
setShouldRender(true);
return;
}

return scheduleDeferredToolBodyMount(() => {
setShouldRender(true);
});
Expand Down
Loading
Loading