@starwish-ai/xingyuan-dsh 0.5.5 → 0.5.7

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,200 @@ 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.5.7] - 2026-08-29
9
+
10
+ > **Note:** this release is the third six-perspective quality pass (product,
11
+ > design, end-user, dsh-plugin engineering, architecture, UX). It closes the
12
+ > last honesty gap in the write paths, makes tab switching lossless for
13
+ > in-progress input, hardens the opportunity-date calculator against a
14
+ > process-hanging edge case, and lands the accessibility/contrast leftovers.
15
+ > No data format changes; existing databases and recorded sessions replay as before.
16
+
17
+ ### Added
18
+ - **Draft snapshots across tab switches**: typing a memory (up to 1000 chars)
19
+ or filling the quick-create forms used to be silently lost when switching
20
+ conversation view tabs (pages unmount on switch). Form drafts now survive tab
21
+ switches and panel collapse; successful saves clear them.
22
+ - **Instant growth feedback on check-in**: the check-in tool reply appends the
23
+ current level and experience progress, closing the "check in → grow" loop at
24
+ the moment of the action instead of deferring it to the Growth page.
25
+ - **Non-preset session hint on the chat-directed pages**: in "always show" mode the six tabs
26
+ render in every session, but wishes/tasks/memory empty states directed users
27
+ to a chat that has no XingYuan tools there. The Today page's "preset not
28
+ enabled" hint row now also renders on the other three pages.
29
+ - **HTTP shell tests**: route dispatch (status-code mapping, the 64 KB limit,
30
+ JSON parsing, the `ToolError.code` passthrough that powers client-side error
31
+ localization) had zero coverage; locked with mocked `IncomingMessage`/
32
+ `ServerResponse` in `test/routes-shell.test.ts`.
33
+
34
+ ### Changed
35
+ - **Claiming an overdue unclaimed task is rejected, not silently closed**
36
+ (`claim_expired`): the old flow claimed it and immediately expired-closed it
37
+ while the tool reply asserted "claimed, now in progress" and the page toast
38
+ said "go check in" — both false, and the task-line button kept offering an
39
+ action that always fails. The row shows a guidance hint instead, the chat
40
+ task card hides its claim button for the same state, and the detail panel's
41
+ revive row (extend due date) now also covers overdue-unclaimed tasks.
42
+ - **The early check-in confirms no longer assert a falsehood**: the task-row,
43
+ card and detail dialogs said "today is not a check-in day" — false when today
44
+ was already checked in (the natural path: check in via chat, then tap the
45
+ button on the updated card); the calendar dialog said "{date} is not a
46
+ check-in day" for a day the panel only lists because it IS one. All four now
47
+ state only what is true: the target day is after today, and an early check-in
48
+ commits to finishing that day.
49
+ - **Growth-ladder and completed rows dim by text color, not row opacity**:
50
+ whole-row `opacity` (0.65-0.78) blended 12 px text below the WCAG AA 4.5:1
51
+ line (≈2.6-3.1:1 measured in both themes); the hierarchy now comes from the
52
+ secondary text color (4.76/5.15:1) while transparency stays on decorative
53
+ elements only.
54
+ - **Danger icon buttons share one quiet tier**: the memory-row delete joined
55
+ the wish-card delete at 0.78 idle opacity with hover/focus restore (long
56
+ lists stop glowing red; non-text contrast ≥3:1, the stale "≥4.5:1" comment
57
+ corrected to the non-text standard).
58
+ - **Growth chart stops squeezing its columns on narrow panels**: below the
59
+ 520 px breakpoint the 30 daily columns scroll horizontally at a minimum
60
+ width (~13 px per column, up from ~10 px) instead of being flex-crushed;
61
+ a deliberate trade-off against the 24 px touch-target ideal, whose full
62
+ width would make the chart scroll several screens.
63
+ - **HITL confirm sorts callers by whether any confirm UI exists**: callers
64
+ with no confirm UI at all (`NO_PROVIDER`, `CALLER_NOT_LIVE` — both raised
65
+ before `NO_PROVIDER` in rc.2) are allowed, as before for headless; a
66
+ delegated caller (`DELEGATED_CALLER`, e.g. a session-spawned subagent) is
67
+ now failed closed with actionable guidance ("confirm in the main
68
+ conversation, then run it there") instead of the raw platform error —
69
+ keeping the always-on delete confirmation intact on delegated paths.
70
+ Recorded in the engineering doc's HITL contract section.
71
+ - **weekly/monthly expectation management**: tool descriptions and the task
72
+ guide now state that opportunity days run from the claim date (every 7 days /
73
+ monthly by claim date) and never bind to a weekday — the model must say so
74
+ instead of silently accepting "every Wednesday" requests. Recorded in Known
75
+ Limitations.
76
+ - **The recommendation tool states its ordering**: `get_recommended_tasks` is
77
+ a fixed "top 5 by historical check-in count" ranking, not curation;
78
+ description and reply now say so.
79
+ - **Dedup guidance covers paraphrase**: the wish/task guides instruct the model
80
+ to sweep the existing list semantically when substring dedup finds no hit but
81
+ the request is loosely phrased.
82
+ - **Prompt/UI drift sync**: the Tasks page has four status groups (not three)
83
+ and two more settings entries (confirm-card language, tab visibility) — the
84
+ capabilities section now matches what users see.
85
+ - **Copy/wording**: user-facing strings unify "机会日" to "打卡日" (the check-in
86
+ tool guidance already used the plain term; leftover error strings swept
87
+ too); the bare-wish card points to the
88
+ chat path for task suggestions; the task-created toast explains "claim it to
89
+ start checking in"; five English strings polished ("To check in", "entries",
90
+ "Check-ins · last 30 days", "saved when you leave the field", the grid
91
+ summary).
92
+
93
+ ### Fixed
94
+ - **Opportunity-date loops can no longer hang the process**: termination used
95
+ ISO string comparison, which silently assumes four-digit years — a sequence
96
+ crossing 9999-12-31 (only reachable via hand-edited sqlite) compared
97
+ `'10000-xx' <= '9999-xx'` as true forever, hanging daily/weekly/monthly in an
98
+ allocating loop. Termination is now numeric (UTC day numbers; the monthly
99
+ branch via `Date.UTC`, since `Date.parse('10000-…')` returns `NaN`). Locked
100
+ by termination tests including the year-boundary crossing.
101
+ - **Toast glyph semantics & screen readers**: the info glyph was "★" — the star
102
+ is the high-importance memory marker (`.xy-star-hi`); info is now "i", and
103
+ toast glyphs are `aria-hidden` so decorative symbols are not announced inside
104
+ the live region.
105
+ - **Centered header baselines**: the `.xy-meta` top margin leaked into centered
106
+ card/page heads, sinking secondary text by 2 px; the treatment the detail
107
+ area already had now covers the heads too.
108
+ - **Focus management**: closing the category rename editor returns focus (to
109
+ the row's rename button, or the page title when the row was rebuilt by a
110
+ rename); finishing the last "load more" page moves focus to the "all shown"
111
+ line and announces it (`role="status"`).
112
+ - **Docs**: reference links pointed at the nonexistent `main` branch of the
113
+ upstream repository (the default branch is `master`) and at a cookbook page
114
+ that does not exist (the conversation-node contract lives in
115
+ `subsystems/conversation`); a dsh upgrade checklist now records the two
116
+ patch-DSL hazards (schedule insert id collision, storage-domain whole-line
117
+ replacement).
118
+
119
+ ## [0.5.6] - 2026-08-29
120
+
121
+ > **Note:** this release is the follow-up to the v0.5.5 six-perspective quality
122
+ > pass: the same six reviews re-ran against 0.5.5 and every accepted finding is
123
+ > fixed here — one data-safety gate, honesty fixes in the growth ladder, focus
124
+ > management on the highest-frequency action, and a batch of contrast, copy and
125
+ > localization closures. No data format changes; existing databases and recorded
126
+ > sessions replay as before.
127
+
128
+ ### Added
129
+ - **Write-path schema gate** (`makeXingyuanStore`): the storage domain validates
130
+ records only when cold-opening the medium — a write path that skipped a field
131
+ check (e.g. an over-long task name from the model) used to persist silently and
132
+ then brick the next startup (`invalid-record` refuses to open, plugin dead until
133
+ manual surgery). Every `put`/`update`/`global.set` is now guarded by the declared
134
+ zod schema at the single store assembly point and rejected at write time with a
135
+ stable `invalid_record` code; valid records parse as identity, reads pay nothing.
136
+ Locked by `test/domain-guard.test.ts`.
137
+ - **Network-failure copy**: connection-level fetch failures (dsh restarting) no
138
+ longer surface the browser's raw English (`Failed to fetch` / `Load failed`) in
139
+ the Chinese UI; they map to a localized "network request failed, retry" message.
140
+ - **Task rows keep keyboard focus after group-migrating actions**: a successful
141
+ check-in moves the row between the Today page's open/done lists and destroyed the
142
+ pressed button, dropping focus to `<body>`; focus now falls back to the page
143
+ title (the same pattern as undo). The same treatment covers a check-in that
144
+ closes a task anywhere task rows render, and claiming a task on the Tasks page,
145
+ where the row moves between status groups.
146
+ - **Memory page**: the search empty-state no longer flashes between keystrokes
147
+ while the debounce is pending, a settled zero-hit search now shows the "no
148
+ matches" copy instead of the "no memories yet" empty-library one (the server's
149
+ `total` is query-filtered, so the two were indistinguishable before), and the
150
+ key field's server minimum (2 characters) is validated inline with its guidance
151
+ message instead of a generic error.
152
+
153
+ ### Changed
154
+ - **Level rewards describe what exists**: the growth ladder no longer promises
155
+ avatar frames, task templates or style gating that the product does not have
156
+ (coach styles were always free in Settings); rewards are now the honorary title
157
+ itself, capped by the Lv.10 pinnacle. Server table and both dictionaries updated
158
+ together (the Chinese dictionary is locked to the server table by
159
+ `test/growth.test.ts`).
160
+ - **Levels read "荣誉" not "权益"** on the Growth page hero and in `get_growth_stats`
161
+ output, matching the honorary rewards.
162
+ - **Today plan keeps the achieved-closing row**: checking the last opportunity day
163
+ closes the task, which used to remove the row from the Today page entirely (the
164
+ counter shrank right after checking in). A closed-achieved task with a check-in
165
+ on that date stays in the done group with its undo entry; cancelling the check-in
166
+ revives the task through the existing freshness path.
167
+ - **Calendar hover preserves state colors**: hovering a partially/fully-completed
168
+ day no longer washes the semantic amber/green chip with the accent hover tint —
169
+ the state color is exactly what the hover was inspecting.
170
+ - **Contrast closures**: micro-action card secondary text and skipped steps use the
171
+ `label-on-2` pair and a higher muted opacity so both themes stay above the
172
+ 4.5:1 line (the same lift applies to deleted wish cards in chat); calendar/
173
+ check-in-cell state foreground colors moved into the theme token region as
174
+ paired `--xyd-on-c2` / `--xyd-on-c3` / `--xyd-on-dcell` tokens.
175
+ - **Date validation accepts only real calendar dates**: `2026-02-30` used to pass
176
+ the format check (`Date.parse` rolls it over instead of returning NaN) and then
177
+ silently produce a task with an empty opportunity sequence — or land in the
178
+ check-in table as a fake day that skews the streak replay. Task due dates, wish
179
+ estimated dates and check-in target dates (including the any-date back-fill on
180
+ no-deadline tasks) now share the opportunity calculator's semantic `isIsoDate`
181
+ round-trip check, and the routes' duplicate implementation converges onto it.
182
+ - **Prompt coherence**: the check-in example moved out of the "no-confirm mode"
183
+ section into confirm mode (check-in is a confirm-gated write) and no longer
184
+ demonstrates a streak number the tool does not return; list-tool outputs tell the
185
+ model to use bracketed IDs only for follow-up calls, not to render them to the
186
+ user; `generate_chart`'s `wishId` parameter names the camelCase chart keys.
187
+ - **Memory search bar wraps** so the saved notice is not clipped off narrow panels.
188
+ - **Copy/wording**: the Settings preference-unavailable hint no longer hardcodes a
189
+ stale count; the English "clear all memories" dialog is grammatical for one item;
190
+ the Growth page EXP/experience units are consistent in Chinese; an unknown level
191
+ falls back to a localized name instead of an empty label; the 404 API error and
192
+ the wish-not-found tool/route error carry localizable codes; the English
193
+ check-in grid summary is grammatical for one cell; README wording aligned with
194
+ the UI ("连续加成", straight quotes in English).
195
+
196
+ ### Fixed
197
+ - **sqlite backend**: a corrupted table row now reports `malformed-medium` like the
198
+ global slot does, instead of a raw `SyntaxError` with a split troubleshooting path.
199
+ - **Mock generator**: the detail-panel fixture in `debug/gen-mock.ts` takes an id
200
+ parameter so the wish/task scenarios no longer emit duplicate DOM ids.
201
+
8
202
  ## [0.5.5] - 2026-08-28
9
203
 
10
204
  > **Note:** this release is a six-perspective quality pass (product, visual design,
package/README.en-US.md CHANGED
@@ -28,7 +28,7 @@ Tools are only mounted on sessions using the XingYuan preset; other sessions are
28
28
  dsh plugin --profile web add @starwish-ai/xingyuan-dsh
29
29
  ```
30
30
 
31
- After starting the Web GUI, installation succeeded when「星愿」appears in the agent picker.
31
+ After starting the Web GUI, installation succeeded when "星愿" (XingYuan) appears in the agent picker.
32
32
 
33
33
  ## Data & Backup
34
34
 
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  - **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
14
14
  - **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
15
15
  - **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
16
- - **成长体系**:等级 Lv.1–Lv.10、连击加成、统计卡与近 30 天柱状图
16
+ - **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
17
17
  - **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
18
18
  默认仅星愿预设的会话显示,可在设置里切换「始终显示 / 始终隐藏」并按标签勾选
19
19
  - **记忆**:重要记忆自动注入上下文(上限可配),支持增删改查
package/lib/client/api.js CHANGED
@@ -22,6 +22,7 @@ const ERROR_KEY = {
22
22
  already_checked: 'err.already_checked',
23
23
  not_opportunity_day: 'err.not_opportunity_day',
24
24
  already_claimed: 'err.already_claimed',
25
+ claim_expired: 'err.claim_expired',
25
26
  task_closed: 'err.task_closed',
26
27
  due_past: 'err.due_past',
27
28
  due_too_far: 'err.due_too_far',
@@ -40,8 +41,15 @@ const ERROR_KEY = {
40
41
  bad_coach_style: 'err.bad_coach_style',
41
42
  bad_interests: 'err.bad_interests',
42
43
  };
43
- /** 把任意抛错转成用户可读文案:ActionError 按键本地化;其余直出消息。 */
44
+ /** 网络层失败的浏览器原文是英文技术串(Chrome "Failed to fetch"、Safari "Load failed"、
45
+ * Firefox "NetworkError…"、undici "fetch failed"),直出会打破本地化承诺——识别后统一
46
+ * 转本地化文案。仅匹配 TypeError(fetch 在请求发出前失败的规范类型)中的已知模式,
47
+ * 其余 TypeError(代码缺陷)仍原样抛出便于排障。 */
48
+ const NETWORK_ERROR_PATTERN = /failed to fetch|load failed|networkerror|fetch failed/i;
49
+ /** 把任意抛错转成用户可读文案:网络失败/ActionError 按键本地化;其余直出消息。 */
44
50
  export function describeError(e) {
51
+ if (e instanceof TypeError && NETWORK_ERROR_PATTERN.test(e.message))
52
+ return t('common.networkError');
45
53
  if (!(e instanceof ActionError))
46
54
  return e instanceof Error ? e.message : String(e);
47
55
  // 服务端新增未知 code 时回落原始消息,避免渲染 undefined 或插值崩溃
@@ -93,7 +93,10 @@ function TaskView(props) {
93
93
  perform();
94
94
  };
95
95
  const deleted = event.op === 'deleted';
96
- const showClaim = !deleted && phase === 'idle' && task.status === 'pending';
96
+ // 截止日已过的待领取任务写路径拒绝领取(store.claim_expired):卡片按钮同口径隐藏,
97
+ // 避免点按必失败(卡片是快照,today 取渲染当时——与写路径校验同一新鲜口径)
98
+ const claimExpired = task.status === 'pending' && task.dueDate !== undefined && task.dueDate < localYmd(new Date());
99
+ const showClaim = !deleted && phase === 'idle' && task.status === 'pending' && !claimExpired;
97
100
  const showCheckIn = !deleted && phase !== 'done' && (task.status === 'in_progress' || phase === 'claimed');
98
101
  const preview = event.opportunityPreview.length > 0
99
102
  ? t('task.upcoming', { dates: event.opportunityPreview.map((date) => formatShortDate(date)).join(activeLocale() === 'en' ? ', ' : '、') })
@@ -42,6 +42,7 @@ export const ZH_DICT = {
42
42
  'common.requestFailed': '请求失败',
43
43
  'common.actionFailed': '操作失败',
44
44
  'common.requestTimeout': '请求超时,请重试',
45
+ 'common.networkError': '网络连接失败,请确认星愿服务已启动后重试',
45
46
  'common.dayUnit': '{n} 天',
46
47
  'common.countUnit': '{n} 次',
47
48
  'common.countUnitOne': '{n} 次',
@@ -58,7 +59,7 @@ export const ZH_DICT = {
58
59
  // --- 愿望卡 / 愿望页 ---
59
60
  'wish.progress': '进度 {percent}%',
60
61
  'wish.eta': '预计完成 {date}',
61
- 'wish.noTasks': '暂无下属任务',
62
+ 'wish.noTasks': '暂无下属任务——在对话里告诉我,让我为它推荐打卡任务',
62
63
  'wish.pageTitle': '我的愿望',
63
64
  'wish.summary': '{active} 个进行中{achieved}', 'wish.achievedSuffix': ' · {n} 个已达成',
64
65
  'wish.sectionAchieved': '已达成',
@@ -77,6 +78,7 @@ export const ZH_DICT = {
77
78
  'task.group.pending': '待领取',
78
79
  'task.group.achieved': '已达成',
79
80
  'task.group.expired': '已过期',
81
+ 'task.claimExpiredHint': '该任务截止日已过,无法领取——可展开详情延长截止日重新开始',
80
82
  'task.expiredHint': '截止日已过——在下方延长截止日即可重新开始,也可以在对话里让我处理。',
81
83
  'task.pageTitle': '全部任务',
82
84
  'task.totalCount': '共 {n} 个',
@@ -106,17 +108,17 @@ export const ZH_DICT = {
106
108
  'toast.undone': '打卡已撤销',
107
109
  'toast.undoneAt': '已撤销{date}的打卡',
108
110
  'toast.wishCreated': '已创建愿望「{title}」',
109
- 'toast.taskCreated': '已创建任务「{name}」',
111
+ 'toast.taskCreated': '已创建任务「{name}」,领取后开始打卡',
110
112
  'toast.deleted': '已删除「{name}」',
111
113
  'toast.categoryRenamed': '分类已改名为「{name}」',
112
114
  'toast.categoryColored': '「{name}」配色已更新',
113
115
  'toast.categoryColorReset': '「{name}」配色已重置,跟随愿望默认色',
114
116
  'toast.categoryDeleted': '已清除「{name}」的自定义颜色',
115
117
  // --- 确认弹窗 ---
116
- 'confirm.futureCheckin': '「{name}」今天不是打卡日,提前打卡{date}表示承诺当天完成。确认?',
118
+ 'confirm.futureCheckin': '「{name}」的打卡日 {date} 在今天之后,提前打卡表示承诺当天完成。确认?',
117
119
  'confirm.undoToday': '确定撤销「{name}」今天的打卡吗?进度将回退。',
118
120
  'confirm.undoAt': '确定撤销「{name}」在{date}的打卡吗?进度将回退。',
119
- 'confirm.futureAt': '「{name}」{date}不是打卡日,提前打卡表示承诺当天完成。确认?',
121
+ 'confirm.futureAt': '「{name}」的打卡日 {date} 在今天之后,提前打卡表示承诺当天完成。确认?',
120
122
  'confirm.deleteTask': '确定删除任务「{name}」吗?其打卡记录与微行动拆解将一并删除,不可恢复。',
121
123
  'confirm.deleteWish': '确定删除愿望「{name}」吗?下属任务、打卡记录与微行动拆解将一并删除,不可恢复。',
122
124
  'confirm.renameCategory': '把分类「{old}」下的全部愿望改名为「{new}」吗?(共 {count} 个)', 'confirm.deleteEmptyCategory': '删除空分类「{name}」的自定义颜色吗?',
@@ -158,9 +160,9 @@ export const ZH_DICT = {
158
160
  'growth.levelLabel': '当前等级',
159
161
  'growth.levelFallback': '初心者',
160
162
  'growth.maxed': '已满级',
161
- 'growth.expFormat': '{cur} / {next} EXP',
163
+ 'growth.expFormat': '{cur} / {next} 经验',
162
164
  'growth.levelRequire': '需要 {exp} 经验',
163
- 'growth.rewardPrefix': '当前等级权益:{reward}',
165
+ 'growth.rewardPrefix': '当前等级荣誉:{reward}',
164
166
  'growth.stat.checkinDays': '累计打卡天数',
165
167
  'growth.stat.streak': '连续坚持',
166
168
  'growth.stat.maxStreak': '最长连续坚持',
@@ -181,23 +183,23 @@ export const ZH_DICT = {
181
183
  'growth.lv.1.name': '初心者',
182
184
  'growth.lv.1.reward': '开启星愿之旅',
183
185
  'growth.lv.2.name': '探索者',
184
- 'growth.lv.2.reward': '解锁更多 AI 对话功能',
186
+ 'growth.lv.2.reward': '晋升「探索者」称号',
185
187
  'growth.lv.3.name': '实践者',
186
- 'growth.lv.3.reward': '获得专属头像框',
188
+ 'growth.lv.3.reward': '晋升「实践者」称号',
187
189
  'growth.lv.4.name': '坚持者',
188
- 'growth.lv.4.reward': '解锁高级教练风格',
190
+ 'growth.lv.4.reward': '晋升「坚持者」称号',
189
191
  'growth.lv.5.name': '奋斗者',
190
- 'growth.lv.5.reward': '获得专属徽章',
192
+ 'growth.lv.5.reward': '晋升「奋斗者」称号',
191
193
  'growth.lv.6.name': '进取者',
192
- 'growth.lv.6.reward': '解锁更多任务模板',
194
+ 'growth.lv.6.reward': '晋升「进取者」称号',
193
195
  'growth.lv.7.name': '成就者',
194
- 'growth.lv.7.reward': '获得专属称号',
196
+ 'growth.lv.7.reward': '晋升「成就者」称号',
195
197
  'growth.lv.8.name': '卓越者',
196
- 'growth.lv.8.reward': '解锁全部教练风格',
198
+ 'growth.lv.8.reward': '晋升「卓越者」称号',
197
199
  'growth.lv.9.name': '领航者',
198
- 'growth.lv.9.reward': '获得限定头像框',
200
+ 'growth.lv.9.reward': '晋升「领航者」称号',
199
201
  'growth.lv.10.name': '星愿大师',
200
- 'growth.lv.10.reward': '解锁全部特权,成为星愿大师',
202
+ 'growth.lv.10.reward': '晋升「星愿大师」——星愿之旅的最高荣誉',
201
203
  // --- 记忆页 ---
202
204
  'memory.pageTitle': '记忆',
203
205
  'memory.summary': '共 {total} 条 · 对话时按上限自动注入',
@@ -218,6 +220,7 @@ export const ZH_DICT = {
218
220
  'memory.footNote': '删除与清空不可恢复;注入条数上限在 设置 → 星愿 调整。',
219
221
  'memory.clearAll': '清空全部记忆',
220
222
  'memory.needKeyAndValue': '键名和内容都要填写。',
223
+ 'memory.keyTooShort': '键名至少需要 2 个字符。',
221
224
  'memory.overwriteAsk': '「{key}」已存在,覆盖原内容吗?',
222
225
  'memory.confirmDelete': '删除记忆「{key}:{value}」?删除后不可恢复。',
223
226
  'memory.confirmClear': '清空全部 {total} 条记忆?此操作不可恢复!',
@@ -259,7 +262,7 @@ export const ZH_DICT = {
259
262
  'settings.profile.saved': '画像已保存',
260
263
  'settings.profile.sharedHint': '与对话侧共享同一份档案;对话里说「叫我小星」也会更新。',
261
264
  'settings.pref.title': '对话偏好',
262
- 'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下两项不可用。',
265
+ 'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下选项不可用。',
263
266
  'settings.pref.loading': '偏好加载中…',
264
267
  'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
265
268
  'settings.pref.readOnly': '当前设置为只读,无法保存修改。',
@@ -354,7 +357,7 @@ export const ZH_DICT = {
354
357
  'detail.grid.checked': '{date} 已打卡',
355
358
  'detail.grid.missed': '{date} 未打卡',
356
359
  'detail.grid.future': '{date} 待打卡',
357
- 'detail.next.title': '接下来的机会日:{dates}',
360
+ 'detail.next.title': '接下来的打卡日:{dates}',
358
361
  'detail.revive.label': '延长截止日(重新开始)',
359
362
  'detail.revive.action': '延长并重新开始',
360
363
  'detail.revive.done': '截止日已延长,任务重新开始',
@@ -423,8 +426,9 @@ export const ZH_DICT = {
423
426
  'err.missing_field': '缺少必填字段:{field}',
424
427
  'err.not_found': '目标不存在或已被删除',
425
428
  'err.already_checked': '该日期已打卡;如需修改,请先撤销当日打卡',
426
- 'err.not_opportunity_day': '该日期不是机会日;可在日历页选择最近的机会日',
429
+ 'err.not_opportunity_day': '该日期不是打卡日;可在日历页选择最近的打卡日',
427
430
  'err.already_claimed': '该任务已领取过',
431
+ 'err.claim_expired': '该任务截止日已过,无法领取——请先延长截止日使任务重新开始',
428
432
  'err.task_closed': '任务已完结,无法再打卡',
429
433
  'err.due_past': '截止日期不能早于今天',
430
434
  'err.due_too_far': '截止日期不能超过今天起 10 年',
@@ -433,7 +437,7 @@ export const ZH_DICT = {
433
437
  'err.bad_date': '日期格式或取值不合法(yyyy-MM-dd,且不得早于今天)',
434
438
  'err.overwrite_required': '已存在同名记录,需确认覆盖',
435
439
  'err.not_claimed': '任务尚未领取,请先领取',
436
- 'err.no_opportunity_left': '没有可勾选的打卡日:机会日已全部完成或截止',
440
+ 'err.no_opportunity_left': '没有可勾选的打卡日:应打卡的日期已全部完成或截止',
437
441
  'err.no_checkins': '该任务暂无打卡记录',
438
442
  'err.title_too_long': '标题不能超过 50 字符',
439
443
  'err.name_too_long': '任务名不能超过 100 字符',
@@ -461,6 +465,7 @@ const EN_DICT = {
461
465
  'common.requestFailed': 'Request failed',
462
466
  'common.actionFailed': 'Action failed',
463
467
  'common.requestTimeout': 'Request timed out, please retry',
468
+ 'common.networkError': 'Network request failed. Make sure the XingYuan service is running, then try again',
464
469
  'common.dayUnit': '{n} days',
465
470
  'common.countUnit': '{n} times',
466
471
  'common.countUnitOne': '{n} time',
@@ -475,7 +480,7 @@ const EN_DICT = {
475
480
  'state.done': 'Done',
476
481
  'wish.progress': 'Progress {percent}%',
477
482
  'wish.eta': 'Target date {date}',
478
- 'wish.noTasks': 'No tasks yet',
483
+ 'wish.noTasks': 'No tasks yet — tell me in chat and I will suggest check-in tasks for it',
479
484
  'wish.pageTitle': 'My Wishes',
480
485
  'wish.summary': '{active} in progress{achieved}',
481
486
  'wish.achievedSuffix': ' · {n} achieved',
@@ -494,6 +499,7 @@ const EN_DICT = {
494
499
  'task.group.pending': 'Unclaimed',
495
500
  'task.group.achieved': 'Achieved',
496
501
  'task.group.expired': 'Expired',
502
+ 'task.claimExpiredHint': 'Past its due date and unclaimed — open the task detail to extend the due date and restart it',
497
503
  'task.expiredHint': 'The due date has passed — extend it below to restart the task, or just ask me in chat.',
498
504
  'task.pageTitle': 'All Tasks',
499
505
  'task.totalCount': '{n} in total',
@@ -521,16 +527,16 @@ const EN_DICT = {
521
527
  'toast.undone': 'Check-in reverted',
522
528
  'toast.undoneAt': 'Reverted the check-in on {date}',
523
529
  'toast.wishCreated': 'Wish “{title}” created',
524
- 'toast.taskCreated': 'Task “{name}” created',
530
+ 'toast.taskCreated': 'Task “{name}” created — claim it to start checking in',
525
531
  'toast.deleted': '“{name}” deleted',
526
532
  'toast.categoryRenamed': 'Category renamed to “{name}”',
527
533
  'toast.categoryColored': 'Color updated for “{name}”',
528
534
  'toast.categoryColorReset': 'Color of “{name}” reset to follow wish colors',
529
535
  'toast.categoryDeleted': 'Custom color of “{name}” cleared',
530
- 'confirm.futureCheckin': '“{name}” is not due today. Checking in early for {date} means committing to finish it that day. Continue?',
536
+ 'confirm.futureCheckin': 'The check-in day {date} for “{name}” is after today. Checking in early means committing to finish it that day. Continue?',
531
537
  'confirm.undoToday': "Undo today’s check-in for “{name}”? Progress will roll back.",
532
538
  'confirm.undoAt': 'Undo the check-in of “{name}” on {date}? Progress will roll back.',
533
- 'confirm.futureAt': '{date} is not the due day of “{name}”. Checking in early means committing to finish it that day. Continue?',
539
+ 'confirm.futureAt': 'The check-in day {date} for “{name}” is after today. Checking in early means committing to finish it that day. Continue?',
534
540
  'confirm.deleteTask': 'Delete task “{name}”? Its check-ins and micro-step plan will be removed too. This cannot be undone.',
535
541
  'confirm.deleteWish': 'Delete wish “{name}”? Its tasks, check-ins and micro-step plans will be removed too. This cannot be undone.',
536
542
  'confirm.renameCategory': 'Rename category “{old}” to “{new}” everywhere? ({count} affected)',
@@ -581,7 +587,7 @@ const EN_DICT = {
581
587
  'growth.stat.taskTotal': 'Tasks created',
582
588
  'growth.stat.taskDone': 'Tasks achieved',
583
589
  'growth.stat.none': 'None',
584
- 'growth.chart.title': 'Last 30 days',
590
+ 'growth.chart.title': 'Check-ins · last 30 days',
585
591
  'growth.chart.empty': 'No data yet. Your trend chart grows after the first check-in.',
586
592
  'growth.chart.hint': 'Blue = checked in, striped = missed gap, green = full day. Hover or focus a bar for daily details.',
587
593
  'growth.chart.tooltip': '{date}: checked {checked}/{total}',
@@ -591,23 +597,23 @@ const EN_DICT = {
591
597
  'growth.lv.1.name': 'Novice',
592
598
  'growth.lv.1.reward': 'Begin your journey',
593
599
  'growth.lv.2.name': 'Explorer',
594
- 'growth.lv.2.reward': 'Unlock more AI chat features',
600
+ 'growth.lv.2.reward': 'Earn the “Explorer” title',
595
601
  'growth.lv.3.name': 'Practitioner',
596
- 'growth.lv.3.reward': 'Earn an exclusive avatar frame',
602
+ 'growth.lv.3.reward': 'Earn the “Practitioner” title',
597
603
  'growth.lv.4.name': 'Perseverer',
598
- 'growth.lv.4.reward': 'Unlock advanced coach styles',
604
+ 'growth.lv.4.reward': 'Earn the “Perseverer” title',
599
605
  'growth.lv.5.name': 'Striver',
600
- 'growth.lv.5.reward': 'Earn an exclusive badge',
606
+ 'growth.lv.5.reward': 'Earn the “Striver” title',
601
607
  'growth.lv.6.name': 'Go-getter',
602
- 'growth.lv.6.reward': 'Unlock more task templates',
608
+ 'growth.lv.6.reward': 'Earn the “Go-getter” title',
603
609
  'growth.lv.7.name': 'Achiever',
604
- 'growth.lv.7.reward': 'Earn an exclusive title',
610
+ 'growth.lv.7.reward': 'Earn the “Achiever” title',
605
611
  'growth.lv.8.name': 'Paragon',
606
- 'growth.lv.8.reward': 'Unlock all coach styles',
612
+ 'growth.lv.8.reward': 'Earn the “Paragon” title',
607
613
  'growth.lv.9.name': 'Pathfinder',
608
- 'growth.lv.9.reward': 'Earn a limited avatar frame',
614
+ 'growth.lv.9.reward': 'Earn the “Pathfinder” title',
609
615
  'growth.lv.10.name': 'XingYuan Master',
610
- 'growth.lv.10.reward': 'Unlock all perks and become a XingYuan Master',
616
+ 'growth.lv.10.reward': 'Earn the “XingYuan Master” title — the highest honor of the journey',
611
617
  'memory.pageTitle': 'Memories',
612
618
  'memory.summary': '{total} in total · auto-injected up to the limit during chats',
613
619
  'memory.editing': 'Editing “{key}” (key is fixed)',
@@ -618,7 +624,7 @@ const EN_DICT = {
618
624
  'memory.importanceLabel': 'Importance: {level}',
619
625
  'memory.add': '+ Add',
620
626
  'memory.saveEdit': 'Save changes',
621
- 'memory.capNote': '{total} in total, showing the latest {shown}; narrow down with search.',
627
+ 'memory.capNote': '{total} in total, showing the latest {shown} entries; narrow down with search.',
622
628
  'memory.more': 'Load more',
623
629
  'memory.loadedAll': 'All {n} shown',
624
630
  'memory.empty.title': 'No memories yet',
@@ -627,9 +633,10 @@ const EN_DICT = {
627
633
  'memory.footNote': 'Deletions are permanent; adjust the injection limit under Settings → XingYuan.',
628
634
  'memory.clearAll': 'Clear all memories',
629
635
  'memory.needKeyAndValue': 'Both key and content are required.',
636
+ 'memory.keyTooShort': 'Key must be at least 2 characters.',
630
637
  'memory.overwriteAsk': '“{key}” already exists. Overwrite it?',
631
638
  'memory.confirmDelete': 'Delete memory “{key}: {value}”? This cannot be undone.',
632
- 'memory.confirmClear': 'Clear all {total} memories? This cannot be undone!',
639
+ 'memory.confirmClear': 'Clear all memories ({total})? This cannot be undone!',
633
640
  'memory.savedNew': 'Added “{key}”',
634
641
  'memory.savedOverwrite': 'Saved “{key}”',
635
642
  'memory.deletedOne': '“{key}” deleted',
@@ -667,14 +674,14 @@ const EN_DICT = {
667
674
  'settings.profile.saved': 'Profile saved',
668
675
  'settings.profile.sharedHint': 'Shared with the chat side; saying “call me Star” in chat updates it too.',
669
676
  'settings.pref.title': 'Chat preferences',
670
- 'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the two options below are disabled.',
677
+ 'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the options below are disabled.',
671
678
  'settings.pref.loading': 'Loading preferences…',
672
679
  'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
673
680
  'settings.pref.readOnly': 'Settings are read-only; changes cannot be saved.',
674
681
  'settings.pref.confirmWrites': 'Confirm write actions',
675
682
  'settings.pref.confirmWritesHint': 'Show an in-app confirmation before creating wishes/tasks, checking in or canceling (deletes always confirm). Turn off to execute directly.',
676
683
  'settings.pref.memoryLimit': 'Memory injection limit',
677
- 'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved on blur.',
684
+ 'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved when you leave the field.',
678
685
  'settings.pref.limitInvalid': 'Enter an integer between 5 and 200',
679
686
  'settings.pref.writeFailed': 'The change did not save; please try again.',
680
687
  'settings.pref.confirmLang': 'Confirm card language',
@@ -749,10 +756,10 @@ const EN_DICT = {
749
756
  'chart.subtitle.daysRatio': '{done}/{due} days',
750
757
  'chart.subtitle.streak': 'Current {current}d / best {best}d',
751
758
  'detail.grid.title': 'Check-in record',
752
- 'detail.grid.summary': 'Last {total} cells: {checked} checked, {missed} missed, {future} pending',
759
+ 'detail.grid.summary': 'Last {total} shown: {checked} checked, {missed} missed, {future} to check in',
753
760
  'detail.legend.checked': 'Checked in',
754
761
  'detail.legend.missed': 'Missed',
755
- 'detail.legend.future': 'Pending today',
762
+ 'detail.legend.future': 'To check in',
756
763
  'detail.grid.checked': '{date} checked in',
757
764
  'detail.grid.missed': '{date} missed',
758
765
  'detail.grid.future': '{date} pending check-in',
@@ -820,8 +827,9 @@ const EN_DICT = {
820
827
  'err.missing_field': 'Missing required field: {field}',
821
828
  'err.not_found': 'Not found, or already deleted',
822
829
  'err.already_checked': 'Already checked in on this date — undo it first to change',
823
- 'err.not_opportunity_day': 'Not an opportunity day — pick the nearest one in the Calendar tab',
830
+ 'err.not_opportunity_day': 'Not a check-in day — pick the nearest one in the Calendar tab',
824
831
  'err.already_claimed': 'Already claimed',
832
+ 'err.claim_expired': 'This task is past its due date — extend the due date first to restart it',
825
833
  'err.task_closed': 'Task is closed and cannot be checked in',
826
834
  'err.due_past': 'Due date cannot be before today',
827
835
  'err.due_too_far': 'Due date cannot be more than 10 years ahead',
@@ -830,7 +838,7 @@ const EN_DICT = {
830
838
  'err.bad_date': 'Invalid date (yyyy-MM-dd, not before today)',
831
839
  'err.overwrite_required': 'Already exists; confirmation required to overwrite',
832
840
  'err.not_claimed': 'Task must be claimed first',
833
- 'err.no_opportunity_left': 'No check-in day left to tick — all opportunity days are done or past the deadline',
841
+ 'err.no_opportunity_left': 'No check-in day left to tick — all scheduled days are done or past the deadline',
834
842
  'err.no_checkins': 'No check-in records for this task',
835
843
  'err.title_too_long': 'Title exceeds 50 characters',
836
844
  'err.name_too_long': 'Task name exceeds 100 characters',
@@ -905,7 +913,12 @@ export function useXyT() {
905
913
  return xyTranslateStable;
906
914
  }
907
915
  const xyTranslateStable = (key, params) => t(key, params);
908
- /** 当前语言(供调试/埋点类场景;无 face 时返回 'zh' 兜底口径)。 */
916
+ /** 当前语言(供调试/埋点类场景;无 face 时返回 'zh' 兜底口径)。
917
+ * 平台事实(rc.2 实测):宿主发布的 LocaleFace 契约(dsh-client-ui-slots
918
+ * renderer.d.ts)只声明 getSnapshot(): { revision } 与 bind(ns),快照的 `active`
919
+ * 字段不在类型面内但运行时存在——本函数依赖它做语言判定,读取处已做 unknown
920
+ * 收窄,字段缺席时静默回落 zh(en 用户会看到英文正文配中文日期,而非崩溃)。
921
+ * 上游若把 active 纳入正式契约,此处无需改动。 */
909
922
  export function activeLocale() {
910
923
  if (localeFace === undefined)
911
924
  return 'zh';
@@ -7,7 +7,7 @@ import { createElement, useEffect, useRef, useState } from 'react';
7
7
  import { getJson, postAction, describeError } from '../api.js';
8
8
  import { useXyT } from '../i18n.js';
9
9
  import { softConfirm, softConfirmDanger, useActionGuard } from '../hooks.js';
10
- import { toast } from '../ui.js';
10
+ import { toast, focusPageTitle } from '../ui.js';
11
11
  import { categoryVars } from '../../category-color.js';
12
12
  import { SwatchRow } from './color-swatch.js';
13
13
  import { countKey } from './format.js';
@@ -82,6 +82,29 @@ export function CategoryManager(props) {
82
82
  const [renaming, setRenaming] = useState(undefined);
83
83
  const [coloring, setColoring] = useState(undefined);
84
84
  const { busy, guard } = useActionGuard();
85
+ // 改名编辑器随关闭(提交/取消/Esc)整体卸载,焦点会落空:记下触发行内「改名」
86
+ // 按钮,关闭后焦点归还。仅在「打开→关闭」迁移时执行(面板挂载的那次 effect
87
+ // 不动焦点——否则每次打开面板焦点就被拽到页面标题);改名成功后行按新名
88
+ // re-key、旧触发按钮随之销毁,且 effect 执行时 reload 数据未回、旧按钮还在
89
+ // DOM(归还焦点会在 re-key 中落空到 body)——成功路径显式清空触发引用,令
90
+ // effect 直接走页面标题兜底
91
+ const renameTriggerRef = useRef(null);
92
+ const renameOpenRef = useRef(false);
93
+ useEffect(() => {
94
+ if (renaming !== undefined) {
95
+ renameOpenRef.current = true;
96
+ return;
97
+ }
98
+ if (!renameOpenRef.current)
99
+ return;
100
+ renameOpenRef.current = false;
101
+ const trigger = renameTriggerRef.current;
102
+ renameTriggerRef.current = null;
103
+ if (trigger !== null && document.contains(trigger))
104
+ trigger.focus();
105
+ else
106
+ focusPageTitle();
107
+ }, [renaming]);
85
108
  const load = () => {
86
109
  setError(undefined);
87
110
  getJson('/xingyuan/api/categories')
@@ -94,6 +117,7 @@ export function CategoryManager(props) {
94
117
  const commitRename = (oldName, newName) => {
95
118
  guard(() => postAction('category-rename', { oldName, newName }).then(() => {
96
119
  toast(t('toast.categoryRenamed', { name: newName }), 'ok');
120
+ renameTriggerRef.current = null;
97
121
  setRenaming(undefined);
98
122
  reload();
99
123
  }));
@@ -114,7 +138,11 @@ export function CategoryManager(props) {
114
138
  }, row.name), createElement('span', { className: 'xy-meta xy-catcount' }, t(countKey(row.wishCount, 'catmgr.countOne', 'catmgr.count'), { n: row.wishCount })), createElement('span', { className: 'xy-catops' }, createElement('button', {
115
139
  className: 'xy-btn',
116
140
  disabled: busy,
117
- onClick: () => { setRenaming(row.name); setColoring(undefined); },
141
+ onClick: (e) => {
142
+ renameTriggerRef.current = e.currentTarget;
143
+ setRenaming(row.name);
144
+ setColoring(undefined);
145
+ },
118
146
  }, t('catmgr.rename')), createElement('button', {
119
147
  className: 'xy-btn',
120
148
  disabled: busy,