@linxin666/dsh-client-ui-task-board 0.1.11 → 0.1.13

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.
@@ -8,7 +8,7 @@
8
8
  * shell fails the whole boot when a plugin apply throws, and an external
9
9
  * plugin must not take the GUI down.
10
10
  */
11
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
11
+ import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client';
12
12
  import { type TaskBoardKey } from './locales.ts';
13
13
  declare module '@deepseek-ai/dsh-client-ui-slots' {
14
14
  interface LocaleNamespaceMap {
@@ -34,6 +34,18 @@ export interface SettingsPluginItemOwnerProps {
34
34
  /** Marker field: card owner props are intentionally empty. */
35
35
  children?: never;
36
36
  }
37
+ declare module '@deepseek-ai/cordis' {
38
+ interface Context {
39
+ /**
40
+ * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
41
+ * absent when that group plugin is not installed, so callers fall back to
42
+ * the official settings scope.
43
+ */
44
+ webUiSettings?: {
45
+ bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S>;
46
+ };
47
+ }
48
+ }
37
49
  /** Required services (fiber inject waiting — the runtime must be up first). */
38
50
  export declare const inject: string[];
39
51
  /**
@@ -7,9 +7,26 @@ export interface TaskStore {
7
7
  save(tasks: readonly TaskRecord[]): void;
8
8
  /** Drop the persisted ledger (leaves the in-memory state alone). */
9
9
  clear(): void;
10
+ /**
11
+ * Subscribe to ledger changes written by ANOTHER tab of the same origin
12
+ * (browser storage events). The board controller reloads the ledger on
13
+ * such a change, so a task deleted in one tab cannot keep firing (or be
14
+ * written back) from the stale in-memory copy of another tab. No-op when
15
+ * the backend has no cross-instance channel (in-memory store).
16
+ */
17
+ subscribeExternal?(listener: () => void): () => void;
10
18
  }
11
19
  /** Storage key for the task ledger document. */
12
20
  export declare const DEFAULT_STORAGE_KEY = "dsh.taskBoard.v1";
21
+ /** Structural shape of the storage event fired in sibling tabs (DOM-free). */
22
+ export interface StorageChangeEvent {
23
+ key: string | null;
24
+ }
25
+ /** The event-target face the store needs for cross-tab notifications. */
26
+ export interface StorageEvents {
27
+ addEventListener(type: 'storage', listener: (event: StorageChangeEvent) => void): void;
28
+ removeEventListener(type: 'storage', listener: (event: StorageChangeEvent) => void): void;
29
+ }
13
30
  /** A task record is structurally valid if it round-trips through the UI. */
14
31
  export declare function isTaskRecord(value: unknown): value is TaskRecord;
15
32
  /** Parse + validate a persisted ledger document; invalid rows are dropped. */
@@ -18,14 +35,25 @@ export declare function parseLedger(raw: string | null): TaskRecord[];
18
35
  export declare class LocalStorageTaskStore implements TaskStore {
19
36
  private readonly key;
20
37
  private readonly storage;
38
+ private readonly events;
21
39
  /**
22
40
  * @param key - storage key for the ledger document.
23
41
  * @param storage - storage backend (defaults to the global localStorage; tests inject fakes).
42
+ * @param events - storage-event target for cross-tab notifications (defaults
43
+ * to the browser global; undefined in non-browser runtimes, where the
44
+ * subscription becomes a no-op).
24
45
  */
25
- constructor(key?: string, storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined);
46
+ constructor(key?: string, storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined, events?: StorageEvents | undefined);
26
47
  load(): TaskRecord[];
27
48
  save(tasks: readonly TaskRecord[]): void;
28
49
  clear(): void;
50
+ /**
51
+ * Cross-tab change subscription (see {@link TaskStore.subscribeExternal}).
52
+ * The browser fires the storage event in every OTHER tab of the same origin
53
+ * when one tab writes; a null key means the whole storage was cleared. Both
54
+ * cases reload the ledger here; unrelated keys are ignored.
55
+ */
56
+ subscribeExternal(listener: () => void): () => void;
29
57
  }
30
58
  /** In-memory backend (tests, and a fallback when storage is unavailable). */
31
59
  export declare class InMemoryTaskStore implements TaskStore {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.1.11",
4
+ "version": "0.1.13",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -35,10 +35,6 @@
35
35
  }
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
39
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
40
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
41
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
42
38
  "react": "^18.2.0"
43
39
  },
44
40
  "dependencies": {
@@ -72,7 +68,7 @@
72
68
  "src",
73
69
  "cordis.patch.yml"
74
70
  ],
75
- "license": "BSD-3-Clause",
71
+ "license": "Apache-2.0",
76
72
  "repository": {
77
73
  "type": "git",
78
74
  "url": "https://github.com/zhu1090093659/dsh-web-ui.git"
@@ -41,13 +41,14 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
41
41
  if (!state.available) return null
42
42
  const title = props.t(props.titleKey)
43
43
  const blocked = !state.dirty || state.invalid || state.saving
44
+ const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card
44
45
  // The namespace exists but the Host does not serve it to this client (the
45
46
  // official settings allowlist omits third-party namespaces): show a card
46
47
  // that explains the gap instead of vanishing, so a missing card never
47
48
  // reads as a missing plugin.
48
49
  if (!state.exposed) {
49
50
  return (
50
- <li className={css.card}>
51
+ <li className={cardClass}>
51
52
  <button
52
53
  type="button"
53
54
  className={css.header}
@@ -59,7 +60,19 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
59
60
  <span className={css.name}>{title}</span>
60
61
  <span className={css.description}>{props.t(props.descriptionKey)}</span>
61
62
  </span>
62
- <span className={open ? css.chevronOpen : css.chevron}>▾</span>
63
+ <svg
64
+ width="14"
65
+ height="14"
66
+ viewBox="0 0 14 14"
67
+ fill="none"
68
+ xmlns="http://www.w3.org/2000/svg"
69
+ className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
70
+ >
71
+ <path
72
+ d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
73
+ fill="currentColor"
74
+ />
75
+ </svg>
63
76
  </button>
64
77
  {open
65
78
  ? (
@@ -72,7 +85,7 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
72
85
  )
73
86
  }
74
87
  return (
75
- <li className={css.card}>
88
+ <li className={cardClass}>
76
89
  <button
77
90
  type="button"
78
91
  className={css.header}
@@ -85,7 +98,19 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
85
98
  <span className={css.description}>{props.t(props.descriptionKey)}</span>
86
99
  </span>
87
100
  {state.dirty ? <span className={css.pending}>{props.t('settings.unsaved')}</span> : null}
88
- <span className={open ? css.chevronOpen : css.chevron}>▾</span>
101
+ <svg
102
+ width="14"
103
+ height="14"
104
+ viewBox="0 0 14 14"
105
+ fill="none"
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
108
+ >
109
+ <path
110
+ d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
111
+ fill="currentColor"
112
+ />
113
+ </svg>
89
114
  </button>
90
115
  {open
91
116
  ? (
@@ -234,4 +259,4 @@ export function BooleanField(props: FieldProps & {
234
259
  <p className={css.hint}>{props.hint}</p>
235
260
  </div>
236
261
  )
237
- }
262
+ }
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * The `conversation` slot is single-occupant (ui-conversation) and external
5
5
  * plugins cannot declare slots, so the board takes over the center column at
6
- * the DOM level: a container is appended inside the `[data-pane="conversation"]`
7
- * grid item (an extra trailing child React never manages), and a stylesheet
6
+ * the DOM level: a container is appended inside the center column
7
+ * (`[class*="centerCol"]`, the dsh 0.1.0-rc.6 AppFrame layout; previously
8
+ * `[data-pane="conversation"]` on older shells — the mount selector keeps both)
9
+ * as an extra trailing child
10
+ * React never manages, and a stylesheet
8
11
  * rule hides the conversation content while the board is active. Toggling is
9
12
  * a data attribute on <html> — no React involvement, so the conversation
10
13
  * subtree underneath stays mounted and stateful.
@@ -17,7 +20,7 @@ import css from './board.module.css'
17
20
  /** The injected board container (kept in the DOM, hidden when inactive). */
18
21
  export const BOARD_VIEW_SELECTOR = '[data-dsh-taskboard-view]'
19
22
 
20
- const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
23
+ const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
21
24
  const ACTIVE_ATTR = 'data-dsh-taskboard-active'
22
25
  /** The sibling panel's activation attribute (ssh), removed when this panel opens. */
23
26
  const OTHER_ACTIVE_ATTR = 'data-dsh-ssh-active'
@@ -6,7 +6,8 @@
6
6
 
7
7
  /* --- center-column takeover (global rules, attribute-scoped) ----------------- */
8
8
 
9
- [data-pane='conversation'] {
9
+ [data-pane='conversation'],
10
+ [class*='centerCol'] {
10
11
  position: relative;
11
12
  }
12
13
 
@@ -17,6 +18,11 @@
17
18
  inset: 0;
18
19
  display: none;
19
20
  z-index: 60;
21
+ /* Opaque backdrop: the conversation subtree stays mounted under the view.
22
+ Even if the hide rule below were defeated by a stronger host inline
23
+ style, this background keeps sticky code-block banners (host z-index 6)
24
+ from showing through the active board (issue #100). */
25
+ background: var(--dsw-alias-bg-base);
20
26
  }
21
27
 
22
28
  /* The center column is single-occupant; the :not() guards keep the two
@@ -31,8 +37,12 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-dsh-taskboard-v
31
37
  (0.1.0-rc.6) wraps the conversation view in a node with an inline
32
38
  `display: contents`, and inline styles beat a plain stylesheet rule. Without
33
39
  it the composer (input card) stays visible at the bottom and paints over the
34
- board modals' footer, hiding the run/delete buttons (issue #76). */
35
- html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-taskboard-view]) {
40
+ board modals' footer, hiding the run/delete buttons (issue #76).
41
+ The rule targets both the pane attribute and the center-column class so the
42
+ chat subtree (including the code-block copy banner, issue #100) stays hidden
43
+ whichever host container carries it. */
44
+ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-taskboard-view]),
45
+ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol'] > :not([data-dsh-taskboard-view]) {
36
46
  display: none !important;
37
47
  }
38
48
 
@@ -8,7 +8,7 @@
8
8
  * shell fails the whole boot when a plugin apply throws, and an external
9
9
  * plugin must not take the GUI down.
10
10
  */
11
- import type { ClientContext, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
11
+ import type { ClientContext, SessionId, SettingsScope, SettingsScopeSpec, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
12
12
  import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
13
13
  import type {} from '@deepseek-ai/dsh-client-ui-slots'
14
14
  // Type-only: pulls the locale plugin's Context merge (ctx.locale) and its
@@ -54,6 +54,18 @@ export interface SettingsPluginItemOwnerProps {
54
54
  children?: never
55
55
  }
56
56
 
57
+ declare module '@deepseek-ai/cordis' {
58
+ interface Context {
59
+ /**
60
+ * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
61
+ * absent when that group plugin is not installed, so callers fall back to
62
+ * the official settings scope.
63
+ */
64
+ webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }
65
+ }
66
+ }
67
+
68
+
57
69
  /** Required services (fiber inject waiting — the runtime must be up first). */
58
70
  export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'settingsScope', 'locale', 'remote']
59
71
 
@@ -66,7 +78,8 @@ export function apply(ctx: ClientContext): void {
66
78
 
67
79
  // Plugin configuration card: one staged form over the `task-board` settings
68
80
  // namespace, contributed to the Web UI plugin group.
69
- const settingsScope = ctx.settingsScope.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
81
+ const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
82
+ const settingsScope = binder.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
70
83
  const settingsCard = new TaskBoardSettingsCardController(settingsScope)
71
84
  ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
72
85
  name: 'web-ui.plugin.item',