-
Notifications
You must be signed in to change notification settings - Fork 1.4k
UI 布局修复:标题栏压缩、滚动条对齐、composer 精简 #3589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,3 +58,4 @@ Thumbs.db | |
| # Scratch / agent working dirs (never committed) | ||
| /tmp/ | ||
| /.codex/ | ||
| .alma-snapshots | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import { AlertTriangle, ArrowUp, Check, ChevronDown, Eye, FileText, Folder, Fold | |
| import { asArray } from "../lib/array"; | ||
| import { DedupIndex, sha256 } from "../lib/attachDedup"; | ||
| import { app, onFilesDropped } from "../lib/bridge"; | ||
| import { SPINNER_WORDS, useI18n } from "../lib/i18n"; | ||
| import { useI18n } from "../lib/i18n"; | ||
| import { clearLayoutSize, loadOptionalLayoutSize, saveLayoutSize } from "../lib/layoutPreferences"; | ||
| import type { CommandInfo, ComposerInsertRequest, DirEntry, EffortInfo, HistoryMessage, Mode, SessionMeta, SessionReference, SlashArgItem, SlashArgsResult, WorkspaceView } from "../lib/types"; | ||
| import { | ||
|
|
@@ -95,18 +95,6 @@ function loadComposerHeight(): number | null { | |
| return loadOptionalLayoutSize("composerHeight", clampComposerHeight); | ||
| } | ||
|
|
||
| function fmtTokens(n: number): string { | ||
| if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; | ||
| return String(n); | ||
| } | ||
|
|
||
| function fmtElapsed(ms: number): string { | ||
| const s = Math.floor(ms / 1000); | ||
| if (s < 60) return `${s}s`; | ||
| return `${Math.floor(s / 60)}m ${s % 60}s`; | ||
| } | ||
|
|
||
| // --- past:chats hover preview helpers (PR-C2) --- | ||
| // Pure formatting helpers used by the past:chats list tooltip. They never read | ||
| // from disk, never call PreviewSession — they only shape the data that already | ||
| // lives in the SessionMeta snapshot we fetched on entry. | ||
|
|
@@ -134,16 +122,6 @@ function pastChatTitle(session: SessionMeta): string { | |
| return session.title || session.topicTitle || session.preview || "Untitled"; | ||
| } | ||
|
|
||
| function useTick(on: boolean): number { | ||
| const [, setN] = useState(0); | ||
| useEffect(() => { | ||
| if (!on) return; | ||
| const id = window.setInterval(() => setN((n) => n + 1), 1000); | ||
| return () => window.clearInterval(id); | ||
| }, [on]); | ||
| return Date.now(); | ||
| } | ||
|
|
||
| function isImeKeyEvent( | ||
| e: KeyboardEvent<HTMLTextAreaElement>, | ||
| composing: boolean, | ||
|
|
@@ -265,9 +243,6 @@ export function Composer({ | |
| disabled, | ||
| decisionPending = false, | ||
| ready, | ||
| turnStartAt, | ||
| turnTokens, | ||
| retry, | ||
| workspaceRefreshSignal, | ||
| }: { | ||
| running: boolean; | ||
|
|
@@ -294,13 +269,9 @@ export function Composer({ | |
| // is nil before then), the available set changes when the workspace switches, | ||
| // and a completed turn may have installed skills or MCP prompts. | ||
| ready?: boolean; | ||
| turnStartAt?: number; | ||
| turnTokens?: number; | ||
| retry?: { attempt: number; max: number }; | ||
| workspaceRefreshSignal?: number; | ||
| }) { | ||
| const { t, locale } = useI18n(); | ||
| const now = useTick(running); | ||
| const { t } = useI18n(); | ||
| const [text, setText] = useState(""); | ||
| const [attachments, setAttachments] = useState<Attachment[]>([]); | ||
| const [workspaceRefs, setWorkspaceRefs] = useState<WorkspaceReference[]>([]); | ||
|
|
@@ -1267,17 +1238,6 @@ export function Composer({ | |
| { id: "plan", label: "plan", icon: <List size={13} /> }, | ||
| { id: "yolo", label: "yolo", icon: <AlertTriangle size={13} /> }, | ||
| ]; | ||
| const runActivity = retry | ||
| ? t("status.retrying", { attempt: retry.attempt, max: retry.max }) | ||
| : running && turnStartAt | ||
| ? (() => { | ||
| const elapsedMs = Math.max(0, now - turnStartAt); | ||
| const words = SPINNER_WORDS[locale]; | ||
| const word = words[Math.floor(elapsedMs / 3000) % words.length]; | ||
| const tok = turnTokens && turnTokens > 0 ? ` · ↓ ${fmtTokens(turnTokens)} ${t("status.tokens")}` : ""; | ||
| return `${word}… ${fmtElapsed(elapsedMs)}${tok}`; | ||
| })() | ||
| : null; | ||
| const hasWorkspace = Boolean(cwd); | ||
| const hasEffort = Boolean(effort?.supported); | ||
| const composerMetaClass = [ | ||
|
|
@@ -1502,35 +1462,6 @@ export function Composer({ | |
| /> | ||
| ) | ||
| )} | ||
| <div className="composer-toolbar"> | ||
| <div className="composer-modebar" role="toolbar" aria-label={t("composer.modeTitle")}> | ||
| {modeOptions.map((option) => ( | ||
| <button | ||
| key={option.id} | ||
| type="button" | ||
| className={`composer-modebar__item composer-modebar__item--${option.id}${mode === option.id ? " composer-modebar__item--active" : ""}`} | ||
| onClick={() => onSetMode(option.id)} | ||
| aria-pressed={mode === option.id} | ||
| disabled={disabled || running} | ||
| > | ||
| {option.icon} | ||
| <span>{option.label}</span> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| {runActivity && ( | ||
| <div className="composer-runstatus" role="status" aria-live="polite"> | ||
| <span className="composer-runstatus__dot" /> | ||
| <span className="composer-runstatus__text">{runActivity}</span> | ||
| <Tooltip label={t("composer.stop")}> | ||
| <button className="composer-runstatus__stop" type="button" onClick={handleCancel} disabled={decisionPending}> | ||
| <Square size={10} fill="currentColor" /> | ||
| <span>{t("composer.stopShort")}</span> | ||
| </button> | ||
| </Tooltip> | ||
| </div> | ||
| )} | ||
| </div> | ||
| {(attachments.length > 0 || workspaceRefs.length > 0 || sessionRefs.length > 0) && ( | ||
| <div className="composer-context" aria-label={t("composer.contextItems")}> | ||
| {attachments.map((a) => ( | ||
|
|
@@ -1654,7 +1585,6 @@ export function Composer({ | |
| onDragOver={onDragOver} | ||
| onDragLeave={onDragLeave} | ||
| > | ||
| <span className="composer__caret">{text.trimStart().startsWith("!") ? "$" : "›"}</span> | ||
| <textarea | ||
| ref={taRef} | ||
| className="composer__input" | ||
|
|
@@ -1678,17 +1608,6 @@ export function Composer({ | |
| rows={1} | ||
| disabled={disabled} | ||
| /> | ||
| {!running && ( | ||
| <Tooltip label={t("composer.send")}> | ||
| <button | ||
| className="composer__btn composer__btn--send" | ||
| onClick={submit} | ||
| disabled={submitting || pendingPaste > 0 || (!text.trim() && attachments.length === 0 && workspaceRefs.length === 0) || disabled} | ||
| > | ||
| <ArrowUp size={16} /> | ||
| </button> | ||
| </Tooltip> | ||
| )} | ||
| </div> | ||
| <div className={composerMetaClass}> | ||
| {cwd && ( | ||
|
|
@@ -1706,6 +1625,25 @@ export function Composer({ | |
| </button> | ||
| </div> | ||
| )} | ||
| <div className="composer-modebar" role="toolbar" aria-label={t("composer.modeTitle")}> | ||
| {modeOptions.map((option) => { | ||
| const isActive = mode === option.id; | ||
| const btn = ( | ||
| <button | ||
| key={option.id} | ||
| type="button" | ||
| className={`composer-modebar__item composer-modebar__item--${option.id}${isActive ? " composer-modebar__item--active" : ""}`} | ||
| onClick={() => onSetMode(option.id)} | ||
| aria-pressed={isActive} | ||
| disabled={disabled || running} | ||
| > | ||
| {option.icon} | ||
| {isActive && <span>{option.label}</span>} | ||
| </button> | ||
| ); | ||
| return isActive ? btn : <Tooltip key={option.id} label={option.label}>{btn}</Tooltip>; | ||
| })} | ||
| </div> | ||
| <div className="composer-meta__params"> | ||
| <div className="composer-meta__control composer-meta__control--model"> | ||
| <ModelSwitcher label={modelLabel} tabId={tabId} onPick={onSwitchModel} /> | ||
|
|
@@ -1716,6 +1654,25 @@ export function Composer({ | |
| </div> | ||
| )} | ||
| </div> | ||
| <div className="composer-meta__stop-wrap"> | ||
| {running ? ( | ||
| <Tooltip label={t("composer.stop")}> | ||
| <button className="composer-meta__action-btn composer-meta__action-btn--stop" type="button" onClick={handleCancel} disabled={decisionPending}> | ||
| <Square size={14} fill="currentColor" /> | ||
| </button> | ||
| </Tooltip> | ||
| ) : ( | ||
| <Tooltip label={t("composer.send")}> | ||
| <button | ||
| className="composer-meta__action-btn composer-meta__action-btn--send" | ||
| onClick={submit} | ||
|
Comment on lines
+1666
to
+1668
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The send/stop button was moved from Useful? React with 👍 / 👎. |
||
| disabled={submitting || pendingPaste > 0 || (!text.trim() && attachments.length === 0 && workspaceRefs.length === 0) || disabled} | ||
| > | ||
| <ArrowUp size={16} /> | ||
| </button> | ||
| </Tooltip> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,13 @@ | ||
| import { useState } from "react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { Tooltip } from "./Tooltip"; | ||
| import { useI18n } from "../lib/i18n"; | ||
| import { useI18n, SPINNER_WORDS } from "../lib/i18n"; | ||
| import type { BalanceInfo, ContextInfo, JobView, Mode, WireUsage } from "../lib/types"; | ||
|
|
||
| function fmtTokens(n: number): string { | ||
| if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; | ||
| return String(n); | ||
| } | ||
|
|
||
| // JobsChip is the status-bar background-jobs indicator: a count that opens an | ||
| // upward popover listing the running jobs (id · label · status), mirroring the | ||
| // ModelSwitcher's click-to-open pattern. With no jobs it still reserves a stable | ||
|
|
@@ -89,6 +94,7 @@ export function StatusBar({ | |
| mode, | ||
| cost, | ||
| currency, | ||
| turnTokens, | ||
| }: { | ||
| context: ContextInfo; | ||
| usage?: WireUsage; | ||
|
|
@@ -98,8 +104,19 @@ export function StatusBar({ | |
| mode: Mode; | ||
| cost?: number; | ||
| currency?: string; | ||
| turnTokens?: number; | ||
| }) { | ||
| const { t } = useI18n(); | ||
| const { t, locale } = useI18n(); | ||
| const [, setTick] = useState(0); | ||
| useEffect(() => { | ||
| if (!running) return; | ||
| const id = window.setInterval(() => setTick((n) => n + 1), 1000); | ||
| return () => window.clearInterval(id); | ||
| }, [running]); | ||
| const spinnerRunning = running; | ||
| const spinnerWord = spinnerRunning | ||
| ? SPINNER_WORDS[locale][Math.floor(Date.now() / 3000) % SPINNER_WORDS[locale].length] | ||
| : ""; | ||
| const pct = context.window ? Math.min(100, Math.round((context.used / context.window) * 100)) : null; | ||
| const compactPct = context.compactRatio ? Math.round(context.compactRatio * 100) : null; | ||
| const nowPct = nowRate(usage); | ||
|
|
@@ -132,7 +149,15 @@ export function StatusBar({ | |
| </span> | ||
| </Tooltip> | ||
| <span className="statusbar__spacer" /> | ||
| {mode === "plan" && <span className="statusbar__plan">{t("status.plan")}</span>} | ||
| <div className="statusbar__trailing"> | ||
| {spinnerRunning && ( | ||
| <span className="statusbar__spinner">{spinnerWord}…</span> | ||
| )} | ||
| {(turnTokens ?? 0) > 0 && ( | ||
| <span className="statusbar__tokens">↓ {fmtTokens(turnTokens ?? 0)} {t("status.tokens")}</span> | ||
|
Comment on lines
+156
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After any turn that reports usage, Useful? React with 👍 / 👎. |
||
| )} | ||
| {mode === "plan" && <span className="statusbar__plan">{t("status.plan")}</span>} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The controller still stores backend
retryingevents instate.retry, but these removed props were the only consumer of that state (confirmed with repo-widergforstate.retry/retry). During API retries the UI now only shows the generic spinner, so users lose the explicit retry attempt/max indication and cannot tell that the app is retrying rather than simply hung.Useful? React with 👍 / 👎.