@fonlan/dsh-task-kanban 0.1.1 → 0.2.1

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.
@@ -21,23 +21,29 @@
21
21
  .kb-board-header {
22
22
  display: flex;
23
23
  align-items: center;
24
- justify-content: space-between;
25
24
  gap: 12px;
26
25
  padding-bottom: 10px;
27
26
  border-bottom: 1px solid rgba(128, 128, 128, 0.25);
28
27
  flex: 0 0 auto;
29
28
  }
30
- .kb-board-actions { display: flex; gap: 8px; align-items: center; flex: none; }
31
- .kb-board-actions .kb-btn { flex: none; }
29
+ /* Left-aligned group: workspace selector + new-task button sit adjacent. */
30
+ .kb-board-title {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 8px;
34
+ min-width: 0;
35
+ flex: 0 1 auto;
36
+ }
37
+ .kb-board-title .kb-btn { flex: none; }
32
38
  .kb-ws-select {
33
39
  max-width: 320px;
34
40
  min-width: 0;
35
41
  flex: 0 1 auto;
36
42
  padding: 6px 10px;
37
43
  border-radius: 6px;
38
- border: 1px solid rgba(128, 128, 128, 0.35);
39
- background: rgba(128, 128, 128, 0.12);
40
- color: inherit;
44
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));
45
+ background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.12)));
46
+ color: var(--dsw-alias-label-primary, inherit);
41
47
  font-size: 14px;
42
48
  }
43
49
  .kb-btn {
@@ -266,17 +272,23 @@
266
272
  text-overflow: ellipsis;
267
273
  }
268
274
  .kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
275
+ /* Field labels in a row never wrap; selects get the remaining fixed width. */
276
+ .kb-field-row > span { white-space: nowrap; flex-shrink: 0; margin-bottom: 0; }
269
277
  .kb-input {
270
278
  width: 100%;
271
279
  box-sizing: border-box;
272
280
  padding: 7px 10px;
273
281
  border-radius: 6px;
274
- border: 1px solid rgba(128, 128, 128, 0.35);
275
- background: rgba(128, 128, 128, 0.1);
276
- color: inherit;
282
+ /* Form controls follow the shell theme: DSH's input surface token plus a
283
+ theme-tuned border; the fallbacks cover shells without these tokens. */
284
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));
285
+ background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.1)));
286
+ color: var(--dsw-alias-label-primary, inherit);
277
287
  font-size: 13px;
278
288
  }
279
289
  .kb-input-number { width: 90px; }
290
+ /* All settings selects share one width so the column looks even. */
291
+ .kb-settings select.kb-input { width: 200px; flex: 0 0 200px; }
280
292
  .kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }
281
293
  .kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
282
294
  .kb-detail-section { margin-bottom: 12px; }
@@ -302,18 +314,97 @@
302
314
  .kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }
303
315
  .kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }
304
316
  .kb-plan-summary { font-size: 13px; margin-bottom: 10px; }
305
- .kb-plan-phases { margin: 0; padding-left: 20px; }
306
- .kb-plan-phase { margin-bottom: 10px; }
307
- .kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }
308
- .kb-plan-phase-head { display: flex; gap: 8px; align-items: baseline; }
317
+ /* Phase cards: each plan phase is a card with an ordered number (kept from the
318
+ parent <ol>) and three labeled sections (goal / sessions / conclusion) split
319
+ by section titles and thin divider lines. */
320
+ .kb-plan-phases { margin: 0; padding-left: 24px; }
321
+ .kb-plan-phase {
322
+ margin-bottom: 10px;
323
+ padding: 8px 10px;
324
+ background: rgba(128, 128, 128, 0.08);
325
+ border: 1px solid rgba(128, 128, 128, 0.2);
326
+ border-radius: 8px;
327
+ }
328
+ .kb-plan-phase-head {
329
+ display: flex;
330
+ gap: 8px;
331
+ align-items: baseline;
332
+ padding-bottom: 6px;
333
+ margin-bottom: 6px;
334
+ border-bottom: 1px solid rgba(128, 128, 128, 0.16);
335
+ }
309
336
  .kb-plan-phase-title { font-weight: 600; font-size: 13px; }
310
337
  .kb-plan-phase-id { font-size: 11px; opacity: 0.5; }
311
- .kb-plan-phase-goal { font-size: 12px; opacity: 0.85; margin: 2px 0 6px; white-space: pre-wrap; }
338
+ .kb-plan-phase-body { display: flex; flex-direction: column; }
339
+ /* Section isolation: label on top, thin divider between adjacent sections. */
340
+ .kb-plan-phase-section { display: flex; flex-direction: column; }
341
+ .kb-plan-phase-section + .kb-plan-phase-section {
342
+ margin-top: 6px;
343
+ padding-top: 6px;
344
+ border-top: 1px solid rgba(128, 128, 128, 0.14);
345
+ }
346
+ .kb-plan-phase-section-title { font-size: 11px; opacity: 0.55; margin-bottom: 3px; }
347
+ .kb-plan-phase-goal { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }
348
+ .kb-plan-phase-sessions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
349
+ .kb-plan-phase-conclusion { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }
350
+ .kb-plan-phase-conclusion-item + .kb-plan-phase-conclusion-item { margin-top: 6px; }
351
+ /* Running phase keeps the blue highlight; completed phases get success color. */
352
+ .kb-plan-phase-current { border-color: rgba(59, 130, 246, 0.6); background: rgba(59, 130, 246, 0.08); }
353
+ .kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }
354
+ .kb-plan-phase-completed { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.06); }
355
+ .kb-plan-phase-completed .kb-plan-phase-title { color: #4ade80; }
312
356
  .kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
313
- .kb-session-attempt { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
314
- .kb-session-summary { font-size: 12px; opacity: 0.75; }
315
357
  .kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }
316
358
  .kb-settings-saved { font-size: 12px; color: #4ade80; }
359
+ .kb-settings-group { border-top: 1px solid rgba(128, 128, 128, 0.22); padding-top: 12px; }
360
+ .kb-settings-group-title { font-size: 12px; font-weight: 600; opacity: 0.9; margin-bottom: 10px; }
361
+ /* Settings card (设置 → 插件配置): mirrors the built-in plugin cards
362
+ (dsh-client-ui-settings-plugins PluginCard) — same surface, border, head
363
+ layout, and disclosure chevron, spelled with kb-settings-card prefixed
364
+ classes. The prefix deliberately does NOT reuse the board's `.kb-card*`
365
+ classes: the board card carries its own padding/cursor/user-select, and
366
+ sharing the root class would leak those onto the settings card. */
367
+ .kb-settings-card {
368
+ border: 1px solid var(--dsw-alias-border-l2);
369
+ background: var(--dsw-alias-bg-layer-3);
370
+ border-radius: 12px;
371
+ list-style: none;
372
+ transition: border-color .16s, background .16s;
373
+ }
374
+ .kb-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }
375
+ .kb-settings-card[data-open] {
376
+ background: var(--dsw-alias-bg-layer-2);
377
+ border-color: var(--dsw-alias-label-dimmed);
378
+ }
379
+ .kb-settings-card-head {
380
+ appearance: none;
381
+ width: 100%;
382
+ font: inherit;
383
+ color: inherit;
384
+ text-align: left;
385
+ cursor: pointer;
386
+ background: transparent;
387
+ border: 0;
388
+ border-radius: 12px;
389
+ align-items: center;
390
+ gap: 12px;
391
+ padding: 14px 16px;
392
+ display: flex;
393
+ }
394
+ .kb-settings-card-head:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
395
+ .kb-settings-card-headText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; display: flex; }
396
+ .kb-settings-card-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
397
+ .kb-settings-card-sub { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }
398
+ .kb-settings-card-chevron {
399
+ color: var(--dsw-alias-label-tertiary);
400
+ flex: none;
401
+ transition: transform .16s;
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ }
406
+ .kb-settings-card-chevron[data-open] { transform: rotate(180deg); }
407
+ .kb-settings-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 12px 0 8px; }
317
408
  .kb-footer-action {
318
409
  display: inline-flex;
319
410
  align-items: center;
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
- import type { KanbanCard, Lane } from '../shared/card'
2
+ import type { KanbanCard, Lane, PhaseAttempt, PlanPhase } from '../shared/card'
3
3
  import { DROP_RULES, laneOf } from '../shared/lanes'
4
4
  import { api, gatewaySkillList, type SkillOption } from './api'
5
5
  import { exitBoard, getClient } from './kanban-state'
@@ -189,8 +189,6 @@ export function BoardRoot(props: BoardProps): JSX.Element {
189
189
  <option key={w.workspaceId} value={w.path}>{w.title}</option>
190
190
  ))}
191
191
  </select>
192
- </div>
193
- <div className="kb-board-actions">
194
192
  <button type="button" className="kb-btn kb-btn-primary" onClick={() => setNewTaskOpen(true)}>{t('newTask')}</button>
195
193
  </div>
196
194
  </div>
@@ -328,13 +326,6 @@ function NewTaskModal({ t, workspaces, selectedPath, sessionId, onClose, onCreat
328
326
  api.models()
329
327
  .then((list) => { if (alive) setModels(list) })
330
328
  .catch(() => { if (alive) setModels([]) })
331
- api.settingsGet()
332
- .then((s) => {
333
- if (alive && s.defaultModel !== '') {
334
- setModel(s.defaultModel)
335
- }
336
- })
337
- .catch(() => undefined)
338
329
  return () => { alive = false }
339
330
  }, [])
340
331
 
@@ -564,14 +555,14 @@ function DetailPanel({ card, t, onClose, onChanged, onToast, onOpenSession }: De
564
555
  <div className="kb-plan-summary">{card.plan.summary}</div>
565
556
  <ol className="kb-plan-phases">
566
557
  {card.plan.phases.map((phase, i) => (
567
- <li key={phase.id} className={'kb-plan-phase' + (i === card.currentPhase && card.status === 'running' ? ' kb-plan-phase-current' : '')}>
568
- <div className="kb-plan-phase-head">
569
- <span className="kb-plan-phase-title">{phase.title}</span>
570
- <span className="kb-plan-phase-id">{phase.id}</span>
571
- </div>
572
- <div className="kb-plan-phase-goal">{phase.goal}</div>
573
- <PhaseSessions phaseIndex={i} card={card} t={t} onOpenSession={onOpenSession} />
574
- </li>
558
+ <PhaseCard
559
+ key={phase.id}
560
+ phase={phase}
561
+ phaseIndex={i}
562
+ card={card}
563
+ t={t}
564
+ onOpenSession={onOpenSession}
565
+ />
575
566
  ))}
576
567
  </ol>
577
568
  </div>
@@ -628,23 +619,68 @@ function DetailPanel({ card, t, onClose, onChanged, onToast, onOpenSession }: De
628
619
  )
629
620
  }
630
621
 
631
- function PhaseSessions({ phaseIndex, card, t, onOpenSession }: {
622
+ /** One plan phase rendered as a card with goal / sessions / conclusion sections. */
623
+ function PhaseCard({ phase, phaseIndex, card, t, onOpenSession }: {
624
+ phase: PlanPhase
632
625
  phaseIndex: number
633
626
  card: KanbanCard
634
- t: (key: string) => string
627
+ t: (key: string, params?: Record<string, unknown>) => string
635
628
  onOpenSession: (sessionId: string) => void
636
- }): JSX.Element | null {
629
+ }): JSX.Element {
637
630
  const attempts = card.sessions.phases.filter((a) => a.phaseIndex === phaseIndex)
638
- if (attempts.length === 0) return null
631
+ const summaries = attempts
632
+ .filter((a) => a.summary !== undefined && a.summary.trim() !== '')
633
+ .map((a) => a.summary as string)
634
+ const isCurrent = phaseIndex === card.currentPhase && card.status === 'running'
635
+ // A phase with at least one concluded attempt counts as completed.
636
+ const isCompleted = summaries.length > 0
637
+ const cls = 'kb-plan-phase'
638
+ + (isCurrent ? ' kb-plan-phase-current' : '')
639
+ + (isCompleted ? ' kb-plan-phase-completed' : '')
640
+ return (
641
+ <li className={cls}>
642
+ <div className="kb-plan-phase-head">
643
+ <span className="kb-plan-phase-title">{phase.title}</span>
644
+ <span className="kb-plan-phase-id">{phase.id}</span>
645
+ </div>
646
+ <div className="kb-plan-phase-body">
647
+ <div className="kb-plan-phase-section">
648
+ <div className="kb-plan-phase-section-title">{t('phaseGoal')}</div>
649
+ <div className="kb-plan-phase-goal">{phase.goal}</div>
650
+ </div>
651
+ {attempts.length > 0 && (
652
+ <div className="kb-plan-phase-section">
653
+ <div className="kb-plan-phase-section-title">{t('phaseSessions')}</div>
654
+ <PhaseSessions attempts={attempts} t={t} onOpenSession={onOpenSession} />
655
+ </div>
656
+ )}
657
+ {summaries.length > 0 && (
658
+ <div className="kb-plan-phase-section">
659
+ <div className="kb-plan-phase-section-title">{t('phaseConclusion')}</div>
660
+ <div className="kb-plan-phase-conclusion">
661
+ {summaries.map((s, idx) => (
662
+ <div key={idx} className="kb-plan-phase-conclusion-item">{s}</div>
663
+ ))}
664
+ </div>
665
+ </div>
666
+ )}
667
+ </div>
668
+ </li>
669
+ )
670
+ }
671
+
672
+ /** Open-session buttons for one phase's attempts (retries append more). */
673
+ function PhaseSessions({ attempts, t, onOpenSession }: {
674
+ attempts: PhaseAttempt[]
675
+ t: (key: string) => string
676
+ onOpenSession: (sessionId: string) => void
677
+ }): JSX.Element {
639
678
  return (
640
- <div className="kb-session-links">
679
+ <div className="kb-plan-phase-sessions">
641
680
  {attempts.map((a) => (
642
- <span key={a.sessionId} className="kb-session-attempt">
643
- <button type="button" className="kb-btn kb-btn-small" onClick={() => onOpenSession(a.sessionId)}>
644
- {t('openSession')} · {a.sessionId.slice(0, 13)}
645
- </button>
646
- {a.summary !== undefined && <span className="kb-session-summary">{a.summary}</span>}
647
- </span>
681
+ <button key={a.sessionId} type="button" className="kb-btn kb-btn-small" onClick={() => onOpenSession(a.sessionId)}>
682
+ {t('openSession')} · {a.sessionId.slice(0, 13)}
683
+ </button>
648
684
  ))}
649
685
  </div>
650
686
  )
@@ -2,9 +2,9 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
2
  import type {} from '@deepseek-ai/dsh-client-locale/client'
3
3
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
4
4
  import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
5
- import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
5
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
6
6
  import { BoardRoot } from './board'
7
- import { KanbanFooterButton, KanbanSettingsSection } from './sections'
7
+ import { KanbanFooterButton, KanbanSettingsCard } from './sections'
8
8
  import { LOCALE_NS, zh, en } from './locales'
9
9
  import { setClient, setBoardRoot, bindSessionNavigation } from './kanban-state'
10
10
 
@@ -35,13 +35,15 @@ export function apply(ctx: ClientContext): void {
35
35
  }, KanbanFooterButton as never),
36
36
  )
37
37
 
38
- ctx.slots.inject('settings.section', () =>
38
+ // The plugin's own Settings Card (设置 → 插件配置) rides the `task-kanban`
39
+ // settings namespace: registering into the keyed `settings.plugin.item` slot
40
+ // with the namespace string makes the configurable-plugins tab dispatch the
41
+ // card next to the built-in ones (bash / agent loop / web search).
42
+ ctx.slots.inject('settings.plugin.item', () =>
39
43
  ctx.slots.register({
40
- name: 'settings.section',
41
- id: 'task-kanban',
42
- order: 200,
43
- label: () => t('settingsTitle'),
44
+ name: 'settings.plugin.item',
45
+ key: 'task-kanban',
44
46
  locale: LOCALE_NS,
45
- }, KanbanSettingsSection as never),
47
+ }, KanbanSettingsCard as never),
46
48
  )
47
- }
49
+ }
@@ -38,6 +38,9 @@ export const zh = {
38
38
  plan: '实现方案',
39
39
  summary: '摘要',
40
40
  phases: '阶段',
41
+ phaseGoal: '目标',
42
+ phaseSessions: '会话',
43
+ phaseConclusion: '结论',
41
44
  error: '错误',
42
45
  requirementLabel: '需求文本',
43
46
  requirementPlaceholder: '描述你要实现的需求……',
@@ -48,9 +51,16 @@ export const zh = {
48
51
  modelPlaceholder: '(使用默认模型)',
49
52
  noWorkspace: '没有可用工作区,请先在侧边栏添加',
50
53
  refresh: '刷新',
51
- settingsTitle: '任务看板设置',
54
+ settingsTitle: '任务看板',
55
+ settingsDescription: '全局并行 worker 数与各会话类型的默认模型、推理强度、Agent 预设',
56
+ settingsExpand: '展开设置',
57
+ settingsCollapse: '收起设置',
52
58
  maxParallelWorkers: '全局并行 worker 数',
53
- defaultModel: '默认模型',
59
+ settingsRefineGroup: '细化需求会话默认',
60
+ settingsPhaseGroup: 'Phase 实现会话默认',
61
+ reasoningEffort: '推理强度',
62
+ agentPreset: 'Agent 预设',
63
+ defaultOption: '(默认)',
54
64
  saved: '已保存',
55
65
  loadError: '加载任务失败',
56
66
  noPlan: '该卡片还没有实现计划,请先点击"添加并细化"',
@@ -103,6 +113,9 @@ export const en = {
103
113
  plan: 'Plan',
104
114
  summary: 'Summary',
105
115
  phases: 'Phases',
116
+ phaseGoal: 'Goal',
117
+ phaseSessions: 'Sessions',
118
+ phaseConclusion: 'Conclusion',
106
119
  error: 'Error',
107
120
  requirementLabel: 'Requirement',
108
121
  requirementPlaceholder: 'Describe what you want to build…',
@@ -113,9 +126,16 @@ export const en = {
113
126
  modelPlaceholder: '(use default model)',
114
127
  noWorkspace: 'No workspaces available — add one in the sidebar first',
115
128
  refresh: 'Refresh',
116
- settingsTitle: 'Task Kanban Settings',
129
+ settingsTitle: 'Task Kanban',
130
+ settingsDescription: 'Global parallel workers and per-session-type defaults for model, reasoning effort, and agent preset',
131
+ settingsExpand: 'Show settings',
132
+ settingsCollapse: 'Hide settings',
117
133
  maxParallelWorkers: 'Global parallel workers',
118
- defaultModel: 'Default model',
134
+ settingsRefineGroup: 'Refinement session defaults',
135
+ settingsPhaseGroup: 'Phase implementation defaults',
136
+ reasoningEffort: 'Reasoning effort',
137
+ agentPreset: 'Agent preset',
138
+ defaultOption: '(default)',
119
139
  saved: 'Saved',
120
140
  loadError: 'Failed to load tasks',
121
141
  noPlan: 'This card has no plan yet — click "Add & Refine" first',