@starwish-ai/xingyuan-dsh 0.5.8 → 0.6.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 CHANGED
@@ -5,6 +5,84 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-09-05
9
+
10
+ > **Note:** behavioral revision — long-term ratios (wish progress, progress charts) join
11
+ > the commitment semantics: unclaimed tasks no longer count in the denominator, and
12
+ > achievement additionally requires zero unclaimed tasks. No data format change; upgrades
13
+ > from 0.5.x are seamless — the switch can only raise progress, and every newly-qualifying
14
+ > wish carries unclaimed candidates that the achievement gate holds back (nothing archives
15
+ > by accident).
16
+
17
+ ### Added
18
+ - **Achievement gate and the "wrapping up" state**: a wish achieves only at 100% progress
19
+ with no unclaimed tasks (single formula `wishProgressFromAgg` shared by the write path and
20
+ the read side). Full progress with unclaimed tasks parks the wish at "wrapping up" — the
21
+ bar is full, the wish is not archived — and the card shows the pending count with the
22
+ guidance line "claim to continue, or delete to achieve". The call is always the user's:
23
+ the assistant never claims or deletes on its own to force achievement.
24
+ - **Gate flips always reach the model**: check-in / claim / update / cancel / delete / create
25
+ (including batches) compare before/after wish views and, on achieve / wrap-up / rollback,
26
+ say so in the tool reply and re-emit the wish event, so chat card, reply and wishes page
27
+ stay in sync. Count drift without a flip also re-emits (the "N to claim" badge never
28
+ freezes).
29
+ - **Single-source display bits**: `pendingCount` / `settled` / `planning` / `achieved` are
30
+ produced by `freshWishes` and delivered via `/api/wishes` and `xingyuan/wish` events;
31
+ replays of older events degrade honestly to a plain progress display.
32
+ - **Wording standard**: all user-facing and verbatim-relay surfaces (i18n, tool replies,
33
+ prompt guides, standalone pages) say "待领取的任务" / "待收尾" — no internal jargon.
34
+ Locked by a banned-word test (`test/prompts.test.ts`) and a shared guidance-line constant.
35
+
36
+ ### Changed
37
+ - `wishProgress` and `taskCompletionRate` now divide by claimed-task days only; the
38
+ subtitle is fixed to "已领取任务" (the conditional "含未领取任务" wording is gone —
39
+ one metric, one meaning).
40
+ - Growth-page achievement counts and the `wishAchievement` chart consume the derived
41
+ achieved bit instead of `progress >= 100`, matching the wish card exactly.
42
+ - The session-opening overview reads fresh derived views (not the stored `archived` bit)
43
+ and distinguishes "all wishes achieved" from "no wishes yet".
44
+
45
+ ### Fixed
46
+ - Creating a bare wish showed "进度 0%" on the chat card while the wishes page said
47
+ "计划中" (the create event dropped the derived bits).
48
+ - Claiming a standalone task (no parent wish) claimed its days were folded into "wish
49
+ progress".
50
+ - Batch-delete replies glued the achievement guidance onto the summary line; batch paths
51
+ no longer re-scan the task table per wish on the after side.
52
+
53
+ ## [0.5.9] - 2026-09-04
54
+
55
+ > **Note:** this release migrates the plugin to DeepSeek Harness `0.1.2-rc.1`
56
+ > (the host is still a technical preview and broke APIs on this bump). No data
57
+ > format changes: existing databases, settings and recorded sessions replay as
58
+ > before. On older hosts (`0.1.1-rc.2`) this version no longer loads — pin
59
+ > `0.5.8` if you must stay on the previous harness.
60
+
61
+ ### Changed
62
+ - **Settings namespaces now install through the service face**
63
+ (`ctx.inject(['settings'], (inv) => inv.settings.installSection(...))`):
64
+ `dsh-settings` removed the standalone `installSettingsSection` /
65
+ `settingsNamespace` helpers, so `xingyuan-pref` and `xingyuan-ui` register
66
+ via the new API and are no longer imported at runtime. The persistence
67
+ guarantee is unchanged — namespaces stay on the bundle-resident layer.
68
+ - **Client half re-wired to the reshaped browser packages**:
69
+ `@deepseek-ai/dsh-client-runtime` is gone; `ClientContext` is now plain
70
+ Cordis `Context`. The `ChatNodeDataMap` declaration merge moved to
71
+ `@deepseek-ai/dsh-client-ui-chat/client`, `ConversationStepDataMap` stays in
72
+ `dsh-client-ui-conversation/client`, event-card definitions register through
73
+ `ctx.uiConversation.events`, and the slot service is provided by
74
+ `dsh-client-ui-renderer`. `dsh.client.inject` was rewritten to the new
75
+ package-row semantics (baseline modules are seeded by the shell and need no
76
+ declaration).
77
+ - **Session-preset detection for tab visibility** now reads
78
+ `projectionValues.agentPreset` from the sessions list row — the
79
+ `SessionSummary.agentPreset` field was removed upstream in
80
+ `0.1.2-rc.1` (this was silently hiding all Xingyuan view tabs after the
81
+ host bump; same data source the official preset label uses).
82
+ - **Peer dependency ranges** bumped to `^0.1.2-rc.1` (Cordis `^4.0.2`);
83
+ AGENTS.md records the migration facts and the new troubleshooting rows
84
+ (`cannot get property without inject`, missing view tabs).
85
+
8
86
  ## [0.5.8] - 2026-09-04
9
87
 
10
88
  > **Note:** this release is the fourth six-perspective quality pass. It finishes
package/README.en-US.md CHANGED
@@ -12,6 +12,7 @@ XingYuan is a wish & habit-building companion, shipped as a DeepSeek Harness plu
12
12
 
13
13
  - **Conversation-driven**: create wishes / tasks in natural language with duplicate detection
14
14
  - **Opportunity-day check-ins**: check-in, make-up, cancel, and future pre-checks; progress always recomputed against today
15
+ - **Commitment-based progress**: only claimed tasks count; a wish achieves when everything committed is done and nothing is left to claim — full progress with unclaimed tasks leaves it "wrapping up" for your call (claim to continue, or delete to achieve), never nagging, never deciding for you
15
16
  - **Micro-actions**: break an overwhelming goal into 3–7 tiny steps and walk through them guided
16
17
  - **Growth system**: levels Lv.1–Lv.10, streak bonuses, stat cards, 30-day bar chart
17
18
  - **Session view tabs**: Today / Wishes / Tasks / Calendar / Growth / Memory — buttons call action endpoints directly; shown only in XingYuan-preset sessions by default, switchable to always show/hide and per-tab in Settings
package/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  - **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
14
14
  - **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
15
+ - **承诺口径进度**:只有领取的任务算进度;达成 = 承诺全部完成且没有待领取的任务——
16
+ 做完但有没领的,愿望停在「待收尾」等你决定(领了继续,或删掉就达成),不催也不代劳
15
17
  - **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
16
18
  - **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
17
19
  - **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
@@ -22,9 +22,11 @@ function WishView(props) {
22
22
  style: categoryVars(wish.wish.colorKey, wish.wish.categoryName),
23
23
  }, t('badge.wish')), createElement('span', { className: 'xy-title' }, deleted ? t('state.deleted', { title: wish.wish.title }) : wish.wish.title)),
24
24
  // 已删除卡不再渲染活体进度(删除态 × 进度百分比自相矛盾)
25
+ // 三态位(achieved/planning/settled/pendingCount)来自事件载荷(wishProgressFromAgg 派生),
26
+ // 与愿望页/工具回包同一称呼;旧事件无这些位则如实降级为纯「进度 X%」显示(不重建谓词,§5.2 规则 7)
25
27
  createElement('div', { className: 'xy-meta' }, deleted
26
28
  ? wish.wish.categoryName
27
- : `${wish.wish.categoryName} · ${t('wish.progress', { percent: wish.wish.progress })}`));
29
+ : `${wish.wish.categoryName} · ${wish.wish.achieved ? t('wish.achieved') : wish.wish.planning ? t('wish.planning') : t('wish.progress', { percent: wish.wish.progress })}${wish.wish.pendingCount ? ` · ${t('wish.pendingCount', { n: wish.wish.pendingCount })}` : ''}${wish.wish.settled ? ` · ${t('wish.settled')}` : ''}`));
28
30
  }
29
31
  function TaskView(props) {
30
32
  const t = useXyT();
@@ -81,12 +81,11 @@ export const CHART_SUBTITLE_KEYS = {
81
81
  '按任务 TopN': 'chart.subtitle.byTaskTopN',
82
82
  '按愿望 TopN': 'chart.subtitle.byWishTopN',
83
83
  '按实际打卡时刻分桶;过滤范围为打卡日区间': 'chart.subtitle.byHourBucket',
84
- '%(应打天数完成率)': 'chart.subtitle.progressPercent',
85
- '%(应打天数完成率,含未领取任务)': 'chart.subtitle.progressPercentPending',
84
+ '%(已领取任务完成率)': 'chart.subtitle.claimedPercent',
86
85
  };
87
86
  /**
88
87
  * 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
89
- * 含未领取任务)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
88
+ * 已领取任务口径)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
90
89
  */
91
90
  export function localizeChartSubtitle(subtitle) {
92
91
  const fixed = CHART_SUBTITLE_KEYS[subtitle];
@@ -95,9 +94,9 @@ export function localizeChartSubtitle(subtitle) {
95
94
  let m = /^近 (\d+) 天$/.exec(subtitle);
96
95
  if (m !== null)
97
96
  return t('chart.subtitle.lastDays', { n: m[1] });
98
- m = /^(\d+)\/(\d+) 天(含未领取任务)$/.exec(subtitle);
97
+ m = /^(\d+)\/(\d+) 天(已领取任务)$/.exec(subtitle);
99
98
  if (m !== null)
100
- return t('chart.subtitle.daysInclPending', { done: m[1], due: m[2] });
99
+ return t('chart.subtitle.daysClaimed', { done: m[1], due: m[2] });
101
100
  m = /^(\d+)\/(\d+) 天$/.exec(subtitle);
102
101
  if (m !== null)
103
102
  return t('chart.subtitle.daysRatio', { done: m[1], due: m[2] });
@@ -58,11 +58,18 @@ export const ZH_DICT = {
58
58
  'state.done': '已完成',
59
59
  // --- 愿望卡 / 愿望页 ---
60
60
  'wish.progress': '进度 {percent}%',
61
- 'wish.progressHasPending': '含未领取任务',
61
+ 'wish.planning': '计划中',
62
+ 'wish.achieved': '已达成 🎉',
63
+ 'wish.pendingCount': '{n} 个任务待领取',
64
+ 'wish.settled': '待收尾',
65
+ 'wish.settleHint': '答应自己的都做完了——待领取的领了继续,或删掉就达成',
62
66
  'wish.eta': '预计完成 {date}',
63
67
  'wish.noTasks': '暂无下属任务——在对话里告诉我,让我为它推荐打卡任务',
64
68
  'wish.pageTitle': '我的愿望',
65
- 'wish.summary': '{active} 个进行中{achieved}', 'wish.achievedSuffix': ' · {n} 个已达成',
69
+ 'wish.summary': '{active} 个进行中{planning}{settled}{achieved}',
70
+ 'wish.planningSuffix': ' · {n} 个计划中',
71
+ 'wish.settledSuffix': ' · {n} 个待收尾',
72
+ 'wish.achievedSuffix': ' · {n} 个已达成',
66
73
  'wish.sectionAchieved': '已达成',
67
74
  'wish.empty.title': '还没有愿望',
68
75
  'wish.empty.hint': '告诉我想实现什么,我来帮你拆解成可坚持的计划。',
@@ -343,10 +350,9 @@ export const ZH_DICT = {
343
350
  'chart.subtitle.byTaskTopN': '按任务 TopN',
344
351
  'chart.subtitle.byWishTopN': '按愿望 TopN',
345
352
  'chart.subtitle.byHourBucket': '按实际打卡时刻分桶;过滤范围为打卡日区间',
346
- 'chart.subtitle.progressPercent': '%(应打天数完成率)',
347
- 'chart.subtitle.progressPercentPending': '%(应打天数完成率,含未领取任务)',
353
+ 'chart.subtitle.claimedPercent': '%(已领取任务完成率)',
348
354
  'chart.subtitle.lastDays': '近 {n} 天',
349
- 'chart.subtitle.daysInclPending': '{done}/{due} 天(含未领取任务)',
355
+ 'chart.subtitle.daysClaimed': '{done}/{due} 天(已领取任务)',
350
356
  'chart.subtitle.daysRatio': '{done}/{due} 天',
351
357
  'chart.subtitle.streak': '当前 {current} 天 / 最长 {best} 天',
352
358
  // --- 详情聚合视图 ---
@@ -480,11 +486,17 @@ const EN_DICT = {
480
486
  'state.deleted': 'Deleted “{title}”',
481
487
  'state.done': 'Done',
482
488
  'wish.progress': 'Progress {percent}%',
483
- 'wish.progressHasPending': 'includes unclaimed tasks',
489
+ 'wish.planning': 'Planning',
490
+ 'wish.achieved': 'Achieved 🎉',
491
+ 'wish.pendingCount': '{n} to claim',
492
+ 'wish.settled': 'Wrapping up',
493
+ 'wish.settleHint': 'Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish',
484
494
  'wish.eta': 'Target date {date}',
485
495
  'wish.noTasks': 'No tasks yet — tell me in chat and I will suggest check-in tasks for it',
486
496
  'wish.pageTitle': 'My Wishes',
487
- 'wish.summary': '{active} in progress{achieved}',
497
+ 'wish.summary': '{active} in progress{planning}{settled}{achieved}',
498
+ 'wish.planningSuffix': ' · {n} planning',
499
+ 'wish.settledSuffix': ' · {n} wrapping up',
488
500
  'wish.achievedSuffix': ' · {n} achieved',
489
501
  'wish.sectionAchieved': 'Achieved',
490
502
  'wish.empty.title': 'No wishes yet',
@@ -751,10 +763,9 @@ const EN_DICT = {
751
763
  'chart.subtitle.byTaskTopN': 'By task (top N)',
752
764
  'chart.subtitle.byWishTopN': 'By wish (top N)',
753
765
  'chart.subtitle.byHourBucket': 'Bucketed by actual check-in time; filtered to the check-in date range',
754
- 'chart.subtitle.progressPercent': '% of scheduled-day completion',
755
- 'chart.subtitle.progressPercentPending': '% of scheduled-day completion (incl. unclaimed)',
766
+ 'chart.subtitle.claimedPercent': '% of committed-task completion',
756
767
  'chart.subtitle.lastDays': 'Last {n} days',
757
- 'chart.subtitle.daysInclPending': '{done}/{due} days (incl. unclaimed)',
768
+ 'chart.subtitle.daysClaimed': '{done}/{due} days (claimed tasks)',
758
769
  'chart.subtitle.daysRatio': '{done}/{due} days',
759
770
  'chart.subtitle.streak': 'Current {current}d / best {best}d',
760
771
  'detail.grid.title': 'Check-in record',
@@ -4,7 +4,7 @@ import { STYLE_TEXT } from './styles.js';
4
4
  import { CARD_VIEWS } from './cards.js';
5
5
  import { installTabVisibility } from './tab-visibility.js';
6
6
  import { SettingsSection } from './pages/settings.js';
7
- export const inject = ['slots', 'settingsScope', 'conversationEvents'];
7
+ export const inject = ['slots', 'settingsScope', 'uiConversation'];
8
8
  /** 事件类型 → 渲染 kind。 */
9
9
  const KIND_BY_EVENT = {
10
10
  'xingyuan/wish': 'xy-wish',
@@ -59,11 +59,12 @@ export function apply(ctx) {
59
59
  disposeToasts();
60
60
  disposeConfirms();
61
61
  });
62
- ctx.conversationEvents.register(definition('xy-wish'));
63
- ctx.conversationEvents.register(definition('xy-task'));
64
- ctx.conversationEvents.register(definition('xy-checkin'));
65
- ctx.conversationEvents.register(definition('xy-chart'));
66
- ctx.conversationEvents.register(definition('xy-micro'));
62
+ // dsh 0.1.2:事件 Definition 注册收进 uiConversation 服务的 events 子注册表。
63
+ ctx.uiConversation.events.register(definition('xy-wish'));
64
+ ctx.uiConversation.events.register(definition('xy-task'));
65
+ ctx.uiConversation.events.register(definition('xy-checkin'));
66
+ ctx.uiConversation.events.register(definition('xy-chart'));
67
+ ctx.uiConversation.events.register(definition('xy-micro'));
67
68
  // 卡片注册不声明 locale:t 席位由组件内 useXyT() 取代(键受查 + 订阅刷新),
68
69
  // 避免 'conversation' 窄域 t 与本插件命名空间的类型冲突。
69
70
  ctx.slots.inject('conversation.chat.node', () => [
@@ -39,8 +39,12 @@ export function WishesPage() {
39
39
  const data = page.data;
40
40
  if (data === undefined)
41
41
  return createElement(PageSkeleton);
42
- const active = data.wishes.filter((w) => !w.archived);
42
+ // 页头四态计数:进行中/计划中/待收尾/已达成各计各的,与卡头徽章同一称呼
43
+ // (消灭「摘要算进行中、卡上写计划中」的并见歧义);全部取服务端派生位,展示层不重建谓词
43
44
  const achieved = data.wishes.filter((w) => w.archived);
45
+ const settledCount = data.wishes.filter((w) => w.settled).length;
46
+ const planningCount = data.wishes.filter((w) => !w.archived && w.planning).length;
47
+ const active = data.wishes.filter((w) => !w.archived);
44
48
  /** 删除愿望(级联下属任务与打卡记录,服务端同一写路径):确认后直连动作并整页刷新。
45
49
  * 行移除后触发按钮销毁、焦点落空——刷新完成后把焦点交给页面标题兜底。 */
46
50
  const removeWish = (wish) => {
@@ -70,37 +74,45 @@ export function WishesPage() {
70
74
  ? [line, createElement(TaskDetailPanel, { key: `d-${task.taskId}`, id: `xy-task-detail-${task.taskId}`, taskId: task.taskId, today: data.today, onChanged: page.reload })]
71
75
  : [line];
72
76
  });
73
- const card = (wish) => createElement('div', { key: wish.wishId, className: 'xy-wishcard' }, createElement('div', { className: 'xy-card-head' }, createElement('span', {
74
- className: 'xy-badge xy-badge-cat',
75
- style: categoryVars(wish.colorKey, wish.categoryName),
76
- }, wish.categoryName), createElement('span', { className: 'xy-title' }, wish.title),
77
- // 长期尺度 = 计划口径(分母含未领取任务的应打天数,§5.2 规则 7):愿望含未领取
78
- // 任务时必须呈现「含未领取任务」标注,避免把计划分母误读为承诺分母
79
- createElement('span', { className: 'xy-progress-num' }, t('wish.progress', { percent: wish.progress }), wish.tasks.some((task) => task.status === 'pending')
80
- ? createElement('span', { className: 'xy-meta' }, ` · ${t('wish.progressHasPending')}`)
81
- : null), createElement('button', {
82
- className: 'xy-btn xy-btn-danger xy-btn-icon',
83
- disabled: deleting,
84
- 'aria-label': t('common.delete') + ' · ' + wish.title,
85
- title: t('common.delete'),
86
- onClick: () => removeWish(wish),
87
- }, createElement(IconTrash))), wish.description !== undefined ? createElement('div', { className: 'xy-meta' }, wish.description) : null, wish.estimatedCompletionDate !== undefined
88
- ? createElement('div', { className: 'xy-meta' }, t('wish.eta', { date: formatMediumDate(wish.estimatedCompletionDate) }))
89
- : null,
90
- // 与今日页 hero 进度条同一语义语法:读屏可感知进度(role+valuenow),不只是一根哑条
91
- createElement('div', {
92
- className: 'xy-bar',
93
- role: 'progressbar',
94
- 'aria-valuemin': 0,
95
- 'aria-valuemax': 100,
96
- 'aria-valuenow': wish.progress,
97
- 'aria-label': t('wish.progress', { percent: wish.progress })
98
- + (wish.tasks.some((task) => task.status === 'pending') ? ` · ${t('wish.progressHasPending')}` : ''),
99
- }, createElement('div', { className: 'xy-bar-fill', style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` } })), wish.tasks.length > 0
100
- ? createElement('div', { className: 'xy-wishtasks' }, ...wishTasks(wish))
101
- : createElement('div', { className: 'xy-meta' }, t('wish.noTasks')));
77
+ const card = (wish) => {
78
+ // 承诺口径(§5.2 规则 7):进度分母只计已领取任务。展示三态:
79
+ // 计划中(无已领取任务)/ 进行中(正常百分比)/ 待结算(满进度而有候选——不归档,附收尾指引)。
80
+ // settled/pendingCount/planning 直取服务端 wishProgressFromAgg 派生位(随 /api/wishes 下发),
81
+ // 展示层禁止重建 `progress>=100 && 有候选` 闸门或 planning 谓词(单一判定源,防与工具回包分叉)
82
+ const { pendingCount, settled, planning } = wish;
83
+ const progressLabel = planning ? t('wish.planning') : t('wish.progress', { percent: wish.progress });
84
+ const pendingMeta = pendingCount > 0 ? ` · ${t('wish.pendingCount', { n: pendingCount })}` : '';
85
+ return createElement('div', { key: wish.wishId, className: 'xy-wishcard' }, createElement('div', { className: 'xy-card-head' }, createElement('span', {
86
+ className: 'xy-badge xy-badge-cat',
87
+ style: categoryVars(wish.colorKey, wish.categoryName),
88
+ }, wish.categoryName), createElement('span', { className: 'xy-title' }, wish.title), createElement('span', { className: 'xy-progress-num' }, progressLabel, pendingCount > 0
89
+ ? createElement('span', { className: 'xy-meta' }, pendingMeta)
90
+ : null), createElement('button', {
91
+ className: 'xy-btn xy-btn-danger xy-btn-icon',
92
+ disabled: deleting,
93
+ 'aria-label': t('common.delete') + ' · ' + wish.title,
94
+ title: t('common.delete'),
95
+ onClick: () => removeWish(wish),
96
+ }, createElement(IconTrash))), wish.description !== undefined ? createElement('div', { className: 'xy-meta' }, wish.description) : null, wish.estimatedCompletionDate !== undefined
97
+ ? createElement('div', { className: 'xy-meta' }, t('wish.eta', { date: formatMediumDate(wish.estimatedCompletionDate) }))
98
+ : null,
99
+ // 与今日页 hero 进度条同一语义语法:读屏可感知进度(role+valuenow),不只是一根哑条
100
+ createElement('div', {
101
+ className: 'xy-bar',
102
+ role: 'progressbar',
103
+ 'aria-valuemin': 0,
104
+ 'aria-valuemax': 100,
105
+ 'aria-valuenow': wish.progress,
106
+ 'aria-label': progressLabel + pendingMeta,
107
+ }, createElement('div', { className: 'xy-bar-fill', style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` } })), settled ? createElement('div', { className: 'xy-meta' }, t('wish.settleHint')) : null, wish.tasks.length > 0
108
+ ? createElement('div', { className: 'xy-wishtasks' }, ...wishTasks(wish))
109
+ : createElement('div', { className: 'xy-meta' }, t('wish.noTasks')));
110
+ };
102
111
  return createElement('div', { className: 'xy-page', ref: stabilize }, page.error !== undefined ? createElement(StaleBanner, { onRetry: () => void page.reload() }) : null, showNoPresetHint ? createElement('p', { className: 'xy-hint' }, t('today.noPresetHint')) : null, createElement('div', { className: 'xy-page-head' }, createElement('h2', { className: 'xy-page-title' }, t('wish.pageTitle')), createElement('span', { className: 'xy-meta' }, t('wish.summary', {
103
- active: active.length,
112
+ // 四态计数各计各的(进行中扣除计划中与待收尾,不相套);列表渲染仍归未完成组(未完成≠进行中计数)
113
+ active: active.length - settledCount - planningCount,
114
+ planning: planningCount > 0 ? t('wish.planningSuffix', { n: planningCount }) : '',
115
+ settled: settledCount > 0 ? t('wish.settledSuffix', { n: settledCount }) : '',
104
116
  achieved: achieved.length > 0 ? t('wish.achievedSuffix', { n: achieved.length }) : '',
105
117
  })),
106
118
  // 动作组整体右置(单一 margin-left:auto),避免多按钮各自 auto margin 平分剩余空间
@@ -33,7 +33,7 @@ export function installTabVisibility(ctx) {
33
33
  const hidden = value?.hiddenTabs ?? TAB_VISIBILITY_DEFAULTS.hiddenTabs;
34
34
  const list = sessions?.list?.getSnapshot();
35
35
  const row = list?.current !== undefined ? list.byId[list.current] : undefined;
36
- const isXingyuan = row?.agentPreset === 'xingyuan';
36
+ const isXingyuan = row?.projectionValues?.agentPreset === 'xingyuan';
37
37
  // 「始终显示 × 非星愿会话」的今日页轻提示(Q4=b 结论),其余模式不提示
38
38
  setTodayHint(mode === 'show' && !isXingyuan);
39
39
  const visible = new Set(visibleTabIds(mode, hidden, isXingyuan));
package/lib/client.js CHANGED
@@ -61,11 +61,17 @@ const ZH_DICT = {
61
61
  "state.deleted": "已删除「{title}」",
62
62
  "state.done": "已完成",
63
63
  "wish.progress": "进度 {percent}%",
64
- "wish.progressHasPending": "含未领取任务",
64
+ "wish.planning": "计划中",
65
+ "wish.achieved": "已达成 🎉",
66
+ "wish.pendingCount": "{n} 个任务待领取",
67
+ "wish.settled": "待收尾",
68
+ "wish.settleHint": "答应自己的都做完了——待领取的领了继续,或删掉就达成",
65
69
  "wish.eta": "预计完成 {date}",
66
70
  "wish.noTasks": "暂无下属任务——在对话里告诉我,让我为它推荐打卡任务",
67
71
  "wish.pageTitle": "我的愿望",
68
- "wish.summary": "{active} 个进行中{achieved}",
72
+ "wish.summary": "{active} 个进行中{planning}{settled}{achieved}",
73
+ "wish.planningSuffix": " · {n} 个计划中",
74
+ "wish.settledSuffix": " · {n} 个待收尾",
69
75
  "wish.achievedSuffix": " · {n} 个已达成",
70
76
  "wish.sectionAchieved": "已达成",
71
77
  "wish.empty.title": "还没有愿望",
@@ -332,10 +338,9 @@ const ZH_DICT = {
332
338
  "chart.subtitle.byTaskTopN": "按任务 TopN",
333
339
  "chart.subtitle.byWishTopN": "按愿望 TopN",
334
340
  "chart.subtitle.byHourBucket": "按实际打卡时刻分桶;过滤范围为打卡日区间",
335
- "chart.subtitle.progressPercent": "%(应打天数完成率)",
336
- "chart.subtitle.progressPercentPending": "%(应打天数完成率,含未领取任务)",
341
+ "chart.subtitle.claimedPercent": "%(已领取任务完成率)",
337
342
  "chart.subtitle.lastDays": "近 {n} 天",
338
- "chart.subtitle.daysInclPending": "{done}/{due} 天(含未领取任务)",
343
+ "chart.subtitle.daysClaimed": "{done}/{due} 天(已领取任务)",
339
344
  "chart.subtitle.daysRatio": "{done}/{due} 天",
340
345
  "chart.subtitle.streak": "当前 {current} 天 / 最长 {best} 天",
341
346
  "detail.grid.title": "打卡记录",
@@ -462,11 +467,17 @@ const EN_DICT = {
462
467
  "state.deleted": "Deleted “{title}”",
463
468
  "state.done": "Done",
464
469
  "wish.progress": "Progress {percent}%",
465
- "wish.progressHasPending": "includes unclaimed tasks",
470
+ "wish.planning": "Planning",
471
+ "wish.achieved": "Achieved 🎉",
472
+ "wish.pendingCount": "{n} to claim",
473
+ "wish.settled": "Wrapping up",
474
+ "wish.settleHint": "Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish",
466
475
  "wish.eta": "Target date {date}",
467
476
  "wish.noTasks": "No tasks yet — tell me in chat and I will suggest check-in tasks for it",
468
477
  "wish.pageTitle": "My Wishes",
469
- "wish.summary": "{active} in progress{achieved}",
478
+ "wish.summary": "{active} in progress{planning}{settled}{achieved}",
479
+ "wish.planningSuffix": " · {n} planning",
480
+ "wish.settledSuffix": " · {n} wrapping up",
470
481
  "wish.achievedSuffix": " · {n} achieved",
471
482
  "wish.sectionAchieved": "Achieved",
472
483
  "wish.empty.title": "No wishes yet",
@@ -733,10 +744,9 @@ const EN_DICT = {
733
744
  "chart.subtitle.byTaskTopN": "By task (top N)",
734
745
  "chart.subtitle.byWishTopN": "By wish (top N)",
735
746
  "chart.subtitle.byHourBucket": "Bucketed by actual check-in time; filtered to the check-in date range",
736
- "chart.subtitle.progressPercent": "% of scheduled-day completion",
737
- "chart.subtitle.progressPercentPending": "% of scheduled-day completion (incl. unclaimed)",
747
+ "chart.subtitle.claimedPercent": "% of committed-task completion",
738
748
  "chart.subtitle.lastDays": "Last {n} days",
739
- "chart.subtitle.daysInclPending": "{done}/{due} days (incl. unclaimed)",
749
+ "chart.subtitle.daysClaimed": "{done}/{due} days (claimed tasks)",
740
750
  "chart.subtitle.daysRatio": "{done}/{due} days",
741
751
  "chart.subtitle.streak": "Current {current}d / best {best}d",
742
752
  "detail.grid.title": "Check-in record",
@@ -2107,20 +2117,19 @@ const CHART_SUBTITLE_KEYS = {
2107
2117
  "按任务 TopN": "chart.subtitle.byTaskTopN",
2108
2118
  "按愿望 TopN": "chart.subtitle.byWishTopN",
2109
2119
  "按实际打卡时刻分桶;过滤范围为打卡日区间": "chart.subtitle.byHourBucket",
2110
- "%(应打天数完成率)": "chart.subtitle.progressPercent",
2111
- "%(应打天数完成率,含未领取任务)": "chart.subtitle.progressPercentPending"
2120
+ "%(已领取任务完成率)": "chart.subtitle.claimedPercent"
2112
2121
  };
2113
2122
  /**
2114
2123
  * 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
2115
- * 含未领取任务)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
2124
+ * 已领取任务口径)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
2116
2125
  */
2117
2126
  function localizeChartSubtitle(subtitle) {
2118
2127
  const fixed = CHART_SUBTITLE_KEYS[subtitle];
2119
2128
  if (fixed !== void 0) return t(fixed);
2120
2129
  let m = /^近 (\d+) 天$/.exec(subtitle);
2121
2130
  if (m !== null) return t("chart.subtitle.lastDays", { n: m[1] });
2122
- m = /^(\d+)\/(\d+) 天(含未领取任务)$/.exec(subtitle);
2123
- if (m !== null) return t("chart.subtitle.daysInclPending", {
2131
+ m = /^(\d+)\/(\d+) 天(已领取任务)$/.exec(subtitle);
2132
+ if (m !== null) return t("chart.subtitle.daysClaimed", {
2124
2133
  done: m[1],
2125
2134
  due: m[2]
2126
2135
  });
@@ -2392,7 +2401,7 @@ function WishView(props) {
2392
2401
  return (0, react.createElement)("div", { className: `xy-card ${deleted ? "xy-deleted" : ""}` }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
2393
2402
  className: "xy-badge xy-badge-cat",
2394
2403
  style: categoryVars(wish.wish.colorKey, wish.wish.categoryName)
2395
- }, t("badge.wish")), (0, react.createElement)("span", { className: "xy-title" }, deleted ? t("state.deleted", { title: wish.wish.title }) : wish.wish.title)), (0, react.createElement)("div", { className: "xy-meta" }, deleted ? wish.wish.categoryName : `${wish.wish.categoryName} · ${t("wish.progress", { percent: wish.wish.progress })}`));
2404
+ }, t("badge.wish")), (0, react.createElement)("span", { className: "xy-title" }, deleted ? t("state.deleted", { title: wish.wish.title }) : wish.wish.title)), (0, react.createElement)("div", { className: "xy-meta" }, deleted ? wish.wish.categoryName : `${wish.wish.categoryName} · ${wish.wish.achieved ? t("wish.achieved") : wish.wish.planning ? t("wish.planning") : t("wish.progress", { percent: wish.wish.progress })}${wish.wish.pendingCount ? ` · ${t("wish.pendingCount", { n: wish.wish.pendingCount })}` : ""}${wish.wish.settled ? ` · ${t("wish.settled")}` : ""}`));
2396
2405
  }
2397
2406
  function TaskView(props) {
2398
2407
  const t = useXyT();
@@ -3660,8 +3669,10 @@ function WishesPage() {
3660
3669
  });
3661
3670
  const data = page.data;
3662
3671
  if (data === void 0) return (0, react.createElement)(PageSkeleton);
3663
- const active = data.wishes.filter((w) => !w.archived);
3664
3672
  const achieved = data.wishes.filter((w) => w.archived);
3673
+ const settledCount = data.wishes.filter((w) => w.settled).length;
3674
+ const planningCount = data.wishes.filter((w) => !w.archived && w.planning).length;
3675
+ const active = data.wishes.filter((w) => !w.archived);
3665
3676
  /** 删除愿望(级联下属任务与打卡记录,服务端同一写路径):确认后直连动作并整页刷新。
3666
3677
  * 行移除后触发按钮销毁、焦点落空——刷新完成后把焦点交给页面标题兜底。 */
3667
3678
  const removeWish = (wish) => {
@@ -3696,34 +3707,41 @@ function WishesPage() {
3696
3707
  onChanged: page.reload
3697
3708
  })] : [line];
3698
3709
  });
3699
- const card = (wish) => (0, react.createElement)("div", {
3700
- key: wish.wishId,
3701
- className: "xy-wishcard"
3702
- }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
3703
- className: "xy-badge xy-badge-cat",
3704
- style: categoryVars(wish.colorKey, wish.categoryName)
3705
- }, wish.categoryName), (0, react.createElement)("span", { className: "xy-title" }, wish.title), (0, react.createElement)("span", { className: "xy-progress-num" }, t("wish.progress", { percent: wish.progress }), wish.tasks.some((task) => task.status === "pending") ? (0, react.createElement)("span", { className: "xy-meta" }, ` · ${t("wish.progressHasPending")}`) : null), (0, react.createElement)("button", {
3706
- className: "xy-btn xy-btn-danger xy-btn-icon",
3707
- disabled: deleting,
3708
- "aria-label": t("common.delete") + " · " + wish.title,
3709
- title: t("common.delete"),
3710
- onClick: () => removeWish(wish)
3711
- }, (0, react.createElement)(IconTrash))), wish.description !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, wish.description) : null, wish.estimatedCompletionDate !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.eta", { date: formatMediumDate(wish.estimatedCompletionDate) })) : null, (0, react.createElement)("div", {
3712
- className: "xy-bar",
3713
- role: "progressbar",
3714
- "aria-valuemin": 0,
3715
- "aria-valuemax": 100,
3716
- "aria-valuenow": wish.progress,
3717
- "aria-label": t("wish.progress", { percent: wish.progress }) + (wish.tasks.some((task) => task.status === "pending") ? ` · ${t("wish.progressHasPending")}` : "")
3718
- }, (0, react.createElement)("div", {
3719
- className: "xy-bar-fill",
3720
- style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` }
3721
- })), wish.tasks.length > 0 ? (0, react.createElement)("div", { className: "xy-wishtasks" }, ...wishTasks(wish)) : (0, react.createElement)("div", { className: "xy-meta" }, t("wish.noTasks")));
3710
+ const card = (wish) => {
3711
+ const { pendingCount, settled, planning } = wish;
3712
+ const progressLabel = planning ? t("wish.planning") : t("wish.progress", { percent: wish.progress });
3713
+ const pendingMeta = pendingCount > 0 ? ` · ${t("wish.pendingCount", { n: pendingCount })}` : "";
3714
+ return (0, react.createElement)("div", {
3715
+ key: wish.wishId,
3716
+ className: "xy-wishcard"
3717
+ }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
3718
+ className: "xy-badge xy-badge-cat",
3719
+ style: categoryVars(wish.colorKey, wish.categoryName)
3720
+ }, wish.categoryName), (0, react.createElement)("span", { className: "xy-title" }, wish.title), (0, react.createElement)("span", { className: "xy-progress-num" }, progressLabel, pendingCount > 0 ? (0, react.createElement)("span", { className: "xy-meta" }, pendingMeta) : null), (0, react.createElement)("button", {
3721
+ className: "xy-btn xy-btn-danger xy-btn-icon",
3722
+ disabled: deleting,
3723
+ "aria-label": t("common.delete") + " · " + wish.title,
3724
+ title: t("common.delete"),
3725
+ onClick: () => removeWish(wish)
3726
+ }, (0, react.createElement)(IconTrash))), wish.description !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, wish.description) : null, wish.estimatedCompletionDate !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.eta", { date: formatMediumDate(wish.estimatedCompletionDate) })) : null, (0, react.createElement)("div", {
3727
+ className: "xy-bar",
3728
+ role: "progressbar",
3729
+ "aria-valuemin": 0,
3730
+ "aria-valuemax": 100,
3731
+ "aria-valuenow": wish.progress,
3732
+ "aria-label": progressLabel + pendingMeta
3733
+ }, (0, react.createElement)("div", {
3734
+ className: "xy-bar-fill",
3735
+ style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` }
3736
+ })), settled ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.settleHint")) : null, wish.tasks.length > 0 ? (0, react.createElement)("div", { className: "xy-wishtasks" }, ...wishTasks(wish)) : (0, react.createElement)("div", { className: "xy-meta" }, t("wish.noTasks")));
3737
+ };
3722
3738
  return (0, react.createElement)("div", {
3723
3739
  className: "xy-page",
3724
3740
  ref: stabilize
3725
3741
  }, page.error !== void 0 ? (0, react.createElement)(StaleBanner, { onRetry: () => void page.reload() }) : null, showNoPresetHint ? (0, react.createElement)("p", { className: "xy-hint" }, t("today.noPresetHint")) : null, (0, react.createElement)("div", { className: "xy-page-head" }, (0, react.createElement)("h2", { className: "xy-page-title" }, t("wish.pageTitle")), (0, react.createElement)("span", { className: "xy-meta" }, t("wish.summary", {
3726
- active: active.length,
3742
+ active: active.length - settledCount - planningCount,
3743
+ planning: planningCount > 0 ? t("wish.planningSuffix", { n: planningCount }) : "",
3744
+ settled: settledCount > 0 ? t("wish.settledSuffix", { n: settledCount }) : "",
3727
3745
  achieved: achieved.length > 0 ? t("wish.achievedSuffix", { n: achieved.length }) : ""
3728
3746
  })), (0, react.createElement)("div", { className: "xy-page-actions" }, (0, react.createElement)("button", {
3729
3747
  className: "xy-btn",
@@ -4732,7 +4750,7 @@ function installTabVisibility(ctx) {
4732
4750
  const mode = value?.tabVisibilityMode ?? TAB_VISIBILITY_DEFAULTS.tabVisibilityMode;
4733
4751
  const hidden = value?.hiddenTabs ?? TAB_VISIBILITY_DEFAULTS.hiddenTabs;
4734
4752
  const list = sessions?.list?.getSnapshot();
4735
- const isXingyuan = (list?.current !== void 0 ? list.byId[list.current] : void 0)?.agentPreset === "xingyuan";
4753
+ const isXingyuan = (list?.current !== void 0 ? list.byId[list.current] : void 0)?.projectionValues?.agentPreset === "xingyuan";
4736
4754
  setTodayHint(mode === "show" && !isXingyuan);
4737
4755
  const visible = new Set(visibleTabIds(mode, hidden, isXingyuan));
4738
4756
  for (const entry of TAB_ENTRIES) {
@@ -5151,7 +5169,7 @@ function SettingsSection(props) {
5151
5169
  const inject = [
5152
5170
  "slots",
5153
5171
  "settingsScope",
5154
- "conversationEvents"
5172
+ "uiConversation"
5155
5173
  ];
5156
5174
  /** 事件类型 → 渲染 kind。 */
5157
5175
  const KIND_BY_EVENT = {
@@ -5205,11 +5223,11 @@ function apply(ctx) {
5205
5223
  disposeToasts();
5206
5224
  disposeConfirms();
5207
5225
  });
5208
- ctx.conversationEvents.register(definition("xy-wish"));
5209
- ctx.conversationEvents.register(definition("xy-task"));
5210
- ctx.conversationEvents.register(definition("xy-checkin"));
5211
- ctx.conversationEvents.register(definition("xy-chart"));
5212
- ctx.conversationEvents.register(definition("xy-micro"));
5226
+ ctx.uiConversation.events.register(definition("xy-wish"));
5227
+ ctx.uiConversation.events.register(definition("xy-task"));
5228
+ ctx.uiConversation.events.register(definition("xy-checkin"));
5229
+ ctx.uiConversation.events.register(definition("xy-chart"));
5230
+ ctx.uiConversation.events.register(definition("xy-micro"));
5213
5231
  ctx.slots.inject("conversation.chat.node", () => [
5214
5232
  ctx.slots.register({
5215
5233
  name: "conversation.chat.node",
package/lib/growth.js CHANGED
@@ -117,7 +117,9 @@ export function resolveLevel(totalExperience) {
117
117
  export function growthSummary(store, today = todayIso()) {
118
118
  // 单遍任务索引批量新鲜化(freshWishes),替代逐愿望嵌套全表扫描 O(W×T)
119
119
  const wishes = freshWishes(store);
120
- const completedWishes = wishes.filter((wish) => wish.progress >= 100).length;
120
+ // 达成口径与愿望卡一致(freshWishes 派生的 archived 谓词):承诺全部完成且无未处理候选——
121
+ // 禁止在此重复写 progress >= 100 谓词(候选拦达成会漏掉,见 §5.2 规则 7)
122
+ const completedWishes = wishes.filter((wish) => wish.archived).length;
121
123
  let totalTasks = 0;
122
124
  let completedTasks = 0;
123
125
  for (const [, task] of store.domain.table('tasks').entries()) {
@@ -1,8 +1,7 @@
1
1
  import z from '@deepseek-ai/schemastery';
2
- import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
3
2
  import { CONFIRM_LANGS, MEMORY_LIMIT_MAX, MEMORY_LIMIT_MIN, PREF_DEFAULTS, } from './pref-policy.js';
4
- /** 对话偏好命名空间(client 半侧 settingsScope 同名配对,勿改)。 */
5
- export const PREF_NS = settingsNamespace('xingyuan-pref');
3
+ /** 对话偏好命名空间(client 半侧 settingsScope 同名配对,勿改;小写连字符文法由新 API 类型校验)。 */
4
+ export const PREF_NS = 'xingyuan-pref';
6
5
  /** 配置 schema(默认值写进 schema;step(1) 让服务端也拒绝小数,防手改文档/RPC 直写)。 */
7
6
  export const PrefSettingsSchema = z.object({
8
7
  confirmWrites: z.boolean().default(PREF_DEFAULTS.confirmWrites),
@@ -20,10 +19,15 @@ export const PrefSettingsSchema = z.object({
20
19
  export function installPrefSettings(ctx) {
21
20
  // 闭包局部,非模块级单例(AGENTS.md §8:不做任何跨重载的模块级单例状态)
22
21
  let read = () => PREF_DEFAULTS;
23
- installSettingsSection(ctx, PREF_NS, PrefSettingsSchema, PREF_DEFAULTS, {
24
- setSource: (current) => { read = current; },
25
- // 消费方(工具/提示词)每次执行即时读 thunk,无需在此重建任何东西
26
- onChange: () => { },
22
+ // 回调形参 inv 才是声明了 settings 依赖的上下文:cordis 4.0.2 起,
23
+ // 未在当前 fiber 声明的服务属性不得访问(外层 ctx 直接读 ctx.settings 会
24
+ // 抛 "cannot get property without inject"),注册归属(owner)仍传插件 fiber。
25
+ ctx.inject(['settings'], (inv) => {
26
+ inv.settings.installSection(ctx, PREF_NS, PrefSettingsSchema, PREF_DEFAULTS, {
27
+ setSource: (current) => { read = current; },
28
+ // 消费方(工具/提示词)每次执行即时读 thunk,无需在此重建任何东西
29
+ onChange: () => { },
30
+ });
27
31
  });
28
32
  return () => read();
29
33
  }