chore: rebuild CentOS 7 release package

This commit is contained in:
shiyue
2026-08-16 22:20:00 +08:00
parent 93641df1ad
commit 6ed8c7d30b
21 changed files with 2137 additions and 45 deletions

View File

@@ -13,7 +13,8 @@
const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects';
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'cc-web-sidebar-collapsed';
const SIDEBAR_DRAWER_MEDIA_QUERY = '(max-width: 768px)';
const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)';
const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px)';
const SIDEBAR_HOVER_PREVIEW_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)';
const CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-cross-replies';
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
const ASSISTANT_LAST_SECTION_BUTTON_CLASS = 'msg-last-section-btn';
@@ -65,6 +66,13 @@
return SLASH_COMMANDS.some((item) => item.cmd.toLowerCase() === normalized);
}
function isGoalSetCommandText(text) {
const match = /^\s*\/goal(?:\s+([\s\S]*))?$/i.exec(String(text || ''));
if (!match) return false;
const rest = String(match[1] || '').trim().toLowerCase();
return !!rest && !['clear', 'pause', 'resume'].includes(rest);
}
const MODE_LABELS = {
default: '默认',
plan: 'Plan',
@@ -345,6 +353,7 @@
const mobileInputMedia = window.matchMedia('(max-width: 768px), (pointer: coarse)');
const sidebarDrawerMedia = window.matchMedia(SIDEBAR_DRAWER_MEDIA_QUERY);
const sidebarDesktopCollapseMedia = window.matchMedia(SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY);
const sidebarHoverPreviewMedia = window.matchMedia(SIDEBAR_HOVER_PREVIEW_MEDIA_QUERY);
const chatMain = document.querySelector('.chat-main');
const newChatSplit = sidebar.querySelector('.new-chat-split');
const newChatBtn = $('#new-chat-btn');
@@ -11247,8 +11256,17 @@
return;
}
const requestId = createLocalId('slash');
const clientMessageId = isGoalSetCommandText(text) ? createLocalId('user') : '';
rememberPendingSlashDraft(requestId, text, currentSessionId, currentAgent);
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, requestId });
send({
type: 'message',
text,
sessionId: currentSessionId,
mode: currentMode,
agent: currentAgent,
requestId,
...(clientMessageId ? { clientMessageId } : {}),
});
msgInput.value = '';
autoResize();
return;
@@ -11310,7 +11328,7 @@
});
sidebarOverlay.addEventListener('click', closeSidebar);
[sidebarDrawerMedia, sidebarDesktopCollapseMedia].forEach((media) => {
[sidebarDrawerMedia, sidebarDesktopCollapseMedia, sidebarHoverPreviewMedia].forEach((media) => {
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', handleSidebarInputModeChange);
} else if (typeof media.addListener === 'function') {

View File

@@ -9925,7 +9925,7 @@ html[data-theme='gilded'] {
outline-offset: 2px;
}
@media (width > 768px) and (hover: hover) and (pointer: fine) {
@media (width > 768px) {
.menu-btn {
display: inline-flex;
align-items: center;
@@ -9951,11 +9951,6 @@ html[data-theme='gilded'] {
box-shadow 180ms ease;
}
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within) {
transform: translateX(0);
box-shadow: var(--sidebar-hover-shadow);
}
/* 收起后控制条会贴近左侧轨道,定位浮层需朝聊天画布展开,避免越过应用边界被裁切。 */
.app.sidebar-collapsed .user-outline-panel {
left: 0;
@@ -9981,10 +9976,6 @@ html[data-theme='gilded'] {
transition: opacity 120ms ease;
}
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within)::after {
opacity: 0;
}
html[data-theme='gilded'] .app.sidebar-collapsed .sidebar::after {
border-left-color: rgba(138, 97, 37, 0.34);
background:
@@ -10002,6 +9993,17 @@ html[data-theme='gilded'] {
}
}
@media (width > 768px) and (hover: hover) and (pointer: fine) {
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within) {
transform: translateX(0);
box-shadow: var(--sidebar-hover-shadow);
}
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within)::after {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.app.sidebar-collapsed .sidebar,
.app.sidebar-collapsed .sidebar::after {

View File

@@ -222,6 +222,14 @@
overflow: hidden;
}
.ccweb-task-board__clock {
display: none;
}
.ccweb-task-board__wall {
display: none;
}
.ccweb-task-board__lanes {
display: grid;
grid-auto-flow: column;
@@ -1171,6 +1179,646 @@ html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running
}
}
/* 横屏常亮模式:看板覆盖完整网页视口,宿主聊天界面不再参与排版。 */
@media (orientation: landscape) and (max-height: 768px) {
body:has(.task-board-panel:not([hidden])) .menu-btn {
display: none !important;
}
.task-board-panel {
position: fixed;
inset: 0;
z-index: 400;
display: block;
width: 100vw;
height: 100vh;
height: 100dvh;
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--bg-primary);
}
.task-board-panel__root {
position: absolute;
inset: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.task-board-panel__header {
position: absolute;
top: calc(env(safe-area-inset-top, 0px) + 0.3rem);
right: calc(env(safe-area-inset-right, 0px) + 0.35rem);
z-index: 50;
display: block;
width: 1.9rem;
min-height: 0;
padding: 0;
border: 0;
background: transparent;
pointer-events: none;
}
.task-board-panel__header .usage-dashboard__icon-button {
width: 1.9rem;
min-width: 1.9rem;
height: 1.9rem;
padding: 0;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
pointer-events: auto;
}
.ccweb-task-board {
position: absolute;
inset: 0;
grid-template-rows: minmax(0, 1fr);
width: auto;
height: auto;
min-height: 0;
animation: none;
}
.ccweb-task-board__toolbar {
display: none;
grid-template-columns: minmax(0, auto) minmax(5.2rem, 1fr) auto;
gap: 0.45rem;
min-height: 2.3rem;
padding:
calc(env(safe-area-inset-top, 0px) + 0.22rem)
calc(env(safe-area-inset-right, 0px) + 2.4rem)
0.22rem
calc(env(safe-area-inset-left, 0px) + 0.42rem);
border-bottom: 1px solid var(--border-color);
background: var(--bg-primary);
}
.ccweb-task-board__heading {
display: flex;
align-items: baseline;
gap: 0.38rem;
min-width: 0;
}
.ccweb-task-board__title {
flex: 0 0 auto;
font-size: 0.78rem;
letter-spacing: -0.015em;
white-space: nowrap;
}
.ccweb-task-board__summary {
display: none;
}
.ccweb-task-board__clock {
display: flex;
min-width: 0;
min-height: 0;
padding: clamp(0.65rem, 2.2vw, 1.2rem);
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: clamp(0.35rem, 1.4vh, 0.65rem);
color: var(--text-primary);
white-space: nowrap;
background: var(--bg-secondary);
}
.ccweb-task-board__clock-time {
color: var(--text-primary);
font-size: clamp(2.25rem, 6.4vw, 4.6rem);
font-weight: 680;
font-variant-numeric: tabular-nums;
letter-spacing: -0.07em;
line-height: 0.9;
}
.ccweb-task-board__clock-date {
color: var(--text-secondary);
font-size: clamp(0.6rem, 1.2vw, 0.78rem);
font-weight: 560;
letter-spacing: 0.025em;
}
.ccweb-task-board__controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.28rem;
}
.ccweb-task-board__search {
flex: 0 1 min(9rem, 30vw);
width: min(9rem, 30vw);
transition: flex-basis 160ms ease, width 160ms ease;
}
.ccweb-task-board__search:not(:focus-within) {
flex-basis: 1.9rem;
width: 1.9rem;
}
.ccweb-task-board[data-search-active='true'] .ccweb-task-board__search,
.ccweb-task-board__search:focus-within {
flex-basis: min(9rem, 30vw);
width: min(9rem, 30vw);
}
.ccweb-task-board__search-glyph {
left: 0.55rem;
font-size: 0.9rem;
}
.ccweb-task-board__input {
height: 1.75rem;
padding: 0 0.5rem 0 1.65rem;
border-radius: 0.5rem;
font-size: 0.65rem;
}
.ccweb-task-board[data-search-active='false'] .ccweb-task-board__search:not(:focus-within) .ccweb-task-board__input {
padding-right: 0;
color: transparent;
cursor: pointer;
}
.ccweb-task-board[data-search-active='false'] .ccweb-task-board__search:not(:focus-within) .ccweb-task-board__input::placeholder {
opacity: 0;
}
.ccweb-task-board__button {
min-height: 1.75rem;
padding: 0.2rem 0.4rem;
border-radius: 0.42rem;
font-size: 0.6rem;
}
.ccweb-task-board__workspace {
display: block;
height: 100%;
min-height: 0;
padding: 0;
}
.ccweb-task-board__wall {
display: block;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
padding:
env(safe-area-inset-top, 0px)
env(safe-area-inset-right, 0px)
env(safe-area-inset-bottom, 0px)
env(safe-area-inset-left, 0px);
overflow: hidden;
background: var(--border-color);
}
.ccweb-task-board__wall-grid {
display: grid;
grid-template-columns: repeat(var(--task-board-wall-columns), minmax(0, 1fr));
grid-template-rows: repeat(var(--task-board-wall-rows), minmax(0, 1fr));
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
gap: 1px;
background: var(--border-color);
}
.ccweb-task-board__wall-column {
--task-board-status-color: var(--text-muted);
display: grid;
grid-template-rows: auto minmax(0, 1fr);
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--bg-primary);
}
.ccweb-task-board__wall-column-header,
.ccweb-task-board__wall-column-identity,
.ccweb-task-board__wall-card-meta,
.ccweb-task-board__wall-card-runtime {
display: flex;
align-items: center;
}
.ccweb-task-board__wall-column-header {
min-width: 0;
min-height: clamp(2rem, 6.8vh, 2.55rem);
padding: clamp(0.35rem, 1.2vh, 0.58rem) clamp(0.5rem, 1.4vw, 0.82rem);
justify-content: space-between;
gap: 0.5rem;
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.ccweb-task-board__wall-column-identity {
min-width: 0;
gap: clamp(0.34rem, 0.8vw, 0.52rem);
}
.ccweb-task-board__wall-column-dot {
display: block;
flex: 0 0 auto;
width: clamp(0.38rem, 0.8vw, 0.5rem);
height: clamp(0.38rem, 0.8vw, 0.5rem);
border-radius: 50%;
background: var(--task-board-status-color);
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--task-board-status-color) 18%, transparent);
}
.ccweb-task-board__wall-column-title {
margin: 0;
overflow: hidden;
color: var(--text-primary);
font-size: clamp(0.7rem, 1.5vw, 0.98rem);
font-weight: 680;
letter-spacing: -0.015em;
text-overflow: ellipsis;
white-space: nowrap;
}
.ccweb-task-board__wall-column-count {
flex: 0 0 auto;
color: var(--text-secondary);
font-size: clamp(0.6rem, 1.15vw, 0.76rem);
font-variant-numeric: tabular-nums;
}
.ccweb-task-board__wall-column-page {
flex: 0 0 auto;
color: var(--text-muted);
font-size: clamp(0.55rem, 1vw, 0.68rem);
font-variant-numeric: tabular-nums;
}
.ccweb-task-board__wall-column-body {
display: grid;
grid-auto-rows: minmax(0, 1fr);
min-width: 0;
min-height: 0;
padding: clamp(0.35rem, 1.1vw, 0.65rem);
overflow: hidden;
gap: clamp(0.28rem, 0.8vw, 0.5rem);
}
.ccweb-task-board__wall-card {
--task-board-status-color: var(--text-muted);
position: relative;
z-index: 1;
display: flex;
min-width: 0;
min-height: 0;
padding: clamp(0.4rem, 1.15vw, 0.72rem) clamp(0.45rem, 1.25vw, 0.78rem);
overflow: hidden;
flex-direction: column;
gap: clamp(0.22rem, 0.8vh, 0.42rem);
border: 1px solid var(--border-color);
border-left: 0.16rem solid var(--task-board-status-color);
border-radius: clamp(0.35rem, 0.8vw, 0.55rem);
color: var(--text-primary);
background: var(--bg-secondary);
transform-origin: 0 50%;
will-change: opacity, transform, clip-path;
}
.ccweb-task-board__wall-card-title {
margin: 0;
color: var(--text-primary);
font-size: clamp(0.68rem, 1.45vw, 1rem);
font-weight: 690;
letter-spacing: -0.018em;
line-height: 1.28;
overflow-wrap: anywhere;
}
.ccweb-task-board__wall-card-summary {
display: block;
margin: 0;
color: var(--text-secondary);
font-size: clamp(0.56rem, 1.12vw, 0.76rem);
line-height: 1.38;
overflow-wrap: anywhere;
white-space: normal;
}
.ccweb-task-board__wall-card-meta {
margin-top: auto;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.3rem 0.6rem;
color: var(--text-muted);
font-size: clamp(0.52rem, 1vw, 0.68rem);
}
.ccweb-task-board__wall-card-runtime {
gap: 0.3rem;
}
.ccweb-task-board__wall-card-runtime-dot {
width: 0.36rem;
height: 0.36rem;
border-radius: 50%;
background: var(--text-muted);
}
.ccweb-task-board__wall-card-runtime[data-running='true'] {
color: var(--success);
}
.ccweb-task-board__wall-card-runtime[data-running='true'] .ccweb-task-board__wall-card-runtime-dot {
background: currentColor;
animation: ccweb-task-board-pulse 850ms ease-in-out infinite alternate;
}
.ccweb-task-board__wall-card-updated {
color: var(--text-muted);
}
.ccweb-task-board__wall-column-empty,
.ccweb-task-board__wall-empty {
display: flex;
min-width: 0;
min-height: 0;
align-items: center;
justify-content: center;
gap: 0.38rem;
color: var(--text-muted);
font-size: clamp(0.58rem, 1.15vw, 0.76rem);
}
.ccweb-task-board__wall-empty {
flex-direction: column;
background: var(--bg-primary);
}
.ccweb-task-board__wall-column-empty-mark,
.ccweb-task-board__wall-empty-mark {
color: var(--text-muted);
font-size: 1.1rem;
}
.ccweb-task-board__lanes {
display: none;
grid-auto-flow: row;
grid-auto-columns: auto;
grid-auto-rows: auto;
align-items: stretch;
gap: 0;
height: 100%;
padding: 0;
overflow-y: hidden;
scroll-snap-type: none;
}
.ccweb-task-board__lanes[data-column-layout='fit'] {
grid-template-columns: repeat(var(--task-board-column-count), minmax(0, 1fr));
grid-template-rows: minmax(0, 1fr);
overflow-x: hidden;
}
.ccweb-task-board__lanes[data-column-layout='scroll'] {
grid-template-columns: repeat(var(--task-board-column-count), minmax(10rem, 1fr));
grid-template-rows: minmax(0, 1fr);
overflow-x: auto;
}
.ccweb-task-board__column {
height: 100%;
min-height: 0;
overflow: hidden;
border: 0;
border-left: 1px solid var(--border-color);
border-radius: 0;
scroll-snap-align: none;
}
.ccweb-task-board__column:last-child {
border-right: 1px solid var(--border-color);
}
.ccweb-task-board__column-header {
position: static;
flex: 0 0 auto;
min-height: 1.72rem;
gap: 0.3rem;
padding: 0.27rem 0.36rem;
border-radius: 0;
}
.ccweb-task-board__column-title-wrap {
gap: 0.32rem;
}
.ccweb-task-board__column-dot {
width: 0.4rem;
height: 0.4rem;
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--task-board-status-color) 18%, transparent);
}
.ccweb-task-board__column-title {
font-size: clamp(0.6rem, 1.08vw, 0.72rem);
}
.ccweb-task-board__column-count {
min-width: 1.15rem;
height: 1.08rem;
padding: 0 0.25rem;
font-size: 0.56rem;
}
.ccweb-task-board__column-disabled,
.ccweb-task-board__column-empty {
display: none;
}
.ccweb-task-board__column-body {
flex: 1 1 auto;
gap: 0.28rem;
min-height: 0;
padding: 0.3rem;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.ccweb-task-board__card {
flex: 0 0 auto;
gap: 0.24rem;
padding: 0.34rem;
border-radius: 0.38rem;
}
.ccweb-task-board__card:hover,
.ccweb-task-board__card:focus-within {
transform: none;
}
.ccweb-task-board__card-top,
.ccweb-task-board__card-summary,
.ccweb-task-board__metadata {
display: none;
}
.ccweb-task-board__card-title {
font-size: clamp(0.61rem, 1.05vw, 0.72rem);
line-height: 1.26;
-webkit-line-clamp: 2;
}
.ccweb-task-board__card[data-runtime-state='idle'] .ccweb-task-board__state-row {
display: none;
}
.ccweb-task-board__state-row {
gap: 0.2rem;
}
.ccweb-task-board__runtime-state {
min-height: 1.05rem;
padding: 0.08rem 0.23rem;
border-radius: 0.3rem;
font-size: 0.52rem;
}
.ccweb-task-board__card-footer {
align-items: center;
flex-direction: row;
padding-top: 0.22rem;
}
.ccweb-task-board__card-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
width: 100%;
gap: 0.22rem;
}
.ccweb-task-board__status-select-wrap {
width: auto;
min-width: 0;
}
.ccweb-task-board__card-select {
height: 1.36rem;
padding: 0 1rem 0 0.32rem;
border-radius: 0.35rem;
font-size: 0.52rem;
}
.ccweb-task-board__card-action {
min-height: 1.36rem;
padding: 0.15rem 0.34rem;
border-radius: 0.35rem;
font-size: 0.52rem;
}
.ccweb-task-board__empty {
width: 100%;
height: 100%;
min-height: 0;
padding: 1rem;
}
.ccweb-task-board__status-layer {
display: none !important;
grid-template-columns: minmax(0, 1fr) minmax(19rem, 70vw);
}
.ccweb-task-board__status-header {
padding: 0.55rem 0.7rem;
}
.ccweb-task-board__status-body {
padding: 0.55rem;
}
}
@media (orientation: landscape) and (max-height: 430px) {
.ccweb-task-board__toolbar {
min-height: 2.15rem;
padding-top: calc(env(safe-area-inset-top, 0px) + 0.2rem);
padding-bottom: 0.2rem;
}
.ccweb-task-board__title {
font-size: 0.78rem;
}
.ccweb-task-board__summary {
display: none;
}
.ccweb-task-board__clock-time {
font-size: clamp(2.25rem, 5.5vw, 3.25rem);
}
.ccweb-task-board__clock-date {
display: block;
}
.ccweb-task-board__clock {
padding: 0.55rem;
}
.ccweb-task-board__wall-column-header {
min-height: 1.8rem;
padding: 0.28rem 0.48rem;
}
.ccweb-task-board__wall-column-body {
padding: 0.28rem;
gap: 0.24rem;
}
.ccweb-task-board__wall-card {
padding: 0.32rem 0.42rem;
gap: 0.18rem;
}
.ccweb-task-board__wall-card-title {
font-size: clamp(0.64rem, 1.4vw, 0.82rem);
}
.ccweb-task-board__wall-card-summary {
font-size: clamp(0.52rem, 1.05vw, 0.66rem);
line-height: 1.3;
}
.ccweb-task-board__column-header {
min-height: 1.72rem;
padding-block: 0.24rem;
}
.ccweb-task-board__column-body {
gap: 0.28rem;
padding: 0.28rem;
}
.ccweb-task-board__card {
gap: 0.24rem;
padding: 0.34rem;
}
.ccweb-task-board__card-title {
-webkit-line-clamp: 2;
}
}
@media (prefers-reduced-motion: reduce) {
html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running']::after {
top: 50%;
@@ -1197,11 +1845,17 @@ html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running
.ccweb-task-board__card:focus-within {
transform: none;
}
.ccweb-task-board__wall-card {
will-change: auto;
}
}
@media (forced-colors: active) {
.ccweb-task-board__column-dot,
.ccweb-task-board__definition-dot {
.ccweb-task-board__definition-dot,
.ccweb-task-board__wall-column-dot,
.ccweb-task-board__wall-card-runtime-dot {
border: 1px solid CanvasText;
}

View File

@@ -265,13 +265,24 @@
error: null,
destroyed: false,
};
const fitColumnLimit = Math.min(6, Math.max(1, Math.floor(finiteNumber(options.fitColumnLimit, 6))));
const wallRotationMs = Math.max(20, finiteNumber(options.wallRotationMs, 12000));
const wallUpdateHoldMs = Math.max(wallRotationMs, finiteNumber(options.wallUpdateHoldMs, 20000));
const wallCardsPerPage = Math.min(4, Math.max(1, Math.floor(finiteNumber(options.wallCardsPerPage, 2))));
let searchTimer = null;
let clockTimer = null;
let wallRotationTimer = null;
let wallHasRendered = false;
let wallRotationCursor = 0;
let previousFocus = null;
let latestQueryRequestId = '';
let hasRenderedBoard = false;
const columnRecords = new Map();
const cardRecords = new Map();
const wallColumnRecords = new Map();
const wallCardRecords = new Map();
let wallVisibleSessionIds = new Set();
let boardEmpty = null;
const shell = createNode(doc, 'section', 'ccweb-task-board');
@@ -285,6 +296,12 @@
summary.setAttribute('aria-live', 'polite');
headingGroup.append(title, summary);
const clock = createNode(doc, 'aside', 'ccweb-task-board__clock');
clock.setAttribute('aria-label', '当前时间');
const clockTime = createNode(doc, 'time', 'ccweb-task-board__clock-time');
const clockDate = createNode(doc, 'span', 'ccweb-task-board__clock-date');
clock.append(clockTime, clockDate);
const controls = createNode(doc, 'div', 'ccweb-task-board__controls');
controls.setAttribute('role', 'group');
controls.setAttribute('aria-label', '任务看板搜索与操作');
@@ -321,7 +338,21 @@
const board = createNode(doc, 'div', 'ccweb-task-board__lanes');
board.setAttribute('aria-label', '任务状态列');
board.tabIndex = 0;
workspace.appendChild(board);
const wall = createNode(doc, 'section', 'ccweb-task-board__wall');
wall.setAttribute('aria-label', '横屏任务看板');
const wallGrid = createNode(doc, 'div', 'ccweb-task-board__wall-grid');
wallGrid.setAttribute('role', 'list');
const wallEmpty = createNode(doc, 'section', 'ccweb-task-board__wall-empty');
wallEmpty.setAttribute('role', 'status');
wallEmpty.append(
createNode(doc, 'span', 'ccweb-task-board__wall-empty-mark', '◇'),
createNode(doc, 'span', '', '等待状态定义'),
);
wallGrid.append(clock, wallEmpty);
wall.appendChild(wallGrid);
workspace.append(board, wall);
const statusLayer = createNode(doc, 'div', 'ccweb-task-board__status-layer');
statusLayer.hidden = true;
@@ -361,7 +392,13 @@
searchInput,
archiveButton,
statusButton,
clock,
clockTime,
clockDate,
board,
wall,
wallGrid,
wallEmpty,
statusLayer,
statusPanel,
statusClose,
@@ -374,6 +411,463 @@
refs.liveRegion.textContent = text(message);
}
function renderClock() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
refs.clockTime.textContent = `${hours}:${minutes}`;
refs.clockTime.setAttribute('datetime', now.toISOString());
refs.clockDate.textContent = `${now.getMonth() + 1}${now.getDate()}日 · ${weekdays[now.getDay()]}`;
}
function scheduleClock() {
renderClock();
if (typeof global.setTimeout !== 'function') return;
const now = new Date();
const delay = Math.max(1000, (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 20);
clockTimer = global.setTimeout(scheduleClock, delay);
if (clockTimer && typeof clockTimer.unref === 'function') clockTimer.unref();
}
function wallTaskTimestamp(task) {
return Date.parse(task?.updatedAt || '') || 0;
}
function compareWallTasks(left, right) {
if (left.isRunning !== right.isRunning) return left.isRunning ? -1 : 1;
const timeDifference = wallTaskTimestamp(right) - wallTaskTimestamp(left);
if (timeDifference) return timeDifference;
if (left.order !== right.order) return left.order - right.order;
return left.sessionId.localeCompare(right.sessionId);
}
function getWallTasks() {
return state.tasks
.filter((task) => task.trackingEnabled && !task.archivedAt)
.slice()
.sort(compareWallTasks);
}
function wallTaskSignature(task) {
if (!task) return '';
return JSON.stringify([
task.sessionId,
task.title,
task.statusId,
task.summary,
task.reason,
task.isRunning,
task.runtimeState,
task.runtimeDetail,
task.updatedAt,
task.version,
]);
}
function clearWallRotation() {
if (wallRotationTimer === null) return;
global.clearTimeout(wallRotationTimer);
wallRotationTimer = null;
}
function wallLayoutSize(statusCount) {
const cellCount = Math.max(1, statusCount + 1);
const preferredColumns = cellCount <= 4
? Math.min(cellCount, 2)
: Math.round(Math.sqrt(cellCount * (16 / 9)));
const columns = Math.min(cellCount, Math.max(1, preferredColumns));
return { cellCount, columns, rows: Math.max(1, Math.ceil(cellCount / columns)) };
}
function wallTasksByStatus(definitions, tasks) {
const grouped = new Map(definitions.map((definition) => [definition.id, []]));
for (const task of tasks) grouped.get(task.statusId)?.push(task);
return grouped;
}
function overflowingWallStatusIds() {
const definitions = getBoardDefinitions();
const grouped = wallTasksByStatus(definitions, getWallTasks());
const pageSize = wallLayoutSize(definitions.length || 1).rows >= 3 ? 1 : wallCardsPerPage;
return definitions
.filter((definition) => (grouped.get(definition.id)?.length || 0) > pageSize)
.map((definition) => definition.id);
}
function scheduleWallRotation(delay = wallRotationMs) {
clearWallRotation();
if (state.destroyed || prefersReducedBoardMotion() || !overflowingWallStatusIds().length) return;
wallRotationTimer = global.setTimeout(() => {
wallRotationTimer = null;
advanceWallPage();
}, delay);
if (wallRotationTimer && typeof wallRotationTimer.unref === 'function') wallRotationTimer.unref();
}
function readWallCardRect(card) {
if (!card || typeof card.getBoundingClientRect !== 'function' || !refs.wall.contains(card)) return null;
const rect = card.getBoundingClientRect();
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return null;
return { left: rect.left, top: rect.top };
}
function cancelWallCardAnimation(record, kind = '') {
if (!record?.animations) return;
const entries = kind
? [[kind, record.animations.get(kind)]]
: Array.from(record.animations.entries());
for (const [animationKind, animation] of entries) {
if (!animation) continue;
animation.onfinish = null;
animation.oncancel = null;
record.animations.delete(animationKind);
if (typeof animation.cancel !== 'function') continue;
try {
animation.cancel();
} catch (_) {
// 动画对象可能已由浏览器回收;卡片节点仍继续复用。
}
}
if (!record.animations.size) delete record.element.dataset.motion;
}
function runWallCardAnimation(record, kind, keyframes, options, onFinish) {
cancelWallCardAnimation(record, kind);
const card = record.element;
card.dataset.motion = kind;
const complete = () => {
record.animations.delete(kind);
if (!record.animations.size) delete card.dataset.motion;
if (typeof onFinish === 'function') onFinish();
};
if (prefersReducedBoardMotion() || typeof card.animate !== 'function') {
complete();
return;
}
let animation;
try {
animation = card.animate(keyframes, { fill: 'both', ...options });
} catch (_) {
complete();
return;
}
record.animations.set(kind, animation);
animation.onfinish = () => {
if (record.animations.get(kind) !== animation) return;
animation.onfinish = null;
animation.oncancel = null;
try {
animation.cancel();
} catch (_) {
// 基础样式接管完成帧;旧浏览器回收动画时无需阻断渲染。
}
complete();
};
animation.oncancel = () => {
if (record.animations.get(kind) !== animation) return;
animation.onfinish = null;
animation.oncancel = null;
record.animations.delete(kind);
if (!record.animations.size) delete card.dataset.motion;
};
}
function animateWallCardPageLeave(sessionId, record) {
runWallCardAnimation(record, 'page-leaving', [
{ opacity: 1, transform: 'translate3d(0, 0, 0) scaleY(1)', clipPath: 'inset(0 0 0 0)' },
{ opacity: 0, transform: 'translate3d(-0.7rem, 0, 0) scaleY(0.72)', clipPath: 'inset(0 88% 0 0)' },
], {
id: 'ccweb-task-board-wall-card-page-leave',
duration: 300,
easing: 'cubic-bezier(0.55, 0.05, 0.72, 0.35)',
}, () => {
if (!wallVisibleSessionIds.has(sessionId)) record.element.remove();
});
}
function animateWallCardPageEnter(record, index) {
runWallCardAnimation(record, 'page-entering', [
{ opacity: 0, transform: 'translate3d(0.7rem, 0, 0) scaleY(0.76)', clipPath: 'inset(0 0 100% 0)' },
{ opacity: 1, transform: 'translate3d(0, 0, 0) scaleY(1)', clipPath: 'inset(0 0 0 0)' },
], {
id: 'ccweb-task-board-wall-card-page-enter',
duration: 430,
delay: Math.min(index, 3) * 55,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
});
}
function animateWallCardMove(record, before, after) {
if (!before || !after) return;
const deltaX = before.left - after.left;
const deltaY = before.top - after.top;
if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) return;
runWallCardAnimation(record, 'moving', [
{ transform: `translate3d(${deltaX}px, ${deltaY}px, 0)`, zIndex: 4 },
{ transform: 'translate3d(0, 0, 0)', zIndex: 1 },
], {
id: 'ccweb-task-board-wall-card-move',
duration: 520,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
});
}
function animateWallCardUpdate(record) {
runWallCardAnimation(record, 'updating', [
{ boxShadow: 'inset 0.16rem 0 0 var(--task-board-status-color)', backgroundColor: 'var(--bg-secondary)' },
{ boxShadow: 'inset 0.3rem 0 0 var(--task-board-status-color), 0 0 0 0.16rem color-mix(in srgb, var(--task-board-status-color) 28%, transparent)', backgroundColor: 'color-mix(in srgb, var(--task-board-status-color) 10%, var(--bg-secondary))', offset: 0.36 },
{ boxShadow: 'inset 0.16rem 0 0 var(--task-board-status-color)', backgroundColor: 'var(--bg-secondary)' },
], {
id: 'ccweb-task-board-wall-card-update',
duration: 1050,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
});
}
function createWallColumnRecord(definition, definitionIndex) {
const element = createNode(doc, 'section', 'ccweb-task-board__wall-column');
element.setAttribute('role', 'listitem');
const header = createNode(doc, 'header', 'ccweb-task-board__wall-column-header');
const identity = createNode(doc, 'div', 'ccweb-task-board__wall-column-identity');
const dot = createNode(doc, 'span', 'ccweb-task-board__wall-column-dot');
dot.setAttribute('aria-hidden', 'true');
const title = createNode(doc, 'h2', 'ccweb-task-board__wall-column-title');
const count = createNode(doc, 'span', 'ccweb-task-board__wall-column-count');
identity.append(dot, title, count);
const page = createNode(doc, 'span', 'ccweb-task-board__wall-column-page');
header.append(identity, page);
const body = createNode(doc, 'div', 'ccweb-task-board__wall-column-body');
body.setAttribute('role', 'list');
element.append(header, body);
return {
element,
header,
title,
count,
page,
body,
empty: null,
pageIndex: 0,
totalPages: 1,
definitionIndex,
};
}
function updateWallColumnRecord(record, definition, definitionIndex, taskCount, pageSize) {
const titleId = `task-board-wall-column-${domToken(definition.id, definitionIndex)}`;
record.element.dataset.statusId = definition.id;
record.element.setAttribute('aria-labelledby', titleId);
setStatusColor(record.element, definition.color);
record.title.id = titleId;
record.title.textContent = definition.label;
record.count.textContent = String(taskCount);
record.count.setAttribute('aria-label', `${taskCount} 个任务`);
record.header.title = definition.prompt || '';
record.body.setAttribute('aria-label', `${definition.label}任务`);
record.totalPages = Math.max(1, Math.ceil(taskCount / pageSize));
record.pageIndex = ((record.pageIndex % record.totalPages) + record.totalPages) % record.totalPages;
record.page.textContent = record.totalPages > 1 ? `${record.pageIndex + 1} / ${record.totalPages}` : '';
record.page.hidden = record.totalPages <= 1;
record.definitionIndex = definitionIndex;
}
function wallCardSignature(task, definition) {
return JSON.stringify([wallTaskSignature(task), definition.label, definition.color]);
}
function renderWallCard(task, definition, existingCard = null) {
const card = existingCard || createNode(doc, 'article', 'ccweb-task-board__wall-card');
card.replaceChildren();
card.dataset.sessionId = task.sessionId;
card.dataset.statusId = task.statusId;
card.dataset.runtimeState = task.isRunning ? 'running' : 'idle';
card.setAttribute('role', 'listitem');
setStatusColor(card, definition.color);
card.appendChild(createNode(doc, 'h3', 'ccweb-task-board__wall-card-title', task.title));
const supportingText = task.summary || task.reason;
if (supportingText) card.appendChild(createNode(doc, 'p', 'ccweb-task-board__wall-card-summary', supportingText));
const meta = createNode(doc, 'div', 'ccweb-task-board__wall-card-meta');
const runtimeLabel = task.runtimeState || (task.isRunning ? '运行中' : '');
if (runtimeLabel) {
const runtime = createNode(doc, 'span', 'ccweb-task-board__wall-card-runtime', runtimeLabel);
runtime.dataset.running = task.isRunning ? 'true' : 'false';
runtime.prepend(createNode(doc, 'span', 'ccweb-task-board__wall-card-runtime-dot'));
meta.appendChild(runtime);
}
const updatedLabel = formatTime(task.updatedAt);
if (updatedLabel) {
const updated = createNode(doc, 'time', 'ccweb-task-board__wall-card-updated', updatedLabel);
updated.setAttribute('datetime', String(task.updatedAt));
meta.appendChild(updated);
}
if (meta.children.length) card.appendChild(meta);
return card;
}
function ensureWallColumnEmpty(record) {
if (!record.empty) {
record.empty = createNode(doc, 'div', 'ccweb-task-board__wall-column-empty');
record.empty.append(
createNode(doc, 'span', 'ccweb-task-board__wall-column-empty-mark', '·'),
createNode(doc, 'span', '', '暂无任务'),
);
}
if (record.empty.parentNode !== record.body) record.body.appendChild(record.empty);
}
function renderWall(options = {}) {
if (state.destroyed) return;
const definitions = getBoardDefinitions();
const tasks = getWallTasks();
const grouped = wallTasksByStatus(definitions, tasks);
const layout = wallLayoutSize(definitions.length || 1);
const pageSize = layout.rows >= 3 ? 1 : wallCardsPerPage;
refs.wallGrid.style.setProperty('--task-board-wall-columns', String(layout.columns));
refs.wallGrid.style.setProperty('--task-board-wall-rows', String(layout.rows));
refs.wallGrid.dataset.cellCount = String(definitions.length + 1);
refs.wallGrid.dataset.rowCount = String(layout.rows);
const updatedSessionId = text(options.updatedSessionId);
const updatedTask = updatedSessionId ? tasks.find((task) => task.sessionId === updatedSessionId) : null;
const oldVisibleSessionIds = new Set(wallVisibleSessionIds);
const oldRects = new Map();
for (const [sessionId, record] of wallCardRecords) {
const rect = readWallCardRect(record.element);
if (rect) oldRects.set(sessionId, rect);
cancelWallCardAnimation(record);
}
if (!definitions.length) {
refs.wallEmpty.hidden = false;
syncNodeOrder(refs.wallGrid, [refs.clock, refs.wallEmpty]);
wallVisibleSessionIds = new Set();
for (const [sessionId, record] of wallCardRecords) {
record.element.remove();
wallCardRecords.delete(sessionId);
}
for (const [statusId, record] of wallColumnRecords) {
record.element.remove();
wallColumnRecords.delete(statusId);
}
clearWallRotation();
wallHasRendered = true;
return;
}
refs.wallEmpty.hidden = true;
refs.wallEmpty.remove();
const desiredStatusIds = new Set();
const desiredColumnNodes = [refs.clock];
definitions.forEach((definition, definitionIndex) => {
desiredStatusIds.add(definition.id);
let record = wallColumnRecords.get(definition.id);
if (!record) {
record = createWallColumnRecord(definition, definitionIndex);
wallColumnRecords.set(definition.id, record);
}
const columnTasks = grouped.get(definition.id) || [];
if (updatedTask?.statusId === definition.id) {
const updatedIndex = columnTasks.findIndex((task) => task.sessionId === updatedTask.sessionId);
if (updatedIndex >= 0) record.pageIndex = Math.floor(updatedIndex / pageSize);
}
updateWallColumnRecord(record, definition, definitionIndex, columnTasks.length, pageSize);
desiredColumnNodes.push(record.element);
});
syncNodeOrder(refs.wallGrid, desiredColumnNodes);
const desiredVisibleSessionIds = new Set();
const taskSessionIds = new Set(tasks.map((task) => task.sessionId));
const desiredCardsByStatus = new Map(definitions.map((definition) => [definition.id, []]));
const visibleTransitions = [];
definitions.forEach((definition) => {
const column = wallColumnRecords.get(definition.id);
const columnTasks = grouped.get(definition.id) || [];
const start = column.pageIndex * pageSize;
const visibleTasks = columnTasks.slice(start, start + pageSize);
for (const task of visibleTasks) {
desiredVisibleSessionIds.add(task.sessionId);
let record = wallCardRecords.get(task.sessionId);
if (!record) {
record = {
element: createNode(doc, 'article', 'ccweb-task-board__wall-card'),
signature: '',
statusId: '',
animations: new Map(),
};
wallCardRecords.set(task.sessionId, record);
}
const signature = wallCardSignature(task, definition);
if (record.signature !== signature) {
renderWallCard(task, definition, record.element);
record.signature = signature;
}
record.statusId = task.statusId;
desiredCardsByStatus.get(definition.id).push(record.element);
visibleTransitions.push({ sessionId: task.sessionId, record });
}
});
wallVisibleSessionIds = desiredVisibleSessionIds;
definitions.forEach((definition) => {
const column = wallColumnRecords.get(definition.id);
const desiredCards = desiredCardsByStatus.get(definition.id);
if (desiredCards.length) {
column.empty?.remove();
syncNodeOrder(column.body, desiredCards);
} else {
ensureWallColumnEmpty(column);
}
});
for (const [sessionId, record] of Array.from(wallCardRecords.entries())) {
if (desiredVisibleSessionIds.has(sessionId)) continue;
if (!taskSessionIds.has(sessionId)) {
cancelWallCardAnimation(record);
record.element.remove();
wallCardRecords.delete(sessionId);
continue;
}
if (record.element.parentNode && wallHasRendered) animateWallCardPageLeave(sessionId, record);
else record.element.remove();
}
for (const transition of visibleTransitions) {
const after = readWallCardRect(transition.record.element);
const before = oldRects.get(transition.sessionId);
if (wallHasRendered && before) animateWallCardMove(transition.record, before, after);
else if (wallHasRendered && !oldVisibleSessionIds.has(transition.sessionId)) {
const siblings = desiredCardsByStatus.get(transition.record.statusId) || [];
animateWallCardPageEnter(transition.record, siblings.indexOf(transition.record.element));
}
if (transition.sessionId === updatedSessionId) animateWallCardUpdate(transition.record);
}
for (const [statusId, record] of Array.from(wallColumnRecords.entries())) {
if (desiredStatusIds.has(statusId)) continue;
record.element.remove();
wallColumnRecords.delete(statusId);
}
wallHasRendered = true;
if (updatedTask) scheduleWallRotation(wallUpdateHoldMs);
else if (wallRotationTimer === null) scheduleWallRotation();
}
function advanceWallPage() {
const statusIds = overflowingWallStatusIds();
if (!statusIds.length) {
clearWallRotation();
return;
}
const statusId = statusIds[wallRotationCursor % statusIds.length];
wallRotationCursor = (wallRotationCursor + 1) % Math.max(statusIds.length, 1);
const record = wallColumnRecords.get(statusId);
if (record) record.pageIndex = (record.pageIndex + 1) % Math.max(record.totalPages, 1);
renderWall({ rotatedStatusId: statusId });
}
function emitStateChange(reason) {
if (!adapters.onStateChange) return;
adapters.onStateChange(getState(), reason);
@@ -685,7 +1179,7 @@
function taskCardSignature(task, definition) {
const selectableDefinitions = sortDefinitions(
state.statusDefinitions.filter((item) => item.enabled || item.id === task.statusId),
state.statusDefinitions.filter((item) => item.enabled && item.id !== task.statusId),
).map((item) => [item.id, item.label, item.enabled, item.order]);
return JSON.stringify([
task.sessionId,
@@ -750,13 +1244,14 @@
const statusSelect = createNode(doc, 'select', 'ccweb-task-board__card-select');
statusSelect.dataset.action = 'set-status';
statusSelect.dataset.sessionId = task.sessionId;
statusSelect.setAttribute('aria-label', `调整 ${task.title} 的任务状态`);
const selectableDefinitions = state.statusDefinitions.filter((item) => item.enabled || item.id === task.statusId);
statusSelect.setAttribute('aria-label', `移动 ${task.title} 到其他列`);
statusSelect.appendChild(optionNode(doc, '', '移动到…', true));
const selectableDefinitions = state.statusDefinitions.filter((item) => item.enabled && item.id !== task.statusId);
for (const item of sortDefinitions(selectableDefinitions)) {
statusSelect.appendChild(optionNode(doc, item.id, item.label, item.id === task.statusId));
statusSelect.appendChild(optionNode(doc, item.id, item.label));
}
statusSelect.value = task.statusId;
statusSelect.disabled = !adapters.send;
statusSelect.value = '';
statusSelect.disabled = !adapters.send || !selectableDefinitions.length;
statusLabel.appendChild(statusSelect);
const archive = createNode(
@@ -867,6 +1362,11 @@
if (state.destroyed) return;
const visibleTasks = sortTasks(state.tasks.filter(matchesFilters));
const definitions = getBoardDefinitions();
const columnCount = definitions.length;
refs.board.style.setProperty('--task-board-column-count', String(Math.max(columnCount, 1)));
refs.board.dataset.columnCount = String(columnCount);
refs.board.dataset.columnLayout = columnCount <= fitColumnLimit ? 'fit' : 'scroll';
refs.shell.dataset.searchActive = state.filters.search ? 'true' : 'false';
const animateChanges = options.animate === true && hasRenderedBoard;
const oldRects = new Map();
// 数据事件只移动 keyed 节点;更新前后的位置差用于 FLIP避免整板重绘闪烁。
@@ -987,6 +1487,7 @@
const archiveLabel = state.filters.archived ? '归档任务' : '当前任务';
refs.summary.textContent = `${visibleTasks.length}${archiveLabel} · ${definitions.length} 个状态`;
if (state.statusManagerOpen) renderStatusManager();
renderWall({ updatedSessionId: options.wallUpdatedSessionId });
hasRenderedBoard = true;
}
@@ -1213,6 +1714,9 @@
}
function applyData(data, options = {}) {
const previousWallSignatures = new Map(
state.tasks.map((task) => [task.sessionId, wallTaskSignature(task)]),
);
const envelope = collectEnvelope(data);
const definitions = findArray(envelope, ['statusDefinitions', 'definitions', 'statuses']);
const tasks = findArray(envelope, ['tasks', 'items']);
@@ -1235,7 +1739,13 @@
if (task) mergeTask(task);
const removedSessionId = text(envelope.removedSessionId || envelope.deletedSessionId);
if (removedSessionId) state.tasks = state.tasks.filter((item) => item.sessionId !== removedSessionId);
renderBoard({ animate: true });
const changedWallTask = wallHasRendered
? state.tasks
.filter((item) => previousWallSignatures.get(item.sessionId) !== wallTaskSignature(item))
.filter((item) => !item.archivedAt)
.sort(compareWallTasks)[0]
: null;
renderBoard({ animate: true, wallUpdatedSessionId: changedWallTask?.sessionId || '' });
emitStateChange(options.reason || 'data');
}
@@ -1384,13 +1894,16 @@
const select = event.target?.closest?.('[data-action="set-status"]');
if (!select) return;
const sessionId = text(select.dataset.sessionId);
const statusId = text(select.value);
if (!sessionId || !statusId) return;
const task = state.tasks.find((item) => item.sessionId === sessionId);
sendRequest({
type: MESSAGE_TYPES.statusSet,
sessionId,
statusId: text(select.value),
statusId,
...(task?.version === null ? {} : { expectedVersion: task?.version }),
}, 'task-status');
select.value = '';
}
function onBoardClick(event) {
@@ -1456,6 +1969,12 @@
if (state.destroyed) return;
state.destroyed = true;
if (searchTimer !== null) global.clearTimeout(searchTimer);
if (clockTimer !== null) global.clearTimeout(clockTimer);
clearWallRotation();
for (const record of wallCardRecords.values()) cancelWallCardAnimation(record);
wallCardRecords.clear();
wallColumnRecords.clear();
wallVisibleSessionIds = new Set();
clearBoardRecords();
boardEmpty?.remove();
refs.searchInput.removeEventListener('input', onSearchInput);
@@ -1485,6 +2004,7 @@
refs.shell.addEventListener('click', onShellClick);
refs.statusBody.addEventListener('submit', onStatusSubmit);
doc.addEventListener('keydown', onDocumentKeydown);
scheduleClock();
if (isObject(options.filters)) {
state.filters.search = text(options.filters.search || options.filters.query);