@@ -24,6 +24,14 @@ export function shortId(id) {
2424 return `#${ hex . slice ( - 6 ) } `
2525}
2626
27+ // #1252: the board lists active work; the Done column reads a bounded slice of the archive,
28+ // so a room with hundreds of finished cards still renders a column, not the whole history.
29+ export const ARCHIVE_SHOWN = 20
30+
31+ export function boardTasksFor ( tasks , archive , limit = ARCHIVE_SHOWN ) {
32+ return [ ...( tasks ?? [ ] ) , ...( archive ?? [ ] ) . slice ( 0 , limit ) ]
33+ }
34+
2735export function boardColumns ( tasks ) {
2836 const columns = { 'not-started' : [ ] , 'in-progress' : [ ] , done : [ ] }
2937 for ( const task of tasks ?? [ ] ) {
@@ -150,34 +158,47 @@ function lanesOf(tasks, roster) {
150158 return order . map ( id => ( { id, name : botName ( roster , map . get ( id ) [ 0 ] ) || id , tasks : map . get ( id ) } ) )
151159}
152160
161+ const upsert = ( list , task ) => ( list . some ( item => item . id === task . id ) ? list . map ( item => item . id === task . id ? task : item ) : [ ...list , task ] )
162+ const without = ( list , id ) => list . filter ( item => item . id !== id )
163+
153164export function useBoardTasks ( actions , scope ) {
154165 const [ own , setOwn ] = useState ( [ ] )
155166 const [ groupTasks , setGroupTasks ] = useState ( [ ] )
167+ const [ archive , setArchive ] = useState ( [ ] )
156168 const key = scope ?. kind === 'group' ? `group:${ scope . roomId } ` : 'own'
157169 useEffect ( ( ) => {
158170 let stopped = false
159171 const load = ( ) => {
172+ const query = scope ?. kind === 'group' ? { roomId : scope . roomId } : { }
173+ // #1252: the default list is active work, so the Done column asks for the archive.
160174 const job = scope ?. kind === 'group'
161- ? Promise . resolve ( actions ?. tasks ?. ( { roomId : scope . roomId } ) )
175+ ? Promise . resolve ( actions ?. tasks ?. ( query ) )
162176 : Promise . resolve ( actions ?. tasks ?. ( ) )
163177 // A reading host or an older core has no board; keep the list already shown.
164178 void job . then ( ( list ) => {
165179 if ( stopped || ! Array . isArray ( list ) ) return
166180 if ( scope ?. kind === 'group' ) setGroupTasks ( list )
167181 else setOwn ( list )
168182 } ) . catch ( ( ) => undefined )
183+ void Promise . resolve ( actions ?. tasks ?. ( { ...query , archive : true } ) ) . then ( ( list ) => {
184+ if ( stopped || ! Array . isArray ( list ) ) return
185+ setArchive ( list . filter ( task => columnOf ( task ) === 'done' ) )
186+ } ) . catch ( ( ) => undefined )
169187 }
170188 load ( )
171189 const timer = setInterval ( load , POLL_MS )
172190 return ( ) => { stopped = true ; clearInterval ( timer ) }
173191 } , [ key ] )
174192 const tasks = scope ?. kind === 'group' ? groupTasks : own . filter ( task => ! scope ?. botId || task . ownerBotId === scope . botId )
193+ // A finished card moves off the active list into the archive immediately, before the next poll.
175194 const replace = ( task ) => {
176- const write = ( list ) => ( list . some ( item => item . id === task . id ) ? list . map ( item => item . id === task . id ? task : item ) : [ ...list , task ] )
177- if ( scope ?. kind === 'group' ) setGroupTasks ( write )
178- else setOwn ( write )
195+ const done = columnOf ( task ) === 'done'
196+ if ( scope ?. kind === 'group' ) setGroupTasks ( list => done ? without ( list , task . id ) : upsert ( list , task ) )
197+ else setOwn ( list => done ? without ( list , task . id ) : upsert ( list , task ) )
198+ setArchive ( list => done ? [ task , ...without ( list , task . id ) ] : without ( list , task . id ) )
179199 }
180- return { own, tasks, replace }
200+ const done = scope ?. kind === 'group' ? archive : archive . filter ( task => ! scope ?. botId || task . ownerBotId === scope . botId )
201+ return { own, tasks, archive : done , replace }
181202}
182203
183204function RailButton ( { row, selected, meta, onSelect } ) {
@@ -338,11 +359,12 @@ export function KanbanDialog({ roster, actions, leaving = false }) {
338359 const [ dragging , setDragging ] = useState ( null )
339360 const [ error , setError ] = useState ( '' )
340361 const [ draft , setDraft ] = useState ( '' )
341- const { own, tasks, replace } = useBoardTasks ( actions , scope )
362+ const { own, tasks, archive, replace } = useBoardTasks ( actions , scope )
363+ const boardTasks = boardTasksFor ( tasks , archive )
342364 const close = useCallback ( ( ) => actions . closeBoard ( ) , [ ] )
343365 useEscape ( close )
344366 const counts = { }
345- for ( const task of own ) {
367+ for ( const task of [ ... own , ... archive ] ) {
346368 const id = task . ownerBotId
347369 const bucket = counts [ id ] ?? ( counts [ id ] = { done : 0 , total : 0 } )
348370 if ( task . status === 'cancelled' || task . status === 'failed' ) continue
@@ -385,7 +407,7 @@ export function KanbanDialog({ roster, actions, leaving = false }) {
385407 view === 'delivery'
386408 ? h ( DeliveryView , { actions } )
387409 : h ( BoardView , {
388- roster, tasks, rail, scope, counts, dragging, error, draft,
410+ roster, tasks : boardTasks , rail, scope, counts, dragging, error, draft,
389411 onDraft : setDraft , onSelect : setScope , onMove : move , onDrag : setDragging , onCreate : create ,
390412 } ) ) )
391413}
0 commit comments