feat: massive chat reliability + UX pass (web/desktop/mobile/vscode) (#593)

## Added Features
- Add VS Code save-as-image flow for assistant messages via webview bridge + native save dialog.
- Add hourly desktop update checks after startup.
- Add new tool output display mode: `Changes` (auto-expand edit/write/patch only; keep activity expanded; mode guidance text).
- Add GitHub PR attachment flow in chat input with PR picker + attached PR chip/details.
- Add mobile overlay presentation for GitHub Issue and PR pickers (shared with desktop picker content).

## Fixes
- Save-gate project icon updates until explicit Save; allow icon removal with same save-gated behavior.
- Restore clickable chat action buttons in sticky header mode (desktop + Firefox hit-target issue).
- Clamp sticky user messages to bounded chat height and allow internal scrolling.
- Prevent drawer context crash during iPad/tablet orientation switching.
- Improve text-selection action menu placement on narrow screens.
- Move assistant message time into clock tooltip; keep duration display clean.
- Hide `Link GitHub Issue` row in VS Code chat input area (GitHub flow is not yet ready there).
- Remove laggy close animation in text-selection popover; keep open motion/positioning behavior.
- Fetch branches when picker opens and cache empty; show loading state instead of false “No branches found”.
- Fix share-image export metadata rendering (theme background resolution, timestamp rendering, footer alignment).
- Scope MCP services status/toggles to active directory to avoid cross-project leakage.
- Improve long user-message clamp behavior (40% cap variant, hidden scrollbar, scroll shadows, expansion detection).
- Fix desktop `Check for Updates` menu handler; prevent duplicate checks; show clear success/error toasts.
- Stabilize long user-message scrolling behavior (follow-up hardening).
- Avoid premature web update failure on slower servers.
- Restore user message image previews + fullscreen gallery navigation payload.
- Repair desktop chat drag-and-drop image attachments when native drop coords are missing.
- Move GitHub issue linking entry into Add attachment menu.
- Align header context usage percentage visuals with context panel.
- Align `@` file search with active project in all runtimes.
- Route `@` file discovery through OpenCode SDK `find.files`; remove legacy `/api/fs/search` reliance.
- Make chat `@` mention behavior consistent with files-style behavior.
- Keep status-row todos in stable order after status changes; add compact status icons; replace noisy priority labels.

## Refactors / UX Consistency
- Simplify chat attachment model and remove project file picker path.
- Keep composer focused on `@` mention file flow.
- Use direct `Attach files` action in VS Code instead of attachment dropdown path.
- Unify issue/PR picker behavior between desktop and mobile overlays.
This commit is contained in:
Bohdan Triapitsyn
2026-03-04 01:41:01 +02:00
committed by GitHub
parent ca18b8be0f
commit 79143bff4c
42 changed files with 2212 additions and 1477 deletions
+52 -15
View File
@@ -1,7 +1,15 @@
import React from "react";
import { RiArrowUpSLine, RiArrowDownSLine, RiCloseCircleLine } from "@remixicon/react";
import {
RiArrowDownSLine,
RiArrowUpDoubleLine,
RiArrowUpSLine,
RiCheckboxCircleLine,
RiCloseCircleLine,
RiRecordCircleLine,
RiTimeLine,
} from "@remixicon/react";
import { cn } from "@/lib/utils";
import { useTodoStore, type TodoItem, type TodoStatus } from "@/stores/useTodoStore";
import { useTodoStore, type TodoItem, type TodoPriority, type TodoStatus } from "@/stores/useTodoStore";
import { useSessionStore } from "@/stores/useSessionStore";
import { useUIStore } from "@/stores/useUIStore";
import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
@@ -22,6 +30,18 @@ const statusConfig: Record<TodoStatus, { textClassName: string }> = {
},
};
const priorityClassName: Record<TodoPriority, string> = {
high: "text-[var(--status-warning)]",
medium: "text-muted-foreground",
low: "text-muted-foreground/70",
};
const priorityIcon: Record<TodoPriority, React.ReactNode> = {
high: <RiArrowUpDoubleLine className="h-3.5 w-3.5" aria-hidden="true" />,
medium: <RiArrowUpSLine className="h-3.5 w-3.5" aria-hidden="true" />,
low: <RiArrowDownSLine className="h-3.5 w-3.5" aria-hidden="true" />,
};
interface TodoItemRowProps {
todo: TodoItem;
}
@@ -29,8 +49,18 @@ interface TodoItemRowProps {
const TodoItemRow: React.FC<TodoItemRowProps> = ({ todo }) => {
const config = statusConfig[todo.status] || statusConfig.pending;
const statusIcon =
todo.status === "in_progress" ? (
<RiRecordCircleLine className="h-3.5 w-3.5 text-[var(--status-info)]" aria-hidden="true" />
) : todo.status === "completed" ? (
<RiCheckboxCircleLine className="h-3.5 w-3.5 text-[var(--status-success)]" aria-hidden="true" />
) : (
<RiTimeLine className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
);
return (
<div className="flex items-start min-w-0 py-0.5">
<div className="flex items-start min-w-0 py-0.5 gap-2">
<span className="mt-0.5 flex-shrink-0">{statusIcon}</span>
<span
className={cn(
"flex-1 typography-ui-label",
@@ -39,6 +69,15 @@ const TodoItemRow: React.FC<TodoItemRowProps> = ({ todo }) => {
>
{todo.content}
</span>
<span
className={cn(
"typography-meta flex-shrink-0",
priorityClassName[todo.priority] ?? priorityClassName.medium
)}
title={`${todo.priority} priority`}
>
{priorityIcon[todo.priority] ?? priorityIcon.medium}
</span>
</div>
);
};
@@ -89,18 +128,10 @@ export const StatusRow: React.FC<StatusRowProps> = ({
}
}, [currentSessionId, loadTodos]);
// Filter out cancelled todos for display, sort by status priority
// Filter out cancelled todos for display and keep original order.
// This prevents items from jumping around when status changes.
const visibleTodos = React.useMemo(() => {
const statusOrder: Record<TodoStatus, number> = {
in_progress: 0,
pending: 1,
completed: 2,
cancelled: 3,
};
return [...todos]
.filter((todo) => todo.status !== "cancelled")
.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
return todos.filter((todo) => todo.status !== "cancelled");
}, [todos]);
// Find the current active todo (first in_progress, or first pending)
@@ -119,6 +150,12 @@ export const StatusRow: React.FC<StatusRowProps> = ({
return { completed, total };
}, [todos]);
const statusSummary = React.useMemo(() => {
const active = visibleTodos.filter((t) => t.status === "in_progress").length;
const left = visibleTodos.filter((t) => t.status === "in_progress" || t.status === "pending").length;
return { active, left };
}, [visibleTodos]);
const hasActiveTodos = visibleTodos.some((t) => t.status === "in_progress" || t.status === "pending");
// Original logic from ChatInput
const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive);
@@ -178,7 +215,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
<span className="typography-ui-label">Tasks</span>
)}
<span className="typography-meta">
{progress.completed}/{progress.total}
{statusSummary.active} active · {statusSummary.left} left
</span>
{isExpanded ? (
<RiArrowUpSLine className="h-3.5 w-3.5" />