@yolk_vat-y/dsh-project-memory 0.4.4 → 0.5.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.
- package/CHANGELOG.md +46 -0
- package/README.md +11 -2
- package/README.zh-CN.md +11 -2
- package/client/client.js +892 -107
- package/client/client.js.map +1 -1
- package/package.json +2 -2
- package/src/auto-inject.js +220 -0
- package/src/client/MemoryView.tsx +349 -0
- package/src/client/TaskCommandNode.tsx +30 -11
- package/src/client/TaskComponents.tsx +171 -21
- package/src/client/TaskPanel.module.css +151 -1
- package/src/client/TaskPanel.tsx +74 -14
- package/src/client/client.ts +4 -4
- package/src/client/locales.ts +68 -0
- package/src/client/task-data-store.ts +21 -5
- package/src/commands/insight-actions.js +390 -0
- package/src/commands/task-actions.js +8 -0
- package/src/commands/tasks.js +1 -0
- package/src/global-seed.js +41 -0
- package/src/index.js +38 -4
- package/src/insight-store.js +558 -0
- package/src/project-profile.js +78 -0
- package/src/reflection-pipeline.js +146 -0
- package/src/similarity.js +52 -0
- package/src/store.js +73 -0
- package/src/tools/lesson-tools.js +101 -0
- package/src/tools/task-tools.js +12 -2
- package/src/types.js +56 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Task Panel Presentational 组件 — MiniBar(折叠迷你条)与 TaskCard(任务卡)。
|
|
3
3
|
* 纯渲染 + 卡片局部行内编辑/拖拽状态,动作经 props 回调交给容器(TaskPanel.tsx)。
|
|
4
4
|
*/
|
|
5
|
-
import { useRef, useState } from 'react'
|
|
5
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
6
6
|
import {
|
|
7
7
|
Button,
|
|
8
8
|
IconChevronDownOutline14,
|
|
@@ -13,9 +13,21 @@ import {
|
|
|
13
13
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
14
14
|
import { createTranslate } from './locales.ts'
|
|
15
15
|
import { useTaskDrag } from './task-hooks.ts'
|
|
16
|
-
import { type Task, type TaskStep } from './task-data-store.ts'
|
|
16
|
+
import { type Task, type TaskInsight, type TaskStep } from './task-data-store.ts'
|
|
17
17
|
import css from './TaskPanel.module.css'
|
|
18
18
|
|
|
19
|
+
function insightKindLabel(kind: string, t: ReturnType<typeof createTranslate>): string {
|
|
20
|
+
const map: Record<string, string> = {
|
|
21
|
+
lesson: t('mem.kind.lesson'),
|
|
22
|
+
decision: t('mem.kind.decision'),
|
|
23
|
+
procedure: t('mem.kind.procedure'),
|
|
24
|
+
experience: t('mem.kind.experience'),
|
|
25
|
+
}
|
|
26
|
+
return map[kind] ?? kind
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type InsightAction = 'confirm' | 'promote' | 'delete'
|
|
30
|
+
|
|
19
31
|
function timeAgo(iso?: string) {
|
|
20
32
|
if (!iso) return ''
|
|
21
33
|
const diff = Date.now() - new Date(iso).getTime()
|
|
@@ -33,6 +45,75 @@ function StepIcon({ status }: { status: TaskStep['status'] }) {
|
|
|
33
45
|
return <span className={css.stepPending} />
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 双击判定窗口(ms)。
|
|
50
|
+
* 单击(如卡片展开/收起)会延迟该时长执行:若期间收到第二次点击则按“双击”处理并取消单击,
|
|
51
|
+
* 从而避免“第一下先展开/收起、布局位移后第二下点不到目标”的问题。
|
|
52
|
+
* 数值是可感知与可靠性的折中:太小慢速双击会失效,太大单击会显“肉”。
|
|
53
|
+
*/
|
|
54
|
+
const CLICK_SINGLE_DELAY_MS = 250
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 行内自适应高度文本编辑器(textarea)。
|
|
58
|
+
* 编辑体验与单行输入一致(Enter 提交 / Shift+Enter 换行 / Esc 取消 / 失焦提交),
|
|
59
|
+
* 但高度随内容自动增长(按实际渲染行高测量),最多 maxRows 行后内部滚动,
|
|
60
|
+
* 解决长步骤/长任务名只能挤在一行里横向滚动编辑的问题。
|
|
61
|
+
*/
|
|
62
|
+
interface InlineEditorProps {
|
|
63
|
+
value: string
|
|
64
|
+
className?: string
|
|
65
|
+
maxRows?: number
|
|
66
|
+
onValueChange: (value: string) => void
|
|
67
|
+
onCommit: () => void
|
|
68
|
+
onCancel: () => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function InlineEditor({ value, className, maxRows = 8, onValueChange, onCommit, onCancel }: InlineEditorProps) {
|
|
72
|
+
const ref = useRef<HTMLTextAreaElement>(null)
|
|
73
|
+
|
|
74
|
+
// 在绘制前按真实内容测量并写死高度:内容 <= maxRows 行时刚好包住,超出后内部滚动。
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
const el = ref.current
|
|
77
|
+
if (!el) return
|
|
78
|
+
el.style.height = 'auto'
|
|
79
|
+
const cs = getComputedStyle(el)
|
|
80
|
+
const lineHeight = parseFloat(cs.lineHeight) || 20
|
|
81
|
+
const padV = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0)
|
|
82
|
+
const borderV = (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0)
|
|
83
|
+
const cap = maxRows * lineHeight + padV + borderV
|
|
84
|
+
const height = Math.min(el.scrollHeight, cap)
|
|
85
|
+
el.style.height = `${height}px`
|
|
86
|
+
el.style.overflowY = el.scrollHeight > cap ? 'auto' : 'hidden'
|
|
87
|
+
}, [value, maxRows])
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<textarea
|
|
91
|
+
ref={ref}
|
|
92
|
+
className={className}
|
|
93
|
+
rows={1}
|
|
94
|
+
value={value}
|
|
95
|
+
autoFocus
|
|
96
|
+
spellCheck={false}
|
|
97
|
+
onChange={(e) => onValueChange(e.target.value)}
|
|
98
|
+
onFocus={(e) => {
|
|
99
|
+
// 光标放到末尾,便于直接在原文后继续输入
|
|
100
|
+
const len = e.target.value.length
|
|
101
|
+
e.target.setSelectionRange(len, len)
|
|
102
|
+
}}
|
|
103
|
+
onBlur={onCommit}
|
|
104
|
+
onKeyDown={(e) => {
|
|
105
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
onCommit()
|
|
108
|
+
} else if (e.key === 'Escape') {
|
|
109
|
+
e.preventDefault()
|
|
110
|
+
onCancel()
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
36
117
|
interface MiniBarProps {
|
|
37
118
|
label: string
|
|
38
119
|
hint: string
|
|
@@ -104,6 +185,7 @@ interface TaskCardProps {
|
|
|
104
185
|
onEditStep: (index: number, value: string) => void
|
|
105
186
|
onCycleStatus: (index: number) => void
|
|
106
187
|
onReorderSteps: (fromIndex: number, toIndex: number) => void
|
|
188
|
+
onInsightAction?: (action: InsightAction, id: string) => void
|
|
107
189
|
showHints: boolean
|
|
108
190
|
syncing: boolean
|
|
109
191
|
t: ReturnType<typeof createTranslate>
|
|
@@ -121,6 +203,7 @@ export function TaskCard({
|
|
|
121
203
|
onEditStep,
|
|
122
204
|
onCycleStatus,
|
|
123
205
|
onReorderSteps,
|
|
206
|
+
onInsightAction,
|
|
124
207
|
showHints,
|
|
125
208
|
syncing,
|
|
126
209
|
t,
|
|
@@ -149,29 +232,66 @@ export function TaskCard({
|
|
|
149
232
|
onRename(value)
|
|
150
233
|
}
|
|
151
234
|
|
|
235
|
+
// —— 卡片头单击(展开/收起)与双击(编辑任务名)的时序判定 ——
|
|
236
|
+
// 单击动作延迟 CLICK_SINGLE_DELAY_MS 执行;若在窗口内收到第二次点击,取消单击、
|
|
237
|
+
// 命中任务名则直接进入编辑,避免“第一下已展开/收起、布局位移、第二下点不到任务名”。
|
|
238
|
+
const headClickTimer = useRef<number | null>(null)
|
|
239
|
+
// 编辑状态下,点卡片头任意处会先触发失焦提交;该次点击不应再当作“切换展开”的单击。
|
|
240
|
+
const suppressHeadClick = useRef(false)
|
|
241
|
+
useEffect(() => () => {
|
|
242
|
+
if (headClickTimer.current !== null) window.clearTimeout(headClickTimer.current)
|
|
243
|
+
}, [])
|
|
244
|
+
|
|
245
|
+
const handleHeadClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
246
|
+
if (suppressHeadClick.current) {
|
|
247
|
+
// 上一次 mousedown 落在标题编辑框内(失焦提交产生),吞掉这次点击,不切换展开
|
|
248
|
+
suppressHeadClick.current = false
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (titleEdit !== null) return
|
|
252
|
+
const target = e.target as HTMLElement
|
|
253
|
+
const onTitle = isBound && target.closest('[data-card-title]') !== null
|
|
254
|
+
if (headClickTimer.current !== null) {
|
|
255
|
+
// 第二次点击 → 双击:取消待执行的单击动作
|
|
256
|
+
window.clearTimeout(headClickTimer.current)
|
|
257
|
+
headClickTimer.current = null
|
|
258
|
+
if (onTitle) setTitleEdit({ value: String(task.title ?? '') })
|
|
259
|
+
else onToggleExpand()
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
headClickTimer.current = window.setTimeout(() => {
|
|
263
|
+
headClickTimer.current = null
|
|
264
|
+
onToggleExpand()
|
|
265
|
+
}, CLICK_SINGLE_DELAY_MS)
|
|
266
|
+
}
|
|
267
|
+
|
|
152
268
|
return (
|
|
153
269
|
<article data-task-id={task.id} className={`${css.card}${isBound ? ` ${css.cardBound}` : ''}`}>
|
|
154
270
|
<button
|
|
155
271
|
className={css.cardHead}
|
|
156
|
-
onClick={
|
|
272
|
+
onClick={handleHeadClick}
|
|
273
|
+
onMouseDownCapture={() => {
|
|
274
|
+
// 标题编辑中,点击卡片头任意处都会先失焦提交;标记该次点击以在 onClick 中吞掉,
|
|
275
|
+
// 避免“改名提交一下、卡片又收起/展开一下”的误切换。
|
|
276
|
+
if (titleEdit !== null) suppressHeadClick.current = true
|
|
277
|
+
}}
|
|
278
|
+
onDoubleClick={(e) => e.preventDefault()}
|
|
157
279
|
aria-expanded={expanded}
|
|
158
280
|
>
|
|
159
281
|
<div className={css.cardTitleRow}>
|
|
160
282
|
{isBound && <span className={css.boundBadge}>{t('task.current')}</span>}
|
|
161
283
|
{isBound && titleEdit !== null ? (
|
|
162
|
-
<
|
|
163
|
-
className={css.
|
|
284
|
+
<InlineEditor
|
|
285
|
+
className={css.editorArea}
|
|
164
286
|
value={titleEdit.value}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (e.key === 'Enter') commitTitleEdit()
|
|
170
|
-
else if (e.key === 'Escape') setTitleEdit(null)
|
|
171
|
-
}}
|
|
287
|
+
maxRows={4}
|
|
288
|
+
onValueChange={(value) => setTitleEdit({ value })}
|
|
289
|
+
onCommit={commitTitleEdit}
|
|
290
|
+
onCancel={() => setTitleEdit(null)}
|
|
172
291
|
/>
|
|
173
292
|
) : (
|
|
174
293
|
<span
|
|
294
|
+
data-card-title
|
|
175
295
|
className={`${css.cardTitle}${isBound ? ` ${css.cardTitleEditable}` : ''}`}
|
|
176
296
|
title={isBound ? t('task.title-edit') : undefined}
|
|
177
297
|
onDoubleClick={(e) => {
|
|
@@ -238,16 +358,13 @@ export function TaskCard({
|
|
|
238
358
|
<StepIcon status={step.status} />
|
|
239
359
|
)}
|
|
240
360
|
{stepIsEditing ? (
|
|
241
|
-
<
|
|
242
|
-
className={css.
|
|
361
|
+
<InlineEditor
|
|
362
|
+
className={css.editorArea}
|
|
243
363
|
value={stepEdit!.value}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (e.key === 'Enter') commitStepEdit(i)
|
|
249
|
-
else if (e.key === 'Escape') setStepEdit(null)
|
|
250
|
-
}}
|
|
364
|
+
maxRows={10}
|
|
365
|
+
onValueChange={(value) => setStepEdit({ index: i, value })}
|
|
366
|
+
onCommit={() => commitStepEdit(i)}
|
|
367
|
+
onCancel={() => setStepEdit(null)}
|
|
251
368
|
/>
|
|
252
369
|
) : (
|
|
253
370
|
<span
|
|
@@ -321,6 +438,39 @@ export function TaskCard({
|
|
|
321
438
|
</div>
|
|
322
439
|
)}
|
|
323
440
|
|
|
441
|
+
{onInsightAction && Array.isArray(task.insights) && task.insights.length > 0 && (
|
|
442
|
+
<div className={css.section}>
|
|
443
|
+
<div className={css.sectionLabel}>
|
|
444
|
+
{t('mem.section-label')}
|
|
445
|
+
<span className={css.sectionCount}>{task.insights.length}</span>
|
|
446
|
+
</div>
|
|
447
|
+
<ul className={css.memList}>
|
|
448
|
+
{(task.insights as TaskInsight[]).map((ins) => (
|
|
449
|
+
<li key={ins.id} className={css.memRow}>
|
|
450
|
+
<div className={css.memTitle}>
|
|
451
|
+
<span className={css.kindBadge}>{insightKindLabel(ins.kind, t)}</span>
|
|
452
|
+
<span>{ins.title || ins.id}</span>
|
|
453
|
+
{ins.draft && <span className={`${css.badge} ${css.badgeDraft}`}>{t('mem.draft')}</span>}
|
|
454
|
+
</div>
|
|
455
|
+
<div className={css.memActs}>
|
|
456
|
+
{ins.draft && (
|
|
457
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('confirm', ins.id)}>
|
|
458
|
+
{t('mem.confirm')}
|
|
459
|
+
</Button>
|
|
460
|
+
)}
|
|
461
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('promote', ins.id)}>
|
|
462
|
+
{t('mem.promote')}
|
|
463
|
+
</Button>
|
|
464
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('delete', ins.id)}>
|
|
465
|
+
{t('mem.delete')}
|
|
466
|
+
</Button>
|
|
467
|
+
</div>
|
|
468
|
+
</li>
|
|
469
|
+
))}
|
|
470
|
+
</ul>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
|
|
324
474
|
<div className={css.cardFooter}>
|
|
325
475
|
<Button variant="outline" size="sm" disabled={isBound || syncing} onClick={onSwitch}>
|
|
326
476
|
{t('task.switch')}
|
|
@@ -280,6 +280,7 @@
|
|
|
280
280
|
line-height: 20px;
|
|
281
281
|
color: var(--dsw-alias-label-secondary);
|
|
282
282
|
word-break: break-word;
|
|
283
|
+
white-space: pre-wrap;
|
|
283
284
|
}
|
|
284
285
|
.stepContentDone {
|
|
285
286
|
color: var(--dsw-alias-label-dimmed);
|
|
@@ -430,7 +431,7 @@
|
|
|
430
431
|
outline: 1px dashed var(--dsw-alias-border-l3, rgba(127,127,127,.4));
|
|
431
432
|
outline-offset: 1px;
|
|
432
433
|
}
|
|
433
|
-
/* 行内编辑框(步骤/标题共用,单行 input
|
|
434
|
+
/* 行内编辑框(步骤/标题共用,单行 input,仍被 MemoryView 表单复用) */
|
|
434
435
|
.stepInput {
|
|
435
436
|
flex: 1 1 auto;
|
|
436
437
|
min-width: 0;
|
|
@@ -446,6 +447,33 @@
|
|
|
446
447
|
padding: 0 5px;
|
|
447
448
|
}
|
|
448
449
|
|
|
450
|
+
/* 任务卡行内编辑器(textarea,按内容自适应增高;高度由 JS 测量写死,
|
|
451
|
+
内容超过 maxRows 行后内部滚动,避免长步骤/长任务名单行横向拖拽编辑) */
|
|
452
|
+
.editorArea {
|
|
453
|
+
flex: 1 1 0%;
|
|
454
|
+
min-width: 0;
|
|
455
|
+
box-sizing: border-box;
|
|
456
|
+
font-family: inherit;
|
|
457
|
+
font-size: 12px;
|
|
458
|
+
line-height: 20px;
|
|
459
|
+
color: var(--dsw-alias-label-primary);
|
|
460
|
+
background: var(--dsw-alias-bg-base, transparent);
|
|
461
|
+
border: 1px solid var(--dsw-alias-border-l3, rgba(127,127,127,.45));
|
|
462
|
+
border-radius: 5px;
|
|
463
|
+
padding: 2px 5px;
|
|
464
|
+
margin: 0;
|
|
465
|
+
resize: none;
|
|
466
|
+
overflow: hidden;
|
|
467
|
+
vertical-align: middle;
|
|
468
|
+
cursor: text;
|
|
469
|
+
white-space: pre-wrap;
|
|
470
|
+
word-break: break-word;
|
|
471
|
+
}
|
|
472
|
+
.editorArea:focus {
|
|
473
|
+
outline: none;
|
|
474
|
+
border-color: var(--dsw-alias-border-l3, rgba(127,127,127,.6));
|
|
475
|
+
}
|
|
476
|
+
|
|
449
477
|
/* —— 步骤拖拽重排:源行半透明 —— */
|
|
450
478
|
.stepDragging {
|
|
451
479
|
opacity: 0.3;
|
|
@@ -625,3 +653,125 @@
|
|
|
625
653
|
}
|
|
626
654
|
|
|
627
655
|
/* —— header 防重叠 —— */
|
|
656
|
+
|
|
657
|
+
/* ---- v0.5 Memory View(Task 草稿 / Project / Global 三作用域列表) ---- */
|
|
658
|
+
.memoryPane {
|
|
659
|
+
padding: 8px 10px 10px;
|
|
660
|
+
display: flex;
|
|
661
|
+
flex-direction: column;
|
|
662
|
+
gap: 6px;
|
|
663
|
+
overflow: auto;
|
|
664
|
+
max-height: 480px;
|
|
665
|
+
}
|
|
666
|
+
.memToolbar {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
justify-content: space-between;
|
|
670
|
+
gap: 8px;
|
|
671
|
+
}
|
|
672
|
+
.memCount {
|
|
673
|
+
font-size: 12px;
|
|
674
|
+
opacity: 0.75;
|
|
675
|
+
font-weight: 600;
|
|
676
|
+
}
|
|
677
|
+
.memList {
|
|
678
|
+
display: flex;
|
|
679
|
+
flex-direction: column;
|
|
680
|
+
gap: 6px;
|
|
681
|
+
}
|
|
682
|
+
.memRow {
|
|
683
|
+
border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
|
|
684
|
+
border-radius: 6px;
|
|
685
|
+
padding: 6px 8px;
|
|
686
|
+
display: flex;
|
|
687
|
+
flex-direction: column;
|
|
688
|
+
gap: 4px;
|
|
689
|
+
background: color-mix(in srgb, var(--dsw-bg-secondary, #f5f5f5) 45%, transparent);
|
|
690
|
+
}
|
|
691
|
+
.memRowArchived {
|
|
692
|
+
opacity: 0.55;
|
|
693
|
+
}
|
|
694
|
+
.memRowMain {
|
|
695
|
+
display: flex;
|
|
696
|
+
flex-direction: column;
|
|
697
|
+
gap: 2px;
|
|
698
|
+
}
|
|
699
|
+
.memTitle {
|
|
700
|
+
display: flex;
|
|
701
|
+
align-items: flex-start;
|
|
702
|
+
gap: 6px;
|
|
703
|
+
font-size: 13px;
|
|
704
|
+
line-height: 1.35;
|
|
705
|
+
word-break: break-word;
|
|
706
|
+
}
|
|
707
|
+
.kindBadge {
|
|
708
|
+
flex: none;
|
|
709
|
+
font-size: 11px;
|
|
710
|
+
border-radius: 4px;
|
|
711
|
+
padding: 0 5px;
|
|
712
|
+
line-height: 1.5;
|
|
713
|
+
background: var(--dsw-accent-soft, rgba(80, 120, 255, 0.15));
|
|
714
|
+
color: var(--dsw-accent, #4d6bfe);
|
|
715
|
+
}
|
|
716
|
+
.memMeta {
|
|
717
|
+
display: flex;
|
|
718
|
+
flex-wrap: wrap;
|
|
719
|
+
gap: 4px;
|
|
720
|
+
align-items: center;
|
|
721
|
+
}
|
|
722
|
+
.badge {
|
|
723
|
+
font-size: 11px;
|
|
724
|
+
border-radius: 4px;
|
|
725
|
+
padding: 0 5px;
|
|
726
|
+
line-height: 1.5;
|
|
727
|
+
background: color-mix(in srgb, var(--dsw-fg, #222) 8%, transparent);
|
|
728
|
+
}
|
|
729
|
+
.badgeDraft {
|
|
730
|
+
color: #b26a00;
|
|
731
|
+
background: rgba(255, 180, 0, 0.16);
|
|
732
|
+
}
|
|
733
|
+
.badgeArchived {
|
|
734
|
+
opacity: 0.8;
|
|
735
|
+
}
|
|
736
|
+
.memBody {
|
|
737
|
+
font-size: 12px;
|
|
738
|
+
opacity: 0.8;
|
|
739
|
+
word-break: break-word;
|
|
740
|
+
}
|
|
741
|
+
.memActs {
|
|
742
|
+
display: flex;
|
|
743
|
+
flex-wrap: wrap;
|
|
744
|
+
gap: 4px;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* ---- v0.5 Memory View 新建表单 ---- */
|
|
748
|
+
.memToolbarActs {
|
|
749
|
+
display: flex;
|
|
750
|
+
align-items: center;
|
|
751
|
+
gap: 6px;
|
|
752
|
+
}
|
|
753
|
+
.memNew {
|
|
754
|
+
display: flex;
|
|
755
|
+
flex-direction: column;
|
|
756
|
+
gap: 6px;
|
|
757
|
+
border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
|
|
758
|
+
border-radius: 6px;
|
|
759
|
+
padding: 8px;
|
|
760
|
+
}
|
|
761
|
+
.memNewRow {
|
|
762
|
+
display: flex;
|
|
763
|
+
gap: 8px;
|
|
764
|
+
align-items: center;
|
|
765
|
+
}
|
|
766
|
+
.memLabel {
|
|
767
|
+
display: inline-flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
gap: 6px;
|
|
770
|
+
font-size: 12px;
|
|
771
|
+
opacity: 0.8;
|
|
772
|
+
}
|
|
773
|
+
.memNew select,
|
|
774
|
+
.memNew textarea {
|
|
775
|
+
width: 100%;
|
|
776
|
+
box-sizing: border-box;
|
|
777
|
+
}
|
package/src/client/TaskPanel.tsx
CHANGED
|
@@ -12,17 +12,20 @@ import {
|
|
|
12
12
|
IconQuestionOutline14,
|
|
13
13
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
14
14
|
import { createTranslate, zh, en } from './locales.ts'
|
|
15
|
-
import { useTaskData, useTaskDataActions, parseTaskPayloadText, type TaskStep } from './task-data-store.ts'
|
|
15
|
+
import { useTaskData, useTaskDataActions, taskDataStore, parseTaskPayloadText, type TaskStep } from './task-data-store.ts'
|
|
16
16
|
import { useTaskUI, useTaskUIActions } from './task-ui-store.ts'
|
|
17
17
|
import { MiniBar, TaskCard } from './TaskComponents.tsx'
|
|
18
|
+
import { MemoryView, type InsightScope } from './MemoryView.tsx'
|
|
18
19
|
import css from './TaskPanel.module.css'
|
|
19
20
|
|
|
20
21
|
const NS = 'dsh-project-memory'
|
|
21
22
|
|
|
22
23
|
const STATUS_CYCLE = ['pending', 'in_progress', 'completed'] as const
|
|
24
|
+
const VIEW_CYCLE = ['task', 'project', 'global'] as const
|
|
25
|
+
type PanelView = (typeof VIEW_CYCLE)[number]
|
|
23
26
|
|
|
24
|
-
function getT() {
|
|
25
|
-
const locale = (
|
|
27
|
+
function getT(ctx: any) {
|
|
28
|
+
const locale = ctx?.locale?.getSnapshot?.()?.active === 'zh' ? zh : en
|
|
26
29
|
return createTranslate(locale)
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -65,7 +68,7 @@ export function TaskPanelEntry({ ctx }: { ctx: any }) {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function TaskPanelView({ ctx }: { ctx: any }) {
|
|
68
|
-
const t = getT()
|
|
71
|
+
const t = getT(ctx)
|
|
69
72
|
const data = useTaskData()
|
|
70
73
|
const ui = useTaskUI()
|
|
71
74
|
const sessionId = useSessionId(ctx)
|
|
@@ -73,12 +76,22 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
73
76
|
const [syncedAt, setSyncedAt] = useState(0)
|
|
74
77
|
const [syncError, setSyncError] = useState<string | null>(null)
|
|
75
78
|
const [showHints, setShowHints] = useState(true)
|
|
79
|
+
const [view, setView] = useState<PanelView>('task')
|
|
80
|
+
const cycleView = () => {
|
|
81
|
+
const i = VIEW_CYCLE.indexOf(view)
|
|
82
|
+
setView(VIEW_CYCLE[(i + 1) % VIEW_CYCLE.length])
|
|
83
|
+
}
|
|
84
|
+
const viewTitle =
|
|
85
|
+
view === 'task' ? t('panel.title')
|
|
86
|
+
: view === 'global' ? t('view.global')
|
|
87
|
+
: t('view.project')
|
|
76
88
|
|
|
77
89
|
const dataActions = useTaskDataActions()
|
|
78
90
|
const uiActions = useTaskUIActions()
|
|
79
91
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
92
|
+
const tasks = Array.isArray(data.tasks) ? data.tasks : []
|
|
93
|
+
const activeTasks = tasks.filter((task) => !task.archived)
|
|
94
|
+
const boundTask = data.boundTaskId ? tasks.find((task) => task.id === data.boundTaskId) ?? null : null
|
|
82
95
|
|
|
83
96
|
const applyPayload = (text?: string): boolean => {
|
|
84
97
|
const parsed = parseTaskPayloadText(text)
|
|
@@ -118,7 +131,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
118
131
|
try {
|
|
119
132
|
const prevBoundId = data.boundTaskId
|
|
120
133
|
await runLine('/tasks')
|
|
121
|
-
const newBoundId =
|
|
134
|
+
const newBoundId = taskDataStore.getSnapshot().boundTaskId
|
|
122
135
|
if (newBoundId && newBoundId !== prevBoundId) {
|
|
123
136
|
await runLine(`/task switch ${newBoundId}`)
|
|
124
137
|
}
|
|
@@ -137,6 +150,18 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
// v0.5:任务卡内联记忆动作(confirm/promote/delete 于 /insight task 层),随后刷新任务数据
|
|
154
|
+
const handleInsightTask = async (action: 'confirm' | 'promote' | 'delete', id: string): Promise<void> => {
|
|
155
|
+
if (syncing) return
|
|
156
|
+
setSyncing(true)
|
|
157
|
+
try {
|
|
158
|
+
await runLine(`/insight ${action} task ${id}`)
|
|
159
|
+
await refresh()
|
|
160
|
+
} finally {
|
|
161
|
+
setSyncing(false)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
140
165
|
const pushSteps = (taskId: string, steps: TaskStep[]): void => {
|
|
141
166
|
void runLine(`/task todos ${JSON.stringify(steps.map((s) => ({ content: s.content, status: s.status })))}`)
|
|
142
167
|
}
|
|
@@ -297,14 +322,46 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
297
322
|
>
|
|
298
323
|
<IconFolderOpenOutline16 className={css.headerIcon} />
|
|
299
324
|
</Button>
|
|
300
|
-
<h2 className={css.headerTitle}>{
|
|
325
|
+
<h2 className={css.headerTitle}>{viewTitle}</h2>
|
|
301
326
|
</div>
|
|
302
327
|
<div className={css.headerRight}>
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
328
|
+
<Button
|
|
329
|
+
variant="ghost"
|
|
330
|
+
size="sm"
|
|
331
|
+
onClick={cycleView}
|
|
332
|
+
aria-label={t('view.cycle')}
|
|
333
|
+
title={`${t('view.cycle')}(${view} → ${VIEW_CYCLE[(VIEW_CYCLE.indexOf(view) + 1) % VIEW_CYCLE.length]})`}
|
|
334
|
+
>
|
|
335
|
+
{/* lucide notebook-tabs(视图循环图标) */}
|
|
336
|
+
<svg
|
|
337
|
+
width="16"
|
|
338
|
+
height="16"
|
|
339
|
+
viewBox="0 0 24 24"
|
|
340
|
+
fill="none"
|
|
341
|
+
stroke="currentColor"
|
|
342
|
+
strokeWidth="2"
|
|
343
|
+
strokeLinecap="round"
|
|
344
|
+
strokeLinejoin="round"
|
|
345
|
+
aria-hidden="true"
|
|
346
|
+
>
|
|
347
|
+
<path d="M2 6h4" />
|
|
348
|
+
<path d="M2 10h4" />
|
|
349
|
+
<path d="M2 14h4" />
|
|
350
|
+
<path d="M2 18h4" />
|
|
351
|
+
<rect width="16" height="20" x="4" y="2" rx="2" />
|
|
352
|
+
<path d="M15 2v20" />
|
|
353
|
+
<path d="M15 7h5" />
|
|
354
|
+
<path d="M15 12h5" />
|
|
355
|
+
<path d="M15 17h5" />
|
|
356
|
+
</svg>
|
|
357
|
+
</Button>
|
|
358
|
+
{view === 'task' && (
|
|
359
|
+
<span className={css.counts}>
|
|
360
|
+
{activeTasks.length > 0
|
|
361
|
+
? `${t('panel.active')} ${activeTasks.length} · ${doneCount}/${activeTasks.length}`
|
|
362
|
+
: ''}
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
308
365
|
<Button
|
|
309
366
|
variant="ghost"
|
|
310
367
|
size="sm"
|
|
@@ -338,7 +395,9 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
338
395
|
{!sessionId && <div className={css.notice}>{t('panel.no-session')}</div>}
|
|
339
396
|
{syncError && <div className={css.notice}>{t('panel.sync-failed')}: {syncError}</div>}
|
|
340
397
|
|
|
341
|
-
{
|
|
398
|
+
{view !== 'task' ? (
|
|
399
|
+
<MemoryView ctx={ctx} sessionId={sessionId} scope={view as InsightScope} boundTaskId={data.boundTaskId} t={t} />
|
|
400
|
+
) : activeTasks.length === 0 ? (
|
|
342
401
|
<div className={css.emptyState}>
|
|
343
402
|
<p className={css.emptyTitle}>{t('panel.empty')}</p>
|
|
344
403
|
<p className={css.emptyDesc}>{t('panel.empty-desc')}</p>
|
|
@@ -364,6 +423,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
364
423
|
onEditStep={(index, value) => commitStepText(task.id, index, value)}
|
|
365
424
|
onCycleStatus={(index) => cycleStatus(task.id, index)}
|
|
366
425
|
onReorderSteps={(from, to) => reorderSteps(task.id, from, to)}
|
|
426
|
+
onInsightAction={(action, id) => void handleInsightTask(action, id)}
|
|
367
427
|
showHints={showHints}
|
|
368
428
|
syncing={syncing}
|
|
369
429
|
t={t}
|
package/src/client/client.ts
CHANGED
|
@@ -20,7 +20,7 @@ const NS = 'dsh-project-memory'
|
|
|
20
20
|
export const name = NS
|
|
21
21
|
|
|
22
22
|
/** Required client services: slots registry, session scopes, commands remote (data 通道). */
|
|
23
|
-
export const inject = ['slots', 'sessions', 'remote', 'remote.commands']
|
|
23
|
+
export const inject = ['slots', 'sessions', 'remote', 'remote.commands', 'locale']
|
|
24
24
|
|
|
25
25
|
export function apply(ctx: any): void {
|
|
26
26
|
const slots = ctx?.slots
|
|
@@ -38,9 +38,9 @@ export function apply(ctx: any): void {
|
|
|
38
38
|
() => createElement(TaskPanelEntry, { ctx })
|
|
39
39
|
))
|
|
40
40
|
|
|
41
|
-
// 会话内 /tasks、/task 命令节点:按命令名 key 接管渲染,
|
|
42
|
-
// 替换内置大段文本卡片(文本含 JSON
|
|
43
|
-
const nodeKeys = ['tasks', 'task']
|
|
41
|
+
// 会话内 /tasks、/task、/insight 命令节点:按命令名 key 接管渲染,
|
|
42
|
+
// 替换内置大段文本卡片(文本含 JSON 载荷,只有面板需要它),避免载荷刷屏对话。
|
|
43
|
+
const nodeKeys = ['tasks', 'task', 'insight']
|
|
44
44
|
for (const key of nodeKeys) {
|
|
45
45
|
slots.inject('conversation.chat.commandview', () => slots.register(
|
|
46
46
|
{ name: 'conversation.chat.commandview', key },
|