@yolk_vat-y/dsh-project-memory 0.4.3 → 0.5.0
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 +59 -0
- package/README.md +43 -5
- package/README.zh-CN.md +44 -7
- package/client/client.js +1439 -464
- package/client/client.js.map +1 -1
- package/package.json +2 -2
- package/src/auto-inject.js +214 -0
- package/src/client/MemoryView.tsx +349 -0
- package/src/client/TaskCommandNode.tsx +41 -30
- package/src/client/TaskComponents.tsx +388 -0
- package/src/client/TaskPanel.module.css +225 -40
- package/src/client/TaskPanel.tsx +174 -391
- package/src/client/client.ts +4 -4
- package/src/client/locales.ts +74 -0
- package/src/client/task-data-store.ts +152 -0
- package/src/client/task-hooks.ts +169 -0
- package/src/client/task-ui-store.ts +118 -0
- package/src/commands/insight-actions.js +390 -0
- package/src/commands/task-actions.js +17 -7
- package/src/commands/tasks.js +6 -3
- package/src/global-seed.js +41 -0
- package/src/index.js +42 -7
- 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 +31 -2
- package/src/types.js +56 -0
- package/src/client/task-store.ts +0 -192
package/src/client/TaskPanel.tsx
CHANGED
|
@@ -1,53 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task Panel — dsh web `shell.overlay` 浮动任务面板。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - 默认收成右侧浮标(不影响用户操作/不挡对话);
|
|
6
|
-
* - 展开时通过宿主 `remote.commands.execute(sessionId, '/tasks')` 拉取最新快照
|
|
7
|
-
* (命令只读,走 /tasks 同一数据源 = tasks.json,因此与模型维护的 todo 双向一致);
|
|
8
|
-
* - 卡片内「切换 / 归档」执行 `/task switch|archive <id>`,服务端返回新快照 JSON,
|
|
9
|
-
* 面板即时刷新,无需再跑一次 /tasks;
|
|
10
|
-
* - 可拖拽(仅头部把手),位置持久化 localStorage。
|
|
11
|
-
*
|
|
12
|
-
* 依赖仅 react + @deepseek-ai/dsh-client-ui-primitives(宿主 seed 模块)。
|
|
3
|
+
* Container 组件:负责数据获取、命令桥接、状态协调
|
|
4
|
+
* Presentational 组件在 TaskComponents.tsx
|
|
13
5
|
*/
|
|
14
6
|
import { Component, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
15
7
|
import {
|
|
16
8
|
Button,
|
|
17
9
|
IconChevronDownOutline14,
|
|
18
|
-
IconChevronUpOutline14,
|
|
19
10
|
IconCloseOutline16,
|
|
20
|
-
IconRefreshOutline16,
|
|
21
|
-
IconCheckOutline16,
|
|
22
|
-
IconPlayOutline16,
|
|
23
11
|
IconFolderOpenOutline16,
|
|
12
|
+
IconQuestionOutline14,
|
|
24
13
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
25
14
|
import { createTranslate, zh, en } from './locales.ts'
|
|
26
|
-
import {
|
|
15
|
+
import { useTaskData, useTaskDataActions, taskDataStore, parseTaskPayloadText, type TaskStep } from './task-data-store.ts'
|
|
16
|
+
import { useTaskUI, useTaskUIActions } from './task-ui-store.ts'
|
|
17
|
+
import { MiniBar, TaskCard } from './TaskComponents.tsx'
|
|
18
|
+
import { MemoryView, type InsightScope } from './MemoryView.tsx'
|
|
27
19
|
import css from './TaskPanel.module.css'
|
|
28
20
|
|
|
29
21
|
const NS = 'dsh-project-memory'
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
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]
|
|
35
26
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const m = Math.floor(diff / 60000)
|
|
40
|
-
if (m < 1) return '刚刚'
|
|
41
|
-
if (m < 60) return `${m}分钟前`
|
|
42
|
-
const h = Math.floor(m / 60)
|
|
43
|
-
if (h < 24) return `${h}小时前`
|
|
44
|
-
return `${Math.floor(h / 24)}天前`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function StepIcon({ status }: { status: TaskStep['status'] }) {
|
|
48
|
-
if (status === 'completed') return <IconCheckOutline16 className={css.stepDone} />
|
|
49
|
-
if (status === 'in_progress') return <IconPlayOutline16 className={css.stepRun} />
|
|
50
|
-
return <span className={css.stepPending} />
|
|
27
|
+
function getT(ctx: any) {
|
|
28
|
+
const locale = ctx?.locale?.getSnapshot?.()?.active === 'zh' ? zh : en
|
|
29
|
+
return createTranslate(locale)
|
|
51
30
|
}
|
|
52
31
|
|
|
53
32
|
function useSessionId(ctx: any): string | null {
|
|
@@ -64,139 +43,16 @@ function useSessionId(ctx: any): string | null {
|
|
|
64
43
|
return first?.sessionId ?? null
|
|
65
44
|
}
|
|
66
45
|
|
|
67
|
-
/** 行内编辑框:自动按内容增高(最多 160px),Enter 提交、Shift+Enter 换行、Esc 取消、失焦提交。 */
|
|
68
|
-
function AutoEdit({ value, onCommit, onCancel, singleLine }: {
|
|
69
|
-
value: string
|
|
70
|
-
onCommit: (value: string) => void
|
|
71
|
-
onCancel: () => void
|
|
72
|
-
singleLine?: boolean
|
|
73
|
-
}) {
|
|
74
|
-
const [text, setText] = useState(value)
|
|
75
|
-
const ref = useRef<HTMLTextAreaElement>(null)
|
|
76
|
-
const settled = useRef(false)
|
|
77
|
-
const once = (fn: () => void) => {
|
|
78
|
-
if (settled.current) return
|
|
79
|
-
settled.current = true
|
|
80
|
-
fn()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const adjust = () => {
|
|
84
|
-
const el = ref.current
|
|
85
|
-
if (!el) return
|
|
86
|
-
el.style.height = 'auto'
|
|
87
|
-
el.style.height = `${Math.min(el.scrollHeight, 160)}px`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
adjust()
|
|
92
|
-
const el = ref.current
|
|
93
|
-
el?.focus()
|
|
94
|
-
el?.select()
|
|
95
|
-
}, [])
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<textarea
|
|
99
|
-
ref={ref}
|
|
100
|
-
className={css.stepInput}
|
|
101
|
-
rows={1}
|
|
102
|
-
value={text}
|
|
103
|
-
onChange={(e) => { setText(e.target.value); adjust() }}
|
|
104
|
-
onBlur={() => once(() => onCommit(text))}
|
|
105
|
-
onKeyDown={(e) => {
|
|
106
|
-
if (e.key === 'Escape') {
|
|
107
|
-
e.preventDefault()
|
|
108
|
-
once(() => onCancel())
|
|
109
|
-
} else if (e.key === 'Enter' && (singleLine || !e.shiftKey)) {
|
|
110
|
-
e.preventDefault()
|
|
111
|
-
once(() => onCommit(text))
|
|
112
|
-
}
|
|
113
|
-
}}
|
|
114
|
-
/>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** 折叠迷你条:可拖拽(按住拖动,轻点展开)。 */
|
|
119
|
-
function MiniBar({ label, hint, position, theme, onMove, open }: {
|
|
120
|
-
label: string
|
|
121
|
-
hint: string
|
|
122
|
-
position: { x: number; y: number }
|
|
123
|
-
theme: string
|
|
124
|
-
onMove: (pos: { x: number; y: number }) => void
|
|
125
|
-
open: () => void
|
|
126
|
-
}) {
|
|
127
|
-
const barRef = useRef<HTMLDivElement>(null)
|
|
128
|
-
const down = useRef<{ dx: number; dy: number; sx: number; sy: number; moved: boolean } | null>(null)
|
|
129
|
-
|
|
130
|
-
const onMouseDown = (e: React.MouseEvent) => {
|
|
131
|
-
if (e.button !== 0) return
|
|
132
|
-
const rect = barRef.current?.getBoundingClientRect()
|
|
133
|
-
if (!rect) return
|
|
134
|
-
down.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, sx: e.clientX, sy: e.clientY, moved: false }
|
|
135
|
-
const onMoveEv = (ev: MouseEvent) => {
|
|
136
|
-
const d = down.current
|
|
137
|
-
if (!d) return
|
|
138
|
-
if (!d.moved && Math.abs(ev.clientX - d.sx) + Math.abs(ev.clientY - d.sy) > 4) d.moved = true
|
|
139
|
-
if (d.moved) {
|
|
140
|
-
const w = barRef.current?.offsetWidth ?? 260
|
|
141
|
-
const h = barRef.current?.offsetHeight ?? 34
|
|
142
|
-
const x = Math.max(8, Math.min(window.innerWidth - w - 8, ev.clientX - d.dx))
|
|
143
|
-
const y = Math.max(8, Math.min(window.innerHeight - h - 8, ev.clientY - d.dy))
|
|
144
|
-
onMove({ x, y })
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const onUpEv = () => {
|
|
148
|
-
const d = down.current
|
|
149
|
-
down.current = null
|
|
150
|
-
window.removeEventListener('mousemove', onMoveEv)
|
|
151
|
-
window.removeEventListener('mouseup', onUpEv)
|
|
152
|
-
if (d && !d.moved) open()
|
|
153
|
-
}
|
|
154
|
-
window.addEventListener('mousemove', onMoveEv)
|
|
155
|
-
window.addEventListener('mouseup', onUpEv)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<div
|
|
160
|
-
ref={barRef}
|
|
161
|
-
className={css.miniBar}
|
|
162
|
-
style={{ left: position.x, top: position.y }}
|
|
163
|
-
data-theme={theme === 'native' ? undefined : theme}
|
|
164
|
-
onMouseDown={onMouseDown}
|
|
165
|
-
role="button"
|
|
166
|
-
title={hint}
|
|
167
|
-
aria-label={hint}
|
|
168
|
-
>
|
|
169
|
-
<IconFolderOpenOutline16 className={css.miniIcon} />
|
|
170
|
-
<span className={css.miniText}>{label}</span>
|
|
171
|
-
<IconChevronUpOutline14 className={css.miniChevron} />
|
|
172
|
-
</div>
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 错误边界:任务面板渲染一旦抛错,不再让 shell.overlay 把整条条目退休
|
|
178
|
-
* (宿主对崩溃条目会永久移除直到重载页面)。捕获后显示一个纯文本兜底,
|
|
179
|
-
* 点击重新渲染。
|
|
180
|
-
*/
|
|
181
46
|
class PanelErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
|
182
47
|
state = { failed: false }
|
|
183
|
-
|
|
184
|
-
static getDerivedStateFromError() {
|
|
185
|
-
return { failed: true }
|
|
186
|
-
}
|
|
187
|
-
|
|
48
|
+
static getDerivedStateFromError() { return { failed: true } }
|
|
188
49
|
componentDidCatch(error: unknown) {
|
|
189
|
-
console.warn('[dsh-project-memory] task panel render crashed
|
|
50
|
+
console.warn('[dsh-project-memory] task panel render crashed:', error)
|
|
190
51
|
}
|
|
191
|
-
|
|
192
52
|
render() {
|
|
193
53
|
if (!this.state.failed) return this.props.children
|
|
194
54
|
return (
|
|
195
|
-
<button
|
|
196
|
-
className={css.boundaryFallback}
|
|
197
|
-
onClick={() => this.setState({ failed: false })}
|
|
198
|
-
title="重新渲染任务面板"
|
|
199
|
-
>
|
|
55
|
+
<button className={css.boundaryFallback} onClick={() => this.setState({ failed: false })} title="重新渲染任务面板">
|
|
200
56
|
任务面板(点击重试)
|
|
201
57
|
</button>
|
|
202
58
|
)
|
|
@@ -212,26 +68,35 @@ export function TaskPanelEntry({ ctx }: { ctx: any }) {
|
|
|
212
68
|
}
|
|
213
69
|
|
|
214
70
|
function TaskPanelView({ ctx }: { ctx: any }) {
|
|
215
|
-
const t = getT()
|
|
216
|
-
const
|
|
71
|
+
const t = getT(ctx)
|
|
72
|
+
const data = useTaskData()
|
|
73
|
+
const ui = useTaskUI()
|
|
217
74
|
const sessionId = useSessionId(ctx)
|
|
218
75
|
const [syncing, setSyncing] = useState(false)
|
|
219
76
|
const [syncedAt, setSyncedAt] = useState(0)
|
|
220
|
-
const [copiedPath, setCopiedPath] = useState<string | null>(null)
|
|
221
77
|
const [syncError, setSyncError] = useState<string | null>(null)
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const
|
|
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')
|
|
225
88
|
|
|
226
|
-
const
|
|
89
|
+
const dataActions = useTaskDataActions()
|
|
90
|
+
const uiActions = useTaskUIActions()
|
|
227
91
|
|
|
228
|
-
const
|
|
229
|
-
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
|
|
230
95
|
|
|
231
96
|
const applyPayload = (text?: string): boolean => {
|
|
232
97
|
const parsed = parseTaskPayloadText(text)
|
|
233
98
|
if (!parsed) return false
|
|
234
|
-
|
|
99
|
+
dataActions.setTasks({ tasks: parsed.tasks, boundTaskId: parsed.boundId, archivedCount: parsed.archived })
|
|
235
100
|
setSyncedAt(Date.now())
|
|
236
101
|
setSyncError(null)
|
|
237
102
|
return true
|
|
@@ -264,7 +129,12 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
264
129
|
if (syncing) return
|
|
265
130
|
setSyncing(true)
|
|
266
131
|
try {
|
|
132
|
+
const prevBoundId = data.boundTaskId
|
|
267
133
|
await runLine('/tasks')
|
|
134
|
+
const newBoundId = taskDataStore.getSnapshot().boundTaskId
|
|
135
|
+
if (newBoundId && newBoundId !== prevBoundId) {
|
|
136
|
+
await runLine(`/task switch ${newBoundId}`)
|
|
137
|
+
}
|
|
268
138
|
} finally {
|
|
269
139
|
setSyncing(false)
|
|
270
140
|
}
|
|
@@ -280,14 +150,24 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
280
150
|
}
|
|
281
151
|
}
|
|
282
152
|
|
|
283
|
-
//
|
|
284
|
-
const
|
|
285
|
-
|
|
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
|
+
|
|
286
165
|
const pushSteps = (taskId: string, steps: TaskStep[]): void => {
|
|
287
166
|
void runLine(`/task todos ${JSON.stringify(steps.map((s) => ({ content: s.content, status: s.status })))}`)
|
|
288
167
|
}
|
|
168
|
+
|
|
289
169
|
const cycleStatus = (taskId: string, index: number) => {
|
|
290
|
-
const task =
|
|
170
|
+
const task = data.tasks.find((tt) => tt.id === taskId)
|
|
291
171
|
if (!task) return
|
|
292
172
|
const steps = task.steps || []
|
|
293
173
|
const next = steps.map((s, i) => {
|
|
@@ -297,45 +177,55 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
297
177
|
})
|
|
298
178
|
pushSteps(taskId, next)
|
|
299
179
|
}
|
|
180
|
+
|
|
300
181
|
const commitStepText = (taskId: string, index: number, value: string) => {
|
|
301
|
-
const task =
|
|
182
|
+
const task = data.tasks.find((tt) => tt.id === taskId)
|
|
302
183
|
const trimmed = value.trim()
|
|
303
|
-
if (!task)
|
|
184
|
+
if (!task) return
|
|
304
185
|
const same = (task.steps || [])[index]?.content === trimmed
|
|
305
|
-
setEditing(null)
|
|
306
186
|
if (same || !trimmed) return
|
|
307
187
|
const next = (task.steps || []).map((s, i) => (i === index ? { ...s, content: trimmed } : s))
|
|
308
188
|
pushSteps(taskId, next)
|
|
309
189
|
}
|
|
310
190
|
|
|
311
|
-
|
|
312
|
-
|
|
191
|
+
const reorderSteps = (taskId: string, fromIndex: number, toIndex: number) => {
|
|
192
|
+
if (fromIndex === toIndex) return
|
|
193
|
+
const task = data.tasks.find((tt) => tt.id === taskId)
|
|
194
|
+
if (!task) return
|
|
195
|
+
const steps = [...(task.steps || [])]
|
|
196
|
+
const [moved] = steps.splice(fromIndex, 1)
|
|
197
|
+
const targetIndex = toIndex > fromIndex ? toIndex - 1 : toIndex
|
|
198
|
+
steps.splice(targetIndex, 0, moved)
|
|
199
|
+
pushSteps(taskId, steps)
|
|
200
|
+
}
|
|
201
|
+
|
|
313
202
|
const commitTitle = (taskId: string, value: string) => {
|
|
314
|
-
const task =
|
|
203
|
+
const task = data.tasks.find((tt) => tt.id === taskId)
|
|
315
204
|
const trimmed = value.trim()
|
|
316
|
-
if (!task)
|
|
205
|
+
if (!task) return
|
|
317
206
|
const same = task.title === trimmed
|
|
318
|
-
setEditingTitle(null)
|
|
319
207
|
if (same || !trimmed) return
|
|
320
208
|
void runLine(`/task rename ${taskId} ${JSON.stringify(trimmed)}`)
|
|
321
209
|
}
|
|
322
210
|
|
|
323
211
|
const expand = () => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (Date.now() - Math.max(state.lastUpdate, syncedAt) > 30_000) {
|
|
212
|
+
uiActions.open()
|
|
213
|
+
if (Date.now() - Math.max(data.lastUpdate, syncedAt) > 30_000) {
|
|
327
214
|
void refresh()
|
|
328
215
|
}
|
|
329
216
|
}
|
|
330
217
|
|
|
331
218
|
const THEMES = ['native', 'glass', 'brutal', 'mono'] as const
|
|
332
|
-
const style = (THEMES as readonly string[]).includes(
|
|
219
|
+
const style = (THEMES as readonly string[]).includes(ui.theme) ? ui.theme : 'native'
|
|
333
220
|
const styleLabel = t(`style.${style}` as keyof typeof zh)
|
|
334
221
|
const cycleTheme = () => {
|
|
335
222
|
const i = THEMES.indexOf(style as (typeof THEMES)[number])
|
|
336
|
-
|
|
223
|
+
uiActions.setTheme(THEMES[(i + 1) % THEMES.length])
|
|
337
224
|
}
|
|
338
225
|
|
|
226
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
227
|
+
const drag = useRef<{ dx: number; dy: number } | null>(null)
|
|
228
|
+
|
|
339
229
|
const handleDragStart = (e: React.MouseEvent) => {
|
|
340
230
|
if (e.target !== e.currentTarget) return
|
|
341
231
|
const rect = panelRef.current?.getBoundingClientRect()
|
|
@@ -347,7 +237,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
347
237
|
const height = panelRef.current?.offsetHeight ?? 480
|
|
348
238
|
const x = Math.max(8, Math.min(window.innerWidth - width - 8, ev.clientX - drag.current.dx))
|
|
349
239
|
const y = Math.max(8, Math.min(window.innerHeight - 64, ev.clientY - drag.current.dy))
|
|
350
|
-
|
|
240
|
+
uiActions.setPanelPosition({ x, y })
|
|
351
241
|
}
|
|
352
242
|
const onUp = () => {
|
|
353
243
|
drag.current = null
|
|
@@ -358,35 +248,37 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
358
248
|
window.addEventListener('mouseup', onUp)
|
|
359
249
|
}
|
|
360
250
|
|
|
361
|
-
|
|
362
|
-
if (copiedPath === path) return
|
|
363
|
-
void navigator.clipboard?.writeText(path).catch(() => undefined)
|
|
364
|
-
setCopiedPath(path)
|
|
365
|
-
window.clearTimeout(copyTimer.current)
|
|
366
|
-
copyTimer.current = window.setTimeout(() => setCopiedPath(null), 1200)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
useEffect(() => () => window.clearTimeout(copyTimer.current), [])
|
|
370
|
-
|
|
371
|
-
// 首次(store 从未同步过)自动拉一次快照(无论是否折叠,折叠条也要显示计数)
|
|
251
|
+
// 会话切换时仅同步数据,不自动打开面板
|
|
372
252
|
useEffect(() => {
|
|
373
253
|
if (!sessionId) return
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
return () => window.clearTimeout(timer)
|
|
254
|
+
// 仅当面板已打开时才自动刷新
|
|
255
|
+
if (!ui.closed) {
|
|
256
|
+
void refresh()
|
|
378
257
|
}
|
|
379
|
-
return undefined
|
|
380
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
381
258
|
}, [sessionId])
|
|
382
259
|
|
|
383
|
-
//
|
|
384
|
-
|
|
260
|
+
// 监听模型工具请求打开面板
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const ctxWithEvents = ctx
|
|
263
|
+
const handler = () => {
|
|
264
|
+
uiActions.open()
|
|
265
|
+
if (Date.now() - Math.max(data.lastUpdate, syncedAt) > 30_000) {
|
|
266
|
+
void refresh()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
ctxWithEvents?.events?.on?.('dsh:task-panel:show', handler)
|
|
270
|
+
return () => {
|
|
271
|
+
ctxWithEvents?.events?.off?.('dsh:task-panel:show', handler)
|
|
272
|
+
}
|
|
273
|
+
}, [ctx, uiActions, data.lastUpdate, syncedAt, refresh])
|
|
274
|
+
|
|
275
|
+
// 彻底隐藏:不渲染任何东西
|
|
276
|
+
if (ui.closed) {
|
|
385
277
|
return null
|
|
386
278
|
}
|
|
387
279
|
|
|
388
|
-
//
|
|
389
|
-
if (
|
|
280
|
+
// 折叠态:迷你条
|
|
281
|
+
if (ui.minimized) {
|
|
390
282
|
const label = boundTask
|
|
391
283
|
? `${t('minibar.current')}: ${boundTask.title}`
|
|
392
284
|
: activeTasks.length > 0
|
|
@@ -396,9 +288,9 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
396
288
|
<MiniBar
|
|
397
289
|
label={label}
|
|
398
290
|
hint={t('minibar.click-expand')}
|
|
399
|
-
position={
|
|
291
|
+
position={ui.panelPosition}
|
|
400
292
|
theme={style}
|
|
401
|
-
onMove={
|
|
293
|
+
onMove={uiActions.setPanelPosition}
|
|
402
294
|
open={expand}
|
|
403
295
|
/>
|
|
404
296
|
)
|
|
@@ -413,7 +305,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
413
305
|
<div
|
|
414
306
|
className={css.panel}
|
|
415
307
|
ref={panelRef}
|
|
416
|
-
style={{ left:
|
|
308
|
+
style={{ left: ui.panelPosition.x, top: ui.panelPosition.y }}
|
|
417
309
|
data-theme={style === 'native' ? undefined : style}
|
|
418
310
|
>
|
|
419
311
|
<div className={css.dragHandle} onMouseDown={handleDragStart} title={t('task.drag')}>
|
|
@@ -421,7 +313,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
421
313
|
</div>
|
|
422
314
|
<header className={css.header}>
|
|
423
315
|
<div className={css.headerLeft}>
|
|
424
|
-
<
|
|
316
|
+
<Button
|
|
425
317
|
type="button"
|
|
426
318
|
className={css.headerIconBtn}
|
|
427
319
|
onClick={cycleTheme}
|
|
@@ -429,30 +321,60 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
429
321
|
aria-label={styleLabel}
|
|
430
322
|
>
|
|
431
323
|
<IconFolderOpenOutline16 className={css.headerIcon} />
|
|
432
|
-
</
|
|
433
|
-
<h2 className={css.headerTitle}>{
|
|
324
|
+
</Button>
|
|
325
|
+
<h2 className={css.headerTitle}>{viewTitle}</h2>
|
|
434
326
|
</div>
|
|
435
327
|
<div className={css.headerRight}>
|
|
436
|
-
<span className={css.counts}>
|
|
437
|
-
{activeTasks.length > 0
|
|
438
|
-
? `${t('panel.active')} ${activeTasks.length} · ${doneCount}/${activeTasks.length}`
|
|
439
|
-
: ''}
|
|
440
|
-
</span>
|
|
441
328
|
<Button
|
|
442
329
|
variant="ghost"
|
|
443
330
|
size="sm"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
aria-label={t('panel.refresh')}
|
|
448
|
-
title={t('panel.refresh')}
|
|
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]})`}
|
|
449
334
|
>
|
|
450
|
-
|
|
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>
|
|
451
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
|
+
)}
|
|
452
365
|
<Button
|
|
453
366
|
variant="ghost"
|
|
454
367
|
size="sm"
|
|
455
|
-
onClick={() =>
|
|
368
|
+
onClick={() => setShowHints(!showHints)}
|
|
369
|
+
aria-label={showHints ? t('panel.hints-off') : t('panel.hints-on')}
|
|
370
|
+
title={showHints ? t('panel.hints-off') : t('panel.hints-on')}
|
|
371
|
+
>
|
|
372
|
+
<IconQuestionOutline14 size={16} />
|
|
373
|
+
</Button>
|
|
374
|
+
<Button
|
|
375
|
+
variant="ghost"
|
|
376
|
+
size="sm"
|
|
377
|
+
onClick={() => uiActions.minimize()}
|
|
456
378
|
aria-label={t('panel.minimize')}
|
|
457
379
|
title={t('panel.minimize')}
|
|
458
380
|
>
|
|
@@ -461,7 +383,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
461
383
|
<Button
|
|
462
384
|
variant="ghost"
|
|
463
385
|
size="sm"
|
|
464
|
-
onClick={() =>
|
|
386
|
+
onClick={() => uiActions.close()}
|
|
465
387
|
aria-label={t('panel.close')}
|
|
466
388
|
title={t('panel.close')}
|
|
467
389
|
>
|
|
@@ -473,7 +395,9 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
473
395
|
{!sessionId && <div className={css.notice}>{t('panel.no-session')}</div>}
|
|
474
396
|
{syncError && <div className={css.notice}>{t('panel.sync-failed')}: {syncError}</div>}
|
|
475
397
|
|
|
476
|
-
{
|
|
398
|
+
{view !== 'task' ? (
|
|
399
|
+
<MemoryView ctx={ctx} sessionId={sessionId} scope={view as InsightScope} boundTaskId={data.boundTaskId} t={t} />
|
|
400
|
+
) : activeTasks.length === 0 ? (
|
|
477
401
|
<div className={css.emptyState}>
|
|
478
402
|
<p className={css.emptyTitle}>{t('panel.empty')}</p>
|
|
479
403
|
<p className={css.emptyDesc}>{t('panel.empty-desc')}</p>
|
|
@@ -483,168 +407,27 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
483
407
|
<div className={css.taskList}>
|
|
484
408
|
{activeTasks.map((task) => {
|
|
485
409
|
const steps = task.steps || []
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
const isBound = state.boundTaskId === task.id
|
|
489
|
-
const pct = steps.length > 0 ? Math.round((done / steps.length) * 100) : 0
|
|
410
|
+
const expanded = ui.expandedTaskIds.includes(task.id)
|
|
411
|
+
const isBound = data.boundTaskId === task.id
|
|
490
412
|
return (
|
|
491
|
-
<
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
title={isBound ? t('task.title-edit') : undefined}
|
|
510
|
-
onDoubleClick={(e) => {
|
|
511
|
-
e.preventDefault()
|
|
512
|
-
e.stopPropagation()
|
|
513
|
-
if (isBound) setEditingTitle({ taskId: task.id, value: task.title })
|
|
514
|
-
}}
|
|
515
|
-
>
|
|
516
|
-
{task.title}
|
|
517
|
-
</span>
|
|
518
|
-
)}
|
|
519
|
-
</div>
|
|
520
|
-
<div className={css.cardMeta}>
|
|
521
|
-
{steps.length > 0 && (
|
|
522
|
-
<span className={css.progressText}>
|
|
523
|
-
{done}/{steps.length}
|
|
524
|
-
</span>
|
|
525
|
-
)}
|
|
526
|
-
<span className={css.updated}>{timeAgo(task.updatedAt || task.lastActiveAt)}</span>
|
|
527
|
-
<span className={css.chevron}>
|
|
528
|
-
{expanded ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}
|
|
529
|
-
</span>
|
|
530
|
-
</div>
|
|
531
|
-
</button>
|
|
532
|
-
{steps.length > 0 && (
|
|
533
|
-
<div className={css.track}>
|
|
534
|
-
<div className={css.trackFill} style={{ width: `${pct}%` }} />
|
|
535
|
-
</div>
|
|
536
|
-
)}
|
|
537
|
-
{expanded && (
|
|
538
|
-
<div className={css.cardBody}>
|
|
539
|
-
<div className={css.section}>
|
|
540
|
-
<div className={css.sectionLabel}>
|
|
541
|
-
{t('task.steps')}
|
|
542
|
-
{steps.length > 0 && <span className={css.sectionCount}>{steps.length}</span>}
|
|
543
|
-
</div>
|
|
544
|
-
{steps.length > 0 ? (
|
|
545
|
-
<ol className={css.stepsList}>
|
|
546
|
-
{steps.map((step, i) => {
|
|
547
|
-
const isEditing = isBound && editing?.taskId === task.id && editing.index === i
|
|
548
|
-
return (
|
|
549
|
-
<li key={i} className={css.stepRow}>
|
|
550
|
-
{isBound ? (
|
|
551
|
-
<button
|
|
552
|
-
type="button"
|
|
553
|
-
className={css.stepToggle}
|
|
554
|
-
disabled={syncing}
|
|
555
|
-
onClick={() => cycleStatus(task.id, i)}
|
|
556
|
-
title={`${t('step.completed')}/${t('step.in-progress')}/${t('step.pending')}(点击切换)`}
|
|
557
|
-
>
|
|
558
|
-
<StepIcon status={step.status} />
|
|
559
|
-
</button>
|
|
560
|
-
) : (
|
|
561
|
-
<StepIcon status={step.status} />
|
|
562
|
-
)}
|
|
563
|
-
{isEditing ? (
|
|
564
|
-
<AutoEdit
|
|
565
|
-
value={editing!.value}
|
|
566
|
-
onCommit={(v) => commitStepText(task.id, i, v)}
|
|
567
|
-
onCancel={() => setEditing(null)}
|
|
568
|
-
/>
|
|
569
|
-
) : (
|
|
570
|
-
<span
|
|
571
|
-
className={`${css.stepContent}${step.status === 'completed' ? ` ${css.stepContentDone}` : ''}${step.status === 'in_progress' ? ` ${css.stepContentRun}` : ''}${isBound ? ` ${css.stepEditable}` : ''}`}
|
|
572
|
-
title={isBound ? t('step.edit-hint') : undefined}
|
|
573
|
-
onDoubleClick={() => {
|
|
574
|
-
if (isBound) setEditing({ taskId: task.id, index: i, value: String(step.content ?? '') })
|
|
575
|
-
}}
|
|
576
|
-
>
|
|
577
|
-
{step.content}
|
|
578
|
-
</span>
|
|
579
|
-
)}
|
|
580
|
-
</li>
|
|
581
|
-
)
|
|
582
|
-
})}
|
|
583
|
-
</ol>
|
|
584
|
-
) : (
|
|
585
|
-
<div className={css.muted}>{t('panel.empty-desc')}</div>
|
|
586
|
-
)}
|
|
587
|
-
</div>
|
|
588
|
-
|
|
589
|
-
{(task.files?.length ?? 0) > 0 && (
|
|
590
|
-
<div className={css.section}>
|
|
591
|
-
<div className={css.sectionLabel}>
|
|
592
|
-
{t('task.files')}
|
|
593
|
-
<span className={css.sectionCount}>{task.files.length}</span>
|
|
594
|
-
</div>
|
|
595
|
-
<ul className={css.filesList}>
|
|
596
|
-
{task.files.slice(0, 12).map((file, i) => (
|
|
597
|
-
<li key={i}>
|
|
598
|
-
<button
|
|
599
|
-
className={css.fileRow}
|
|
600
|
-
onClick={() => copyPath(file.path)}
|
|
601
|
-
title={`${t(copiedPath === file.path ? 'file.copied' : 'file.copy')}: ${file.path}${file.line ? `:${file.line}` : ''}`}
|
|
602
|
-
>
|
|
603
|
-
<span className={css.fileDot} />
|
|
604
|
-
<span className={css.filePath}>{file.path}</span>
|
|
605
|
-
{file.line && <span className={css.fileLine}>:{file.line}</span>}
|
|
606
|
-
</button>
|
|
607
|
-
</li>
|
|
608
|
-
))}
|
|
609
|
-
{(task.files?.length ?? 0) > 12 && (
|
|
610
|
-
<li className={css.muted}>… 共 {task.files.length} 个</li>
|
|
611
|
-
)}
|
|
612
|
-
</ul>
|
|
613
|
-
</div>
|
|
614
|
-
)}
|
|
615
|
-
|
|
616
|
-
<div className={css.cardFooter}>
|
|
617
|
-
<Button
|
|
618
|
-
variant="outline"
|
|
619
|
-
size="sm"
|
|
620
|
-
disabled={isBound || syncing}
|
|
621
|
-
onClick={() => void handleAction('switch', task.id)}
|
|
622
|
-
>
|
|
623
|
-
{t('task.switch')}
|
|
624
|
-
</Button>
|
|
625
|
-
{isBound && (
|
|
626
|
-
<Button
|
|
627
|
-
variant="outline"
|
|
628
|
-
size="sm"
|
|
629
|
-
disabled={syncing}
|
|
630
|
-
onClick={() => void runLine('/task unbind')}
|
|
631
|
-
title={t('task.unbind')}
|
|
632
|
-
>
|
|
633
|
-
{t('task.unbind')}
|
|
634
|
-
</Button>
|
|
635
|
-
)}
|
|
636
|
-
<Button
|
|
637
|
-
variant="outline"
|
|
638
|
-
size="sm"
|
|
639
|
-
disabled={syncing}
|
|
640
|
-
onClick={() => void handleAction('archive', task.id)}
|
|
641
|
-
>
|
|
642
|
-
{t('task.archive')}
|
|
643
|
-
</Button>
|
|
644
|
-
</div>
|
|
645
|
-
</div>
|
|
646
|
-
)}
|
|
647
|
-
</article>
|
|
413
|
+
<TaskCard
|
|
414
|
+
key={task.id}
|
|
415
|
+
task={task}
|
|
416
|
+
isBound={isBound}
|
|
417
|
+
expanded={expanded}
|
|
418
|
+
onToggleExpand={() => uiActions.toggleTaskExpanded(task.id)}
|
|
419
|
+
onSwitch={() => handleAction('switch', task.id)}
|
|
420
|
+
onUnbind={() => runLine('/task unbind')}
|
|
421
|
+
onArchive={() => handleAction('archive', task.id)}
|
|
422
|
+
onRename={(value) => commitTitle(task.id, value)}
|
|
423
|
+
onEditStep={(index, value) => commitStepText(task.id, index, value)}
|
|
424
|
+
onCycleStatus={(index) => cycleStatus(task.id, index)}
|
|
425
|
+
onReorderSteps={(from, to) => reorderSteps(task.id, from, to)}
|
|
426
|
+
onInsightAction={(action, id) => void handleInsightTask(action, id)}
|
|
427
|
+
showHints={showHints}
|
|
428
|
+
syncing={syncing}
|
|
429
|
+
t={t}
|
|
430
|
+
/>
|
|
648
431
|
)
|
|
649
432
|
})}
|
|
650
433
|
{syncing && <div className={css.syncHint}>{t('panel.syncing')}</div>}
|
|
@@ -652,4 +435,4 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
652
435
|
)}
|
|
653
436
|
</div>
|
|
654
437
|
)
|
|
655
|
-
}
|
|
438
|
+
}
|