@linxin666/dsh-client-ui-task-board 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,6 @@ import type { TaskRecord } from '../../core/tasks.ts';
2
2
  /** Compact relative/absolute time label. */
3
3
  export declare function formatHostTimestamp(ms: number, timeZone?: string): string;
4
4
  export declare function formatTime(ms: number, timeZone?: string): string;
5
- /** One card in a column. */
6
5
  declare function TaskCardInner({ task, pending, timeZone, onClick }: {
7
6
  task: TaskRecord;
8
7
  pending: boolean;
@@ -2,8 +2,8 @@
2
2
  * Board controller: the single owner of task-ledger state and view state.
3
3
  *
4
4
  * In production it projects the Host ledger and submits confirmed actions;
5
- * the legacy store seam remains for v1 migration tests. It also closes the
6
- * board whenever the user navigates to a session.
5
+ * the legacy store seam remains for v1 migration tests. The board closes
6
+ * only on explicit user navigation, never implicitly on session-list churn.
7
7
  * Framework-free (structural runtime faces) so the whole orchestration is
8
8
  * unit-testable with fakes.
9
9
  *
@@ -188,9 +188,17 @@ export declare class BoardController {
188
188
  runTask(id: string): Promise<boolean>;
189
189
  /** Re-run a settled task through the Host (the Host replans and executes). */
190
190
  rerunTask(id: string): Promise<void>;
191
- /** Close the board when the user navigates to another session. */
191
+ /**
192
+ * Session-list notifications fire for all kinds of incidental churn
193
+ * (background navigation, the Host runner creating and selecting a fresh
194
+ * execution session, settlement, other plugins), so closing on `current`
195
+ * changes would evict the board without the user asking. The board closes
196
+ * only on explicit user navigation: a sidebar session/workspace row click
197
+ * (board-mount onClickSidebarRow) or the board's own actions
198
+ * (openSession / close). Keeping the hook preserves the subscription
199
+ * contract for future listeners.
200
+ */
192
201
  private onSessionsChanged;
193
- private lastCurrent;
194
202
  private persistAndNotify;
195
203
  private commitRemote;
196
204
  private performRemote;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
- "version": "0.3.4",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { memo, useCallback, useEffect, useState } from 'react'
7
7
  import { selectedTaskOf, type BoardController } from '../../core/controller.ts'
8
- import { COLUMNS, type TaskRecord } from '../../core/tasks.ts'
8
+ import { COLUMNS, canMoveManually, type TaskRecord } from '../../core/tasks.ts'
9
9
  import { t } from '../locales.ts'
10
10
  import css from '../board.module.css'
11
11
  import { NewTaskModal } from './NewTaskModal.tsx'
@@ -124,8 +124,27 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
124
124
  ) : (
125
125
  COLUMNS.map(column => {
126
126
  const tasks = visible.filter(task => task.status === column.status)
127
+ const isManualDropTarget = column.status === 'backlog' || column.status === 'todo'
127
128
  return (
128
- <section key={column.status} className={css.column} data-status={column.status} data-dsh-part="column">
129
+ <section
130
+ key={column.status}
131
+ className={css.column}
132
+ data-status={column.status}
133
+ data-dsh-part="column"
134
+ onDragOver={isManualDropTarget ? (event) => {
135
+ event.preventDefault()
136
+ event.dataTransfer.dropEffect = 'move'
137
+ } : undefined}
138
+ onDrop={isManualDropTarget ? (event) => {
139
+ event.preventDefault()
140
+ const taskId = event.dataTransfer.getData('text/plain')
141
+ if (!taskId) return
142
+ const dropped = snapshot.tasks.find(t => t.id === taskId)
143
+ if (dropped && canMoveManually(dropped.status, column.status) && dropped.status !== column.status) {
144
+ controller.moveTask(taskId, column.status)
145
+ }
146
+ } : undefined}
147
+ >
129
148
  <header className={css.columnHeader}>
130
149
  <span className={css.statusDot} data-status={column.status} aria-hidden="true" />
131
150
  <h3 className={css.columnTitle}>{t(STATUS_KEY[column.status])}</h3>
@@ -37,11 +37,12 @@ export function formatTime(ms: number, timeZone?: string): string {
37
37
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
38
38
  }
39
39
 
40
- /** One card in a column. */
41
40
  function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord; pending: boolean; timeZone?: string; onClick: () => void }) {
42
41
  const latest = task.executions[task.executions.length - 1]
43
42
  const runs = task.executions.length
44
43
  const archived = task.archivedAt !== undefined
44
+ const isDraggable = !archived && task.status !== 'running' && !pending
45
+
45
46
  return (
46
47
  <button
47
48
  type="button"
@@ -49,6 +50,11 @@ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord;
49
50
  data-status={archived ? 'archived' : task.status}
50
51
  data-dsh-part="card"
51
52
  data-pending={pending || undefined}
53
+ draggable={isDraggable}
54
+ onDragStart={isDraggable ? (event) => {
55
+ event.dataTransfer.setData('text/plain', task.id)
56
+ event.dataTransfer.effectAllowed = 'move'
57
+ } : undefined}
52
58
  onClick={onClick}
53
59
  title={task.description !== '' ? task.description : task.title}
54
60
  >
@@ -2,8 +2,8 @@
2
2
  * Board controller: the single owner of task-ledger state and view state.
3
3
  *
4
4
  * In production it projects the Host ledger and submits confirmed actions;
5
- * the legacy store seam remains for v1 migration tests. It also closes the
6
- * board whenever the user navigates to a session.
5
+ * the legacy store seam remains for v1 migration tests. The board closes
6
+ * only on explicit user navigation, never implicitly on session-list churn.
7
7
  * Framework-free (structural runtime faces) so the whole orchestration is
8
8
  * unit-testable with fakes.
9
9
  *
@@ -108,11 +108,6 @@ function randomUuid(): string {
108
108
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
109
109
  }
110
110
 
111
- /** Read the current selection off a session-list snapshot (structural). */
112
- function currentOf(sessions: SessionsControllerFace): string | undefined {
113
- return sessions.list.getSnapshot().current
114
- }
115
-
116
111
  function messageOf(error: unknown): string {
117
112
  return error instanceof Error ? error.message : String(error)
118
113
  }
@@ -205,10 +200,11 @@ export class BoardController {
205
200
 
206
201
  openBoard(): void {
207
202
  if (this.boardOpen) return
208
- // Baseline the selection the board opened against: the board stays open
209
- // until the user navigates (selection changes), never on mere status
210
- // updates of the already-selected session.
211
- this.lastCurrent = currentOf(this.deps.sessions)
203
+ // The board stays open until the user explicitly closes it: by clicking
204
+ // the sidebar entry again, a sidebar session/workspace row, or a board
205
+ // action (openSession / close). Session-list churn (background
206
+ // navigation, the Host runner selecting a fresh execution session,
207
+ // settlement, other plugins) never evicts the board.
212
208
  this.boardOpen = true
213
209
  this.notify()
214
210
  }
@@ -428,16 +424,20 @@ export class BoardController {
428
424
 
429
425
  // --- internals ---------------------------------------------------------------
430
426
 
431
- /** Close the board when the user navigates to another session. */
427
+ /**
428
+ * Session-list notifications fire for all kinds of incidental churn
429
+ * (background navigation, the Host runner creating and selecting a fresh
430
+ * execution session, settlement, other plugins), so closing on `current`
431
+ * changes would evict the board without the user asking. The board closes
432
+ * only on explicit user navigation: a sidebar session/workspace row click
433
+ * (board-mount onClickSidebarRow) or the board's own actions
434
+ * (openSession / close). Keeping the hook preserves the subscription
435
+ * contract for future listeners.
436
+ */
432
437
  private onSessionsChanged(): void {
433
- if (!this.boardOpen) return
434
- const current = currentOf(this.deps.sessions)
435
- if (current !== this.lastCurrent) this.closeBoard()
436
- this.lastCurrent = current
438
+ // Intentionally empty: never close the board implicitly.
437
439
  }
438
440
 
439
- private lastCurrent: string | undefined = undefined
440
-
441
441
  private persistAndNotify(): void {
442
442
  if (this.deps.transport === undefined) this.deps.store.save(this.tasks)
443
443
  this.notify()