feat(web): rework the todo / background-task panels

Four fixes folded together:
- The ~/todo TAB was still collapsible, so opening it could hide everything.
  Collapse now applies only to the floating card; the tab is always expanded.
- The floating background-task card had no collapse. Add a chevron toggle while
  keeping the header click as 'open the tasks tab'.
- The card truncated to 4 rows and showed '+N' even with room. Raise the cap to
  12 and let the list scroll (max-height) so it shows more before truncating.
- Bump the floating stack width 260→300 and card font so the panels aren't so
  cramped.
This commit is contained in:
qer 2026-06-13 12:33:10 +08:00
parent 0f33d72a3c
commit da18be330c
5 changed files with 79 additions and 20 deletions

View file

@ -871,7 +871,9 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
gap: 10px;
width: 260px;
/* Wider than the old 260px — the todo/task cards were cramped. */
width: 300px;
max-width: calc(100vw - 32px);
}
@media (max-width: 1199px) {
.float-stack { display: none; }

View file

@ -3,11 +3,14 @@
of the conversation pane, codex-style). Shows the running tasks only;
clicking the header jumps to the full ~/tasks tab. -->
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { TaskItem } from '../types';
const MAX_ROWS = 4;
// Show plenty of rows before truncating the old cap of 4 collapsed even a
// handful of tasks into a "+N" when the card had room. Beyond this the list
// scrolls (see .tk-list max-height) instead of growing without bound.
const MAX_ROWS = 12;
const props = defineProps<{
tasks: TaskItem[];
@ -19,6 +22,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const collapsed = ref(false);
const running = computed(() => props.tasks.filter((tk) => tk.state === 'run'));
const shown = computed(() => running.value.slice(0, MAX_ROWS));
const overflow = computed(() => running.value.length - shown.value.length);
@ -26,12 +30,25 @@ const overflow = computed(() => running.value.length - shown.value.length);
<template>
<div class="tasks-card">
<button class="tk-head" type="button" :title="t('tasks.openTab')" @click="emit('open')">
<span class="tk-dot" aria-hidden="true" />
<span class="tk-title">{{ t('sidebar.tabTasks') }}</span>
<span class="tk-count">{{ running.length }}</span>
</button>
<div class="tk-list">
<div class="tk-head">
<button class="tk-open" type="button" :title="t('tasks.openTab')" @click="emit('open')">
<span class="tk-dot" aria-hidden="true" />
<span class="tk-title">{{ t('sidebar.tabTasks') }}</span>
<span class="tk-count">{{ running.length }}</span>
</button>
<button
class="tk-collapse"
type="button"
:aria-expanded="!collapsed"
:title="collapsed ? t('tasks.expand') : t('tasks.collapse')"
@click="collapsed = !collapsed"
>
<svg class="tk-chev" :class="{ open: !collapsed }" viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="4,6 8,10 12,6" />
</svg>
</button>
</div>
<div v-if="!collapsed" class="tk-list">
<div v-for="tk in shown" :key="tk.id" class="tk-row">
<span class="tk-name">{{ tk.name }}</span>
<span class="tk-time">{{ tk.timing }}</span>
@ -53,18 +70,40 @@ const overflow = computed(() => running.value.length - shown.value.length);
.tk-head {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 6px 10px;
color: var(--muted);
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.4;
}
.tk-open {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
padding: 7px 4px 7px 10px;
background: none;
border: none;
cursor: pointer;
color: var(--muted);
font-family: var(--mono);
font-size: 12px;
line-height: 1.4;
color: inherit;
font: inherit;
}
.tk-head:hover { color: var(--ink); }
.tk-open:hover { color: var(--ink); }
.tk-collapse {
flex: none;
display: flex;
align-items: center;
justify-content: center;
padding: 7px 10px;
background: none;
border: none;
cursor: pointer;
color: var(--faint);
}
.tk-collapse:hover { color: var(--ink); }
.tk-chev { transition: transform 0.15s; transform: rotate(-90deg); }
.tk-chev.open { transform: none; }
.tk-title { font-weight: 700; letter-spacing: 0.04em; }
.tk-count { color: var(--faint); }
@ -84,6 +123,8 @@ const overflow = computed(() => running.value.length - shown.value.length);
.tk-list {
border-top: 1px solid var(--line);
padding: 4px 10px 6px;
max-height: 45vh;
overflow-y: auto;
}
.tk-row {
display: flex;

View file

@ -16,7 +16,16 @@ const props = defineProps<{
const { t } = useI18n();
// Collapse only applies to the floating CARD (it overlays the transcript). In
// the dedicated ~/todo TAB the whole pane IS the list, so collapsing it would
// just hide everything the user opened the tab to see keep it always open.
const collapsed = ref(false);
const canCollapse = computed(() => !props.inline);
const showList = computed(() => props.inline || !collapsed.value);
function toggle(): void {
if (canCollapse.value) collapsed.value = !collapsed.value;
}
const doneCount = computed(() => props.todos.filter((td) => td.status === 'done').length);
@ -27,7 +36,7 @@ function glyph(status: TodoView['status']): string {
<template>
<div class="todo-card" :class="{ 'tab-mode': inline }">
<button class="tc-head" type="button" @click="collapsed = !collapsed">
<component :is="canCollapse ? 'button' : 'div'" class="tc-head" :class="{ static: !canCollapse }" :type="canCollapse ? 'button' : undefined" @click="toggle">
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true">
<polyline points="2,4.5 3.5,6 5.5,3" />
<polyline points="2,11 3.5,12.5 5.5,9.5" />
@ -36,12 +45,12 @@ function glyph(status: TodoView['status']): string {
</svg>
<span class="tc-title">{{ t('tasks.todoTag') }}</span>
<span v-if="todos.length > 0" class="tc-count">{{ doneCount }}/{{ todos.length }}</span>
<svg v-if="todos.length > 0" class="tc-chev" :class="{ open: !collapsed }" viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<svg v-if="canCollapse && todos.length > 0" class="tc-chev" :class="{ open: !collapsed }" viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="4,6 8,10 12,6" />
</svg>
</button>
</component>
<div v-if="!collapsed" class="tc-list">
<div v-if="showList" class="tc-list">
<div v-if="todos.length === 0" class="tc-empty">{{ t('tasks.emptyTodo') }}</div>
<div v-for="(td, i) in todos" :key="i" class="tc-row" :class="`s-${td.status}`">
<span class="tc-glyph">{{ glyph(td.status) }}</span>
@ -91,6 +100,9 @@ function glyph(status: TodoView['status']): string {
line-height: 1.4;
}
.tc-head:hover { color: var(--ink); }
/* Tab-mode header is a static label, not a collapse toggle. */
.tc-head.static { cursor: default; }
.tc-head.static:hover { color: var(--muted); }
.tc-title { font-weight: 700; letter-spacing: 0.04em; }
.tc-count { color: var(--faint); }
.tc-chev {

View file

@ -9,4 +9,6 @@ export default {
emptyTasks: 'No background tasks running',
emptyTodo: 'No todos yet',
openTab: 'Open the tasks tab',
collapse: 'Collapse',
expand: 'Expand',
} as const;

View file

@ -9,4 +9,6 @@ export default {
emptyTasks: '暂无后台任务',
emptyTodo: '暂无待办事项',
openTab: '查看全部后台任务',
collapse: '折叠',
expand: '展开',
};