Skip to content

Commit a2d50c3

Browse files
authored
Merge pull request #10 from mcode-webui/fix/webui-slash-compact-palette
feat(webui): pin and favourite are both drawn, and both stand without a hover
2 parents 17747ad + a6bfac6 commit a2d50c3

14 files changed

Lines changed: 1773 additions & 52 deletions

‎packages/webui/src/client/components/SessionRail.tsx‎

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import {
2929
WebuiIconFolder,
3030
WebuiIconMore,
3131
WebuiIconProjectAdd,
32-
WebuiIconSessionPin,
32+
WebuiIconSessionPinMark,
33+
WebuiIconSessionStar,
34+
WebuiIconSessionStarMark,
3335
WebuiIconSessionDisclosure,
3436
WebuiIconAgent,
3537
} from "../icons.js";
@@ -228,13 +230,15 @@ export function WebuiProjectList({
228230
onCreateTaskInProject,
229231
error,
230232
pinnedSessions,
233+
starredSessions,
231234
pinnedProjects,
232235
projectNames,
233236
onRenameProject,
234237
onToggleProjectPin,
235238
onArchiveProject,
236239
onRenameSession,
237240
onToggleSessionPin,
241+
onToggleSessionStar,
238242
onArchiveSession,
239243
onForkSession,
240244
onCopySession,
@@ -256,13 +260,16 @@ export function WebuiProjectList({
256260
readonly onCreateTaskInProject?: (project: WebuiProjectGroup) => void;
257261
readonly error?: string;
258262
readonly pinnedSessions?: Readonly<Record<string, boolean>>;
263+
/** Which rows the 收藏 view collects, and which carry the standing star. */
264+
readonly starredSessions?: Readonly<Record<string, boolean>>;
259265
readonly pinnedProjects?: Readonly<Record<string, boolean>>;
260266
readonly projectNames?: Readonly<Record<string, string>>;
261267
readonly onRenameProject?: (project: WebuiProjectGroup) => void;
262268
readonly onToggleProjectPin?: (project: WebuiProjectGroup) => void;
263269
readonly onArchiveProject?: (project: WebuiProjectGroup) => void;
264270
readonly onRenameSession?: (session: WebuiClientSession) => void;
265271
readonly onToggleSessionPin?: (session: WebuiClientSession) => void;
272+
readonly onToggleSessionStar?: (session: WebuiClientSession) => void;
266273
readonly onArchiveSession?: (session: WebuiClientSession) => void;
267274
readonly onForkSession?: (session: WebuiClientSession, createIsolatedWorktree: boolean) => void;
268275
readonly onCopySession?: (session: WebuiClientSession, value: "workspaceDir" | "sessionId") => void;
@@ -401,6 +408,14 @@ export function WebuiProjectList({
401408
disabled: !onToggleSessionPin,
402409
onSelect: () => onToggleSessionPin?.(session),
403410
},
411+
{
412+
kind: "item",
413+
key: "star",
414+
label: starredSessions?.[session.sessionId] ? "取消收藏" : "收藏",
415+
icon: <WebuiIconSessionStar starred={Boolean(starredSessions?.[session.sessionId])} />,
416+
disabled: !onToggleSessionStar,
417+
onSelect: () => onToggleSessionStar?.(session),
418+
},
404419
{
405420
kind: "item",
406421
key: "rename",
@@ -585,12 +600,12 @@ export function WebuiProjectList({
585600
const activeView = viewProp ?? internalView;
586601
const setActiveView = onViewChange ?? setInternalView;
587602
const railTabs = useMemo(
588-
() => selectWebuiRailViewTabs(page.sessions, activity),
589-
[activity, page.sessions],
603+
() => selectWebuiRailViewTabs(page.sessions, activity, starredSessions),
604+
[activity, page.sessions, starredSessions],
590605
);
591606
const viewSessions = useMemo(
592-
() => filterWebuiRailViewSessions(page.sessions, activity, activeView),
593-
[activeView, activity, page.sessions],
607+
() => filterWebuiRailViewSessions(page.sessions, activity, activeView, starredSessions),
608+
[activeView, activity, page.sessions, starredSessions],
594609
);
595610
const activeTab = railTabs.find((entry) => entry.view === activeView);
596611
const activeTabLabel = activeTab?.label ?? "项目";
@@ -636,6 +651,8 @@ export function WebuiProjectList({
636651
error={error}
637652
activity={activity}
638653
now={now}
654+
pinnedSessions={pinnedSessions}
655+
starredSessions={starredSessions}
639656
heading={activeTabLabel}
640657
emptyLabel={activeTab?.emptyLabel}
641658
preserveOrder
@@ -700,6 +717,20 @@ export function WebuiProjectList({
700717
className="webui-project-card text-left text-text_default_secondary"
701718
>
702719
<WebuiIconFolder className="flex-shrink-0" />
720+
{pinnedProjects?.[project.key] ?? project.pinned ? (
721+
/* Leading edge, not the trailing one: `.webui-project-row-actions`
722+
* is pinned to the row's right and only appears on hover, so a
723+
* mark on that side would be standing proof of nothing. */
724+
<span
725+
className="webui-project-pin-mark"
726+
data-webui-project-pin-mark="true"
727+
title="已置顶"
728+
aria-label="已置顶"
729+
role="img"
730+
>
731+
<WebuiIconSessionPinMark />
732+
</span>
733+
) : null}
703734
<span className="min-w-0 flex-1 truncate text-sm leading-5">
704735
{projectName}
705736
</span>
@@ -787,7 +818,13 @@ export function WebuiProjectList({
787818
<span className="min-w-0 flex-1 truncate">
788819
{sessionLabel(session)}
789820
</span>
790-
<SessionActivityMeta session={session} activity={activity} now={now} />
821+
<SessionActivityMeta
822+
session={session}
823+
activity={activity}
824+
now={now}
825+
pinned={Boolean(pinnedSessions?.[session.sessionId])}
826+
starred={Boolean(starredSessions?.[session.sessionId])}
827+
/>
791828
</a>
792829
<div className="webui-session-row-actions">
793830
{onToggleSessionPin ? (
@@ -802,7 +839,30 @@ export function WebuiProjectList({
802839
onToggleSessionPin(session);
803840
}}
804841
>
805-
<WebuiIconSessionPin />
842+
{/* The glyph draws the ACTION, not the state: already
843+
* pinned means the click unpins, so the button offers
844+
* the slashed pin. Same component the context menu
845+
* uses, so the two cannot drift apart. */}
846+
<WebuiIconContextPin pinned={Boolean(pinnedSessions?.[session.sessionId])} />
847+
</button>
848+
) : null}
849+
{onToggleSessionStar ? (
850+
<button
851+
type="button"
852+
aria-label={`${starredSessions?.[session.sessionId] ? "取消收藏" : "收藏"}:${sessionLabel(session)}`}
853+
title={starredSessions?.[session.sessionId] ? "取消收藏" : "收藏"}
854+
className="webui-rail-action"
855+
onClick={(event) => {
856+
event.preventDefault();
857+
event.stopPropagation();
858+
onToggleSessionStar(session);
859+
}}
860+
>
861+
{/* Outline until starred, yellow fill after. Unlike the
862+
* pin, this glyph shows the state rather than the
863+
* action: a filled star is not read as "remove"
864+
* anywhere in the world's icon vocabulary. */}
865+
<WebuiIconSessionStar starred={Boolean(starredSessions?.[session.sessionId])} />
806866
</button>
807867
) : null}
808868
{onArchiveSession ? (
@@ -855,7 +915,13 @@ export function WebuiProjectList({
855915
<span className="min-w-0 flex-1 truncate">
856916
{sessionLabel(child)}
857917
</span>
858-
<SessionActivityMeta session={child} activity={activity} now={now} />
918+
<SessionActivityMeta
919+
session={child}
920+
activity={activity}
921+
now={now}
922+
pinned={Boolean(pinnedSessions?.[child.sessionId])}
923+
starred={Boolean(starredSessions?.[child.sessionId])}
924+
/>
859925
</a>
860926
<div className="webui-session-row-actions">
861927
<button
@@ -940,17 +1006,50 @@ function SessionActivityMeta({
9401006
session,
9411007
activity,
9421008
now,
1009+
pinned = false,
1010+
starred = false,
9431011
}: {
9441012
readonly session: WebuiClientSession;
9451013
readonly activity?: WebuiSessionActivityMap;
9461014
readonly now?: number;
1015+
/** Draws the standing pin mark. Independent of the live state below it. */
1016+
readonly pinned?: boolean;
1017+
/** Draws the standing star mark. Also independent of the live state. */
1018+
readonly starred?: boolean;
9471019
}): ReactElement | null {
948-
if (!activity && now === undefined) return null;
1020+
// `pinned` and `starred` join the guard rather than riding on it. A host that
1021+
// passes neither an activity map nor a clock is the rail rendered outside the
1022+
// app (SSR fixtures, and any host that does not subscribe) -- exactly the case
1023+
// where a marked row has no running/unread signal to read its state against,
1024+
// so swallowing the mark there loses the only thing the row was saying.
1025+
if (!activity && now === undefined && !pinned && !starred) return null;
9491026
const entry = activity?.[session.sessionId];
9501027
const busy = entry?.busy;
9511028
const badge = formatWebuiUnreadBadge(entry?.unread);
9521029
return (
9531030
<span className="webui-rail-session-meta">
1031+
{pinned ? (
1032+
<span
1033+
className="webui-rail-pin-mark"
1034+
data-webui-pin-mark="true"
1035+
title="已置顶"
1036+
aria-label="已置顶"
1037+
role="img"
1038+
>
1039+
<WebuiIconSessionPinMark />
1040+
</span>
1041+
) : null}
1042+
{starred ? (
1043+
<span
1044+
className="webui-rail-star-mark"
1045+
data-webui-star-mark="true"
1046+
title="已收藏"
1047+
aria-label="已收藏"
1048+
role="img"
1049+
>
1050+
<WebuiIconSessionStarMark />
1051+
</span>
1052+
) : null}
9541053
{badge ? (
9551054
<span
9561055
className="webui-rail-unread-badge"
@@ -984,6 +1083,8 @@ export function WebuiSessionList({
9841083
teamModeChoices,
9851084
activity,
9861085
now,
1086+
pinnedSessions,
1087+
starredSessions,
9871088
heading,
9881089
emptyLabel,
9891090
preserveOrder,
@@ -992,11 +1093,24 @@ export function WebuiSessionList({
9921093
readonly loading: boolean;
9931094
readonly onLoadMore?: () => void;
9941095
readonly selectedSessionId?: string;
1096+
readonly onProjectSelect?: (workspaceDir?: string) => void;
9951097
readonly error?: string;
9961098
readonly teamModeChoices?: TeamModeSessionChoices;
9971099
readonly activity?: WebuiSessionActivityMap;
9981100
/** Injected so the age labels re-render on a tick instead of on every event. */
9991101
readonly now?: number;
1102+
/**
1103+
* Which rows carry the standing pin mark. The running and unread views are
1104+
* exactly where a user needs it -- those rows are ordered by activity, so
1105+
* sort position says nothing about which one the user chose to keep.
1106+
*/
1107+
readonly pinnedSessions?: Readonly<Record<string, boolean>>;
1108+
/**
1109+
* Which rows carry the standing star mark. 收藏 is a flat list, so a starred
1110+
* row has no project furniture to be recognised by and the mark is the only
1111+
* thing distinguishing it from an ordinary one.
1112+
*/
1113+
readonly starredSessions?: Readonly<Record<string, boolean>>;
10001114
/**
10011115
* Overrides the section label. The rail reuses this list for its running and
10021116
* unread views, where "recent tasks" would be a lie -- the list is filtered,
@@ -1080,7 +1194,13 @@ export function WebuiSessionList({
10801194
Agent Team
10811195
</span>
10821196
) : null}
1083-
<SessionActivityMeta session={session} activity={activity} now={now} />
1197+
<SessionActivityMeta
1198+
session={session}
1199+
activity={activity}
1200+
now={now}
1201+
pinned={Boolean(pinnedSessions?.[session.sessionId])}
1202+
starred={Boolean(starredSessions?.[session.sessionId])}
1203+
/>
10841204
</a>
10851205
{session.workspaceDir ? (
10861206
<div

‎packages/webui/src/client/components/WebuiClientFoundationApp.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ export function WebuiClientFoundationApp(
353353
const [pinnedSessions, setPinnedSessions] = useState<Record<string, boolean>>(
354354
readSessionOverlay("pins"),
355355
);
356+
const [starredSessions, setStarredSessions] = useState<Record<string, boolean>>(
357+
readSessionOverlay("stars"),
358+
);
356359
const [pinnedProjects, setPinnedProjects] = useState<Record<string, boolean>>(
357360
readProjectPins,
358361
);
@@ -560,6 +563,9 @@ export function WebuiClientFoundationApp(
560563
const handleToggleSessionPin = (session: WebuiClientSession) => {
561564
setPinnedSessions(toggleSessionOverlay("pins", session.sessionId));
562565
};
566+
const handleToggleSessionStar = (session: WebuiClientSession) => {
567+
setStarredSessions(toggleSessionOverlay("stars", session.sessionId));
568+
};
563569
const handleArchiveSession = (session: WebuiClientSession) => {
564570
if (!archiveSession) return;
565571
void archiveSession({ id: session.sessionId })
@@ -1185,13 +1191,15 @@ export function WebuiClientFoundationApp(
11851191
onCreateTaskInProject={(project) => startNewTask(project.workspaceDir)}
11861192
error={pageError}
11871193
pinnedSessions={pinnedSessions}
1194+
starredSessions={starredSessions}
11881195
pinnedProjects={pinnedProjects}
11891196
projectNames={projectNames}
11901197
onRenameProject={handleRenameProject}
11911198
onToggleProjectPin={handleToggleProjectPin}
11921199
onArchiveProject={handleArchiveProject}
11931200
onRenameSession={handleRenameSession}
11941201
onToggleSessionPin={handleToggleSessionPin}
1202+
onToggleSessionStar={handleToggleSessionStar}
11951203
onArchiveSession={handleArchiveSession}
11961204
onForkSession={handleForkSession}
11971205
onCopySession={handleCopySession}

0 commit comments

Comments
 (0)