@yolk_vat-y/dsh-project-memory 0.4.4 → 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 +29 -0
- package/README.md +11 -2
- package/README.zh-CN.md +11 -2
- package/client/client.js +798 -81
- 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 +30 -11
- package/src/client/TaskComponents.tsx +48 -1
- package/src/client/TaskPanel.module.css +122 -0
- 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
|
@@ -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()
|
|
@@ -104,6 +116,7 @@ interface TaskCardProps {
|
|
|
104
116
|
onEditStep: (index: number, value: string) => void
|
|
105
117
|
onCycleStatus: (index: number) => void
|
|
106
118
|
onReorderSteps: (fromIndex: number, toIndex: number) => void
|
|
119
|
+
onInsightAction?: (action: InsightAction, id: string) => void
|
|
107
120
|
showHints: boolean
|
|
108
121
|
syncing: boolean
|
|
109
122
|
t: ReturnType<typeof createTranslate>
|
|
@@ -121,6 +134,7 @@ export function TaskCard({
|
|
|
121
134
|
onEditStep,
|
|
122
135
|
onCycleStatus,
|
|
123
136
|
onReorderSteps,
|
|
137
|
+
onInsightAction,
|
|
124
138
|
showHints,
|
|
125
139
|
syncing,
|
|
126
140
|
t,
|
|
@@ -321,6 +335,39 @@ export function TaskCard({
|
|
|
321
335
|
</div>
|
|
322
336
|
)}
|
|
323
337
|
|
|
338
|
+
{onInsightAction && Array.isArray(task.insights) && task.insights.length > 0 && (
|
|
339
|
+
<div className={css.section}>
|
|
340
|
+
<div className={css.sectionLabel}>
|
|
341
|
+
{t('mem.section-label')}
|
|
342
|
+
<span className={css.sectionCount}>{task.insights.length}</span>
|
|
343
|
+
</div>
|
|
344
|
+
<ul className={css.memList}>
|
|
345
|
+
{(task.insights as TaskInsight[]).map((ins) => (
|
|
346
|
+
<li key={ins.id} className={css.memRow}>
|
|
347
|
+
<div className={css.memTitle}>
|
|
348
|
+
<span className={css.kindBadge}>{insightKindLabel(ins.kind, t)}</span>
|
|
349
|
+
<span>{ins.title || ins.id}</span>
|
|
350
|
+
{ins.draft && <span className={`${css.badge} ${css.badgeDraft}`}>{t('mem.draft')}</span>}
|
|
351
|
+
</div>
|
|
352
|
+
<div className={css.memActs}>
|
|
353
|
+
{ins.draft && (
|
|
354
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('confirm', ins.id)}>
|
|
355
|
+
{t('mem.confirm')}
|
|
356
|
+
</Button>
|
|
357
|
+
)}
|
|
358
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('promote', ins.id)}>
|
|
359
|
+
{t('mem.promote')}
|
|
360
|
+
</Button>
|
|
361
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('delete', ins.id)}>
|
|
362
|
+
{t('mem.delete')}
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</li>
|
|
366
|
+
))}
|
|
367
|
+
</ul>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
|
|
324
371
|
<div className={css.cardFooter}>
|
|
325
372
|
<Button variant="outline" size="sm" disabled={isBound || syncing} onClick={onSwitch}>
|
|
326
373
|
{t('task.switch')}
|
|
@@ -625,3 +625,125 @@
|
|
|
625
625
|
}
|
|
626
626
|
|
|
627
627
|
/* —— header 防重叠 —— */
|
|
628
|
+
|
|
629
|
+
/* ---- v0.5 Memory View(Task 草稿 / Project / Global 三作用域列表) ---- */
|
|
630
|
+
.memoryPane {
|
|
631
|
+
padding: 8px 10px 10px;
|
|
632
|
+
display: flex;
|
|
633
|
+
flex-direction: column;
|
|
634
|
+
gap: 6px;
|
|
635
|
+
overflow: auto;
|
|
636
|
+
max-height: 480px;
|
|
637
|
+
}
|
|
638
|
+
.memToolbar {
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
justify-content: space-between;
|
|
642
|
+
gap: 8px;
|
|
643
|
+
}
|
|
644
|
+
.memCount {
|
|
645
|
+
font-size: 12px;
|
|
646
|
+
opacity: 0.75;
|
|
647
|
+
font-weight: 600;
|
|
648
|
+
}
|
|
649
|
+
.memList {
|
|
650
|
+
display: flex;
|
|
651
|
+
flex-direction: column;
|
|
652
|
+
gap: 6px;
|
|
653
|
+
}
|
|
654
|
+
.memRow {
|
|
655
|
+
border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
|
|
656
|
+
border-radius: 6px;
|
|
657
|
+
padding: 6px 8px;
|
|
658
|
+
display: flex;
|
|
659
|
+
flex-direction: column;
|
|
660
|
+
gap: 4px;
|
|
661
|
+
background: color-mix(in srgb, var(--dsw-bg-secondary, #f5f5f5) 45%, transparent);
|
|
662
|
+
}
|
|
663
|
+
.memRowArchived {
|
|
664
|
+
opacity: 0.55;
|
|
665
|
+
}
|
|
666
|
+
.memRowMain {
|
|
667
|
+
display: flex;
|
|
668
|
+
flex-direction: column;
|
|
669
|
+
gap: 2px;
|
|
670
|
+
}
|
|
671
|
+
.memTitle {
|
|
672
|
+
display: flex;
|
|
673
|
+
align-items: flex-start;
|
|
674
|
+
gap: 6px;
|
|
675
|
+
font-size: 13px;
|
|
676
|
+
line-height: 1.35;
|
|
677
|
+
word-break: break-word;
|
|
678
|
+
}
|
|
679
|
+
.kindBadge {
|
|
680
|
+
flex: none;
|
|
681
|
+
font-size: 11px;
|
|
682
|
+
border-radius: 4px;
|
|
683
|
+
padding: 0 5px;
|
|
684
|
+
line-height: 1.5;
|
|
685
|
+
background: var(--dsw-accent-soft, rgba(80, 120, 255, 0.15));
|
|
686
|
+
color: var(--dsw-accent, #4d6bfe);
|
|
687
|
+
}
|
|
688
|
+
.memMeta {
|
|
689
|
+
display: flex;
|
|
690
|
+
flex-wrap: wrap;
|
|
691
|
+
gap: 4px;
|
|
692
|
+
align-items: center;
|
|
693
|
+
}
|
|
694
|
+
.badge {
|
|
695
|
+
font-size: 11px;
|
|
696
|
+
border-radius: 4px;
|
|
697
|
+
padding: 0 5px;
|
|
698
|
+
line-height: 1.5;
|
|
699
|
+
background: color-mix(in srgb, var(--dsw-fg, #222) 8%, transparent);
|
|
700
|
+
}
|
|
701
|
+
.badgeDraft {
|
|
702
|
+
color: #b26a00;
|
|
703
|
+
background: rgba(255, 180, 0, 0.16);
|
|
704
|
+
}
|
|
705
|
+
.badgeArchived {
|
|
706
|
+
opacity: 0.8;
|
|
707
|
+
}
|
|
708
|
+
.memBody {
|
|
709
|
+
font-size: 12px;
|
|
710
|
+
opacity: 0.8;
|
|
711
|
+
word-break: break-word;
|
|
712
|
+
}
|
|
713
|
+
.memActs {
|
|
714
|
+
display: flex;
|
|
715
|
+
flex-wrap: wrap;
|
|
716
|
+
gap: 4px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ---- v0.5 Memory View 新建表单 ---- */
|
|
720
|
+
.memToolbarActs {
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: center;
|
|
723
|
+
gap: 6px;
|
|
724
|
+
}
|
|
725
|
+
.memNew {
|
|
726
|
+
display: flex;
|
|
727
|
+
flex-direction: column;
|
|
728
|
+
gap: 6px;
|
|
729
|
+
border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
|
|
730
|
+
border-radius: 6px;
|
|
731
|
+
padding: 8px;
|
|
732
|
+
}
|
|
733
|
+
.memNewRow {
|
|
734
|
+
display: flex;
|
|
735
|
+
gap: 8px;
|
|
736
|
+
align-items: center;
|
|
737
|
+
}
|
|
738
|
+
.memLabel {
|
|
739
|
+
display: inline-flex;
|
|
740
|
+
align-items: center;
|
|
741
|
+
gap: 6px;
|
|
742
|
+
font-size: 12px;
|
|
743
|
+
opacity: 0.8;
|
|
744
|
+
}
|
|
745
|
+
.memNew select,
|
|
746
|
+
.memNew textarea {
|
|
747
|
+
width: 100%;
|
|
748
|
+
box-sizing: border-box;
|
|
749
|
+
}
|
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 },
|
package/src/client/locales.ts
CHANGED
|
@@ -40,6 +40,40 @@ export const zh = {
|
|
|
40
40
|
'minibar.click-expand': '点击展开并同步',
|
|
41
41
|
'minibar.tasks': '个任务',
|
|
42
42
|
'minibar.no-task': '暂无任务 · 点击查看',
|
|
43
|
+
'view.cycle': '切换视图:任务 / 项目记忆 / 全局记忆',
|
|
44
|
+
'view.task': '任务',
|
|
45
|
+
'view.project': '项目记忆',
|
|
46
|
+
'view.global': '全局记忆',
|
|
47
|
+
'mem.scope.task': '本任务草稿',
|
|
48
|
+
'mem.scope.project': '项目记忆',
|
|
49
|
+
'mem.scope.global': '全局记忆',
|
|
50
|
+
'mem.empty': '暂无记忆条目(反思或 save_lesson 后出现)',
|
|
51
|
+
'mem.confirm': '审核',
|
|
52
|
+
'mem.promote': '提升',
|
|
53
|
+
'mem.demote': '降级',
|
|
54
|
+
'mem.archive': '归档',
|
|
55
|
+
'mem.restore': '恢复',
|
|
56
|
+
'mem.delete': '删除',
|
|
57
|
+
'mem.draft': '草稿',
|
|
58
|
+
'mem.archived': '已归档',
|
|
59
|
+
'mem.loading': '加载中…',
|
|
60
|
+
'mem.error': '同步失败',
|
|
61
|
+
'mem.kind.lesson': '坑',
|
|
62
|
+
'mem.kind.decision': '权衡',
|
|
63
|
+
'mem.kind.procedure': '指南',
|
|
64
|
+
'mem.kind.experience': '经验',
|
|
65
|
+
'mem.needs-bound': '需绑定任务后可降级',
|
|
66
|
+
'mem.new': '新建',
|
|
67
|
+
'mem.save': '保存',
|
|
68
|
+
'mem.cancel': '取消',
|
|
69
|
+
'mem.f.title': '标题(教训模式/主题/指南名)',
|
|
70
|
+
'mem.f.kind': '类型',
|
|
71
|
+
'mem.f.body': '做法 / 理由 / 解法',
|
|
72
|
+
'mem.f.steps': '步骤(每行一步)',
|
|
73
|
+
'mem.f.trigger': '作为 Skill:触发关键词(逗号分隔)',
|
|
74
|
+
'mem.saved': '已保存',
|
|
75
|
+
'mem.edit': '编辑',
|
|
76
|
+
'mem.section-label': '任务记忆',
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
export const en = {
|
|
@@ -81,6 +115,40 @@ export const en = {
|
|
|
81
115
|
'minibar.click-expand': 'Click to expand & sync',
|
|
82
116
|
'minibar.tasks': 'tasks',
|
|
83
117
|
'minibar.no-task': 'No tasks · click to view',
|
|
118
|
+
'view.cycle': 'Switch view: tasks / project memory / global memory',
|
|
119
|
+
'view.task': 'Tasks',
|
|
120
|
+
'view.project': 'Project Memory',
|
|
121
|
+
'view.global': 'Global Memory',
|
|
122
|
+
'mem.scope.task': 'Task drafts',
|
|
123
|
+
'mem.scope.project': 'Project memory',
|
|
124
|
+
'mem.scope.global': 'Global memory',
|
|
125
|
+
'mem.empty': 'No memory entries yet (created by reflection or save_lesson)',
|
|
126
|
+
'mem.confirm': 'Approve',
|
|
127
|
+
'mem.promote': 'Promote',
|
|
128
|
+
'mem.demote': 'Demote',
|
|
129
|
+
'mem.archive': 'Archive',
|
|
130
|
+
'mem.restore': 'Restore',
|
|
131
|
+
'mem.delete': 'Delete',
|
|
132
|
+
'mem.draft': 'draft',
|
|
133
|
+
'mem.archived': 'archived',
|
|
134
|
+
'mem.loading': 'Loading…',
|
|
135
|
+
'mem.error': 'Sync failed',
|
|
136
|
+
'mem.kind.lesson': 'lesson',
|
|
137
|
+
'mem.kind.decision': 'decision',
|
|
138
|
+
'mem.kind.procedure': 'procedure',
|
|
139
|
+
'mem.kind.experience': 'experience',
|
|
140
|
+
'mem.needs-bound': 'Bind a task to demote',
|
|
141
|
+
'mem.new': 'New',
|
|
142
|
+
'mem.save': 'Save',
|
|
143
|
+
'mem.cancel': 'Cancel',
|
|
144
|
+
'mem.f.title': 'Title (pattern / topic / procedure name)',
|
|
145
|
+
'mem.f.kind': 'Kind',
|
|
146
|
+
'mem.f.body': 'Fix / reason / solution',
|
|
147
|
+
'mem.f.steps': 'Steps (one per line)',
|
|
148
|
+
'mem.f.trigger': 'As Skill: trigger keywords (comma separated)',
|
|
149
|
+
'mem.saved': 'Saved',
|
|
150
|
+
'mem.edit': 'Edit',
|
|
151
|
+
'mem.section-label': 'Task memory',
|
|
84
152
|
}
|
|
85
153
|
|
|
86
154
|
export type LocaleDict = typeof zh
|
|
@@ -15,6 +15,13 @@ export interface TaskFile {
|
|
|
15
15
|
line?: number
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface TaskInsight {
|
|
19
|
+
id: string
|
|
20
|
+
kind: string
|
|
21
|
+
title: string
|
|
22
|
+
draft?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export interface Task {
|
|
19
26
|
id: string
|
|
20
27
|
title: string
|
|
@@ -23,6 +30,7 @@ export interface Task {
|
|
|
23
30
|
lastActiveAt: string
|
|
24
31
|
updatedAt: string
|
|
25
32
|
archived?: boolean
|
|
33
|
+
insights?: TaskInsight[]
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export interface TaskPayload {
|
|
@@ -68,8 +76,16 @@ let dataState: TaskDataState = {
|
|
|
68
76
|
|
|
69
77
|
const dataListeners = new Set<() => void>()
|
|
70
78
|
|
|
71
|
-
function setDataState(next: TaskDataState): void {
|
|
72
|
-
|
|
79
|
+
function setDataState(next: TaskDataState | ((prev: TaskDataState) => TaskDataState)): void {
|
|
80
|
+
// 兼容对象与函数式 updater(BroadcastChannel 曾误把 updater 当对象写入,
|
|
81
|
+
// 导致 dataState 变成函数、tasks 丢失 → 面板崩溃)。写入一律归一化为完整 shape。
|
|
82
|
+
const patch = typeof next === 'function' ? next(dataState) : next
|
|
83
|
+
dataState = {
|
|
84
|
+
tasks: Array.isArray(patch?.tasks) ? patch.tasks : [],
|
|
85
|
+
boundTaskId: patch?.boundTaskId ?? null,
|
|
86
|
+
archivedCount: typeof patch?.archivedCount === 'number' ? patch.archivedCount : 0,
|
|
87
|
+
lastUpdate: typeof patch?.lastUpdate === 'number' ? patch.lastUpdate : 0,
|
|
88
|
+
}
|
|
73
89
|
for (const listener of dataListeners) listener()
|
|
74
90
|
}
|
|
75
91
|
|
|
@@ -77,7 +93,7 @@ function broadcastDataUpdate(tasks: Task[], archivedCount: number): void {
|
|
|
77
93
|
if (!SYNC_CHANNEL) return
|
|
78
94
|
SYNC_CHANNEL.postMessage({
|
|
79
95
|
type: 'PROJECT_TASKS_UPDATED',
|
|
80
|
-
payload: { tasks, archivedCount },
|
|
96
|
+
payload: { tasks: Array.isArray(tasks) ? tasks : [], archivedCount },
|
|
81
97
|
})
|
|
82
98
|
}
|
|
83
99
|
|
|
@@ -88,8 +104,8 @@ if (SYNC_CHANNEL) {
|
|
|
88
104
|
const { tasks, archivedCount } = msg.payload
|
|
89
105
|
setDataState(prev => ({
|
|
90
106
|
...prev,
|
|
91
|
-
tasks: tasks
|
|
92
|
-
archivedCount: archivedCount
|
|
107
|
+
tasks: Array.isArray(tasks) ? tasks : [],
|
|
108
|
+
archivedCount: typeof archivedCount === 'number' ? archivedCount : prev.archivedCount,
|
|
93
109
|
lastUpdate: Date.now(),
|
|
94
110
|
}))
|
|
95
111
|
}
|