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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ Thumbs.db
# Scratch / agent working dirs (never committed)
/tmp/
/.codex/
.alma-snapshots
10 changes: 2 additions & 8 deletions desktop/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@ export default function App() {
[activeTabId],
);
const topicbarEditing = Boolean(activeTab?.topicId && activeTab.topicId === renamingTopicId);
const topicbarProjectPrefix = activeTab ? tabWorkspaceTitle(activeTab) : "";
const visibleTabId = activeTabId;
const visibleTabs = useMemo(() => {
const byId = new Map(tabMetas.map((tab) => [tab.id, tab]));
Expand Down Expand Up @@ -1515,9 +1514,6 @@ export default function App() {
<div className="topicbar__title-row">
{topicbarEditing ? (
<div className="topicbar__title-edit">
{topicbarProjectPrefix && (
<span className="topicbar__title-prefix">{topicbarProjectPrefix} /</span>
)}
<input
autoFocus
className="topicbar__title-input"
Expand All @@ -1537,7 +1533,7 @@ export default function App() {
/>
</div>
) : (
<h1>{topicTitle(activeTab)}</h1>
<h1>{activeTab?.topicTitle || "Untitled"}</h1>
)}
<Tooltip label={t("topicBar.renameSession")}>
<button
Expand Down Expand Up @@ -1663,9 +1659,6 @@ export default function App() {
disabled={state.meta?.ready === false || state.messageAction != null || state.approval != null || state.ask != null}
decisionPending={state.messageAction != null || state.approval != null || state.ask != null}
ready={state.meta?.ready === true}
turnStartAt={state.turnStartAt}
turnTokens={state.turnTokens}
retry={state.retry}
workspaceRefreshSignal={projectRevision}
Comment on lines 1661 to 1662

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve retry attempt status

The controller still stores backend retrying events in state.retry, but these removed props were the only consumer of that state (confirmed with repo-wide rg for state.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 👍 / 👎.

/>
<StatusBar
Expand All @@ -1677,6 +1670,7 @@ export default function App() {
mode={mode}
cost={state.sessionCost}
currency={state.sessionCurrency}
turnTokens={state.turnTokens}
/>
</footer>
</>
Expand Down
123 changes: 40 additions & 83 deletions desktop/frontend/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -265,9 +243,6 @@ export function Composer({
disabled,
decisionPending = false,
ready,
turnStartAt,
turnTokens,
retry,
workspaceRefreshSignal,
}: {
running: boolean;
Expand All @@ -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[]>([]);
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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) => (
Expand Down Expand Up @@ -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"
Expand All @@ -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 && (
Expand All @@ -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} />
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore styling for the moved action button

The send/stop button was moved from .composer__btn--send to the new .composer-meta__action-btn* classes, but a repo-wide search shows no CSS rules for those classes. In the normal non-running composer this makes the primary send control fall back to native button styling/metrics instead of the fixed square primary action, which is a visible regression in the composer layout.

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>
Expand Down
1 change: 0 additions & 1 deletion desktop/frontend/src/components/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ export const AssistantMessage = memo(function AssistantMessage({
tone="violet"
icon={<ProcessBrainIcon size={12} />}
kind="reasoning"
name={t("msg.thinking")}
meta={
<>
<ProcessStatusIcon state={item.streaming ? "running" : "done"} label={item.streaming ? t("msg.thinkingRunning") : t("msg.thinkingDone")} />
Expand Down
12 changes: 4 additions & 8 deletions desktop/frontend/src/components/ProcessCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Children, type ReactNode, type SVGProps, useState } from "react";
import { BrainCircuit } from "lucide-react";

type IconProps = SVGProps<SVGSVGElement> & { size?: number };
export type ProcessTone = "default" | "success" | "warning" | "danger" | "accent" | "violet";
Expand Down Expand Up @@ -47,19 +48,14 @@ export function ProcessXIcon(props: IconProps) {
);
}

export function ProcessBrainIcon(props: IconProps) {
return (
<ProcessIcon {...props}>
<path d="M9 4a3 3 0 0 0-3 3v0a3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 3 3h0a3 3 0 0 0 3-3V4" />
<path d="M15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-3 3" />
</ProcessIcon>
);
export function ProcessBrainIcon({ size = 14, ...rest }: IconProps) {
return <BrainCircuit size={size} strokeWidth={1.8} {...rest} />;
}

export function ProcessToolIcon(props: IconProps) {
return (
<ProcessIcon {...props}>
<path d="M14 7a4 4 0 1 0 4 4l3 3-3 3-3-3a4 4 0 0 1-4-4l-3-3-3 3 3 3a4 4 0 0 0 6 0" />
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</ProcessIcon>
);
}
Expand Down
6 changes: 3 additions & 3 deletions desktop/frontend/src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { Check, ChevronDown } from "lucide-react";
import { Check, ChevronDown, X } from "lucide-react";
import { asArray } from "../lib/array";
import { app } from "../lib/bridge";
import { normalizeLangPref, useI18n, useT, type DictKey, type LangPref } from "../lib/i18n";
Expand Down Expand Up @@ -100,7 +100,7 @@ export function SettingsPanel({ onClose, onChanged, initialTab }: { onClose: ()
<header className="settings-modal__head">
<div className="settings-modal__title">{t("settings.title")}</div>
<Tooltip label={t("common.close")}>
<button className="chip" aria-label={t("common.close")} onClick={onClose}></button>
<button className="chip" aria-label={t("common.close")} onClick={onClose}><X size={14} /></button>
</Tooltip>
</header>

Expand Down Expand Up @@ -2154,7 +2154,7 @@ function RuleList({
{r}
<Tooltip label={t("common.delete")}>
<button className="set-rule__x" disabled={busy} onClick={() => void onRemove(r)}>
<X size={11} />
</button>
</Tooltip>
</span>
Expand Down
33 changes: 29 additions & 4 deletions desktop/frontend/src/components/StatusBar.tsx
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
Expand Down Expand Up @@ -89,6 +94,7 @@ export function StatusBar({
mode,
cost,
currency,
turnTokens,
}: {
context: ContextInfo;
usage?: WireUsage;
Expand All @@ -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);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide turn tokens after the turn completes

After any turn that reports usage, turnTokens remains at the last turn's value until the next turn starts, but this condition renders it whenever it is greater than zero. Previously the token count was part of the running-only activity text, so the status bar now shows a stale per-turn token total after completion, which is misleading while the app is idle.

Useful? React with 👍 / 👎.

)}
{mode === "plan" && <span className="statusbar__plan">{t("status.plan")}</span>}
</div>
</div>
);
}
Loading
Loading