@deepseek-ai/dsh-client-ui-goal 0.1.3-alpha.2 → 0.1.5-alpha.2

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-goal/README.md
5
- README.md: 8556575b00e1d3073f251d83f3dee0bafb4c8969
6
- README.zh.md: 22d0417bc2d37719150ec46aad849ad2bce8b460
5
+ README.md: 1fbc1c7df944f1106fedcff79f6b9df35bda480d
6
+ README.zh.md: 01f8902377c98dc39d64b3bfe26108b68cb29fe0
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- This package renders the goal surface in the Web GUI: a strip in the composer-context stack that shows the current goal of the session and offers edit, pause, resume, and clear actions. It reads the live goal from the host-computed projection and routes every mutation through the goal service, surfacing rejections inline. It also projects each durable `/goal` command run as a `Command input` bubble in the chat, so a goal command entered by the user or the model appears in the transcript. Goal creation is outside this plugin. The shipped Web presets other than `minimal` mount `/goal` in their agent scope.
12
+ The Web GUI goal surface shows both the durable goal state and its current process-local activation, and lets users edit, pause, resume, or clear the goal; rejected changes appear inline. It displays durable `/goal` runs as `Command input` bubbles so commands from users or the model remain visible after reload. Goal creation remains outside this package. Shipped Web presets other than `minimal` make `/goal` available to agents.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,7 +25,7 @@ This package renders the goal surface in the Web GUI: a strip in the composer-co
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Mount this plugin alongside `ui-conversation` and the goal domain package; the strip then appears as the second card in the composer-context stack (after Todo, before Queue) whenever the session has a goal. An active goal offers pause; a paused one offers resume; edit rewrites the objective; clear removes the goal and suppresses the strip until the projection catches up.
28
+ Mount this plugin alongside `ui-conversation` and the goal domain package; the strip then appears as the second card in the composer-context stack (after Todo, before Queue) whenever the session has a goal. An armed active goal offers pause; an active-but-disarmed or paused goal offers resume; edit rewrites the objective; clear removes the goal and suppresses the strip until the projection catches up.
29
29
 
30
30
  ### The command-input bubble
31
31
 
@@ -43,7 +43,7 @@ A rejected mutation surfaces the Remote error inline on the strip; loading, abse
43
43
  <details>
44
44
  <summary>Implementation internals — click to expand</summary>
45
45
 
46
- The strip is projection-mode: the live goal arrives through `useProjection('goal')` (seeded by the history tail page and updated by `session/projection` frames), so the plugin owns no domain store, refresh chain, or event listener. The inject face carries only the four mutation verbs through `ctx.remote.goals`; each reads the CAS ref from the session's current projected value at call time, and the RPC's compare-and-set is the staleness guard. The strip single-flights mutations synchronously because a pending render cannot fence same-frame clicks. The command-input projection is a separate Conversation Definition that builds a `command-input` Chat Node before the generic command result Node; it never creates `user/message` or a model turn.
46
+ The durable goal arrives through `useProjection('goal')` (seeded by the history tail page and updated by `session/projection` frames). The inject face carries a registrant-private activation hook source plus the four mutation verbs. That source starts only while the framework hook observes it, reads `ctx.remote.goals.get`, subscribes to `goal/activation-changed`, and refreshes on running-state or connection resets. Live-event epochs invalidate in-flight reads, so a stale HTTP result cannot overwrite a newer activation edge; running refreshes retain the last known activation until the read resolves. The strip owns no domain store or cross-plugin cache. Each mutation reads the CAS ref from the session's current projected value at call time, and the RPC's compare-and-set is the staleness guard. The strip single-flights mutations synchronously because a pending render cannot fence same-frame clicks. The command-input projection is a separate Conversation Definition that builds a `command-input` Chat Node before the generic command result Node; it never creates `user/message` or a model turn.
47
47
 
48
48
  </details>
49
49
 
@@ -76,7 +76,6 @@ None unless the queued goal context is admitted. An admitted context extends the
76
76
 
77
77
  These limits define the current goal surface. They are current package constraints, not a goal-domain comparison or a task backlog.
78
78
 
79
- - **Durable phase only** — the projection omits process-local activation, so the strip cannot distinguish an active-but-disarmed goal from an armed one; resume re-arms through the RPC side. There is no host-live activation channel.
80
79
  - **Preset-independent host state** — switching an active session to `minimal` leaves its host-owned goal intact. `/goal` and goal tools disappear, while this strip can still edit, pause, resume, or clear the goal.
81
80
 
82
81
  <a id="dev-note"></a>
@@ -89,4 +88,4 @@ None.
89
88
 
90
89
  </details>
91
90
 
92
- **Runtime invariant:** No companion is published. A single GoalBar dock registration whose disposal is proven by the HMR-safety spec — the plugin owns no store (state arrives on the goal projection), emits no cordis events, and holds no cross-plugin mutable state.
91
+ **Runtime invariant:** No companion is published. A single GoalBar dock registration whose disposal is proven by the HMR-safety spec — durable state arrives on the goal projection, process-local activation arrives through the entry's private hook source, and that source subscribes only while the framework hook observes it.
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 本包在 Web GUI 中渲染 goal 表面:composer 上下文堆栈里的一条条带,显示会话的当前目标,并提供编辑、暂停、恢复与清除动作。它从宿主计算的投影读取活目标,把每次变更都经 goal 服务路由,并把拒绝内联呈现。它还把每条持久的 `/goal` 命令运行投影为聊天中的 `Command input` 气泡,让用户或模型输入的 goal 命令出现在文本记录中。goal 创建不归本插件。除 `minimal` 外,随附的 Web preset 都会在其 agent scope 中挂载 `/goal`。
12
+ Web GUI goal 表面同时显示持久 goal 状态及当前的进程本地激活状态,供用户编辑、暂停、恢复或清除 goal;被拒绝的变更会内联显示。它把持久的 `/goal` 运行显示为 `Command input` 气泡,让用户或模型发出的命令在重新加载后仍然可见。goal 创建仍不归本包。除 `minimal` 外,随附的 Web preset 都会向 agent 提供 `/goal`。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,7 +25,7 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 与 `ui-conversation` 及 goal 领域包一起挂载本插件;只要会话存在目标,条带就会作为 composer 上下文堆栈的第二张卡片出现(位于 Todo 之后、Queue 之前)。active 的 goal 提供暂停动作;paused 的提供恢复;编辑重写目标文本;清除移除目标,并在投影追上之前抑制条带。
28
+ 与 `ui-conversation` 及 goal 领域包一起挂载本插件;只要会话存在目标,条带就会作为 composer 上下文堆栈的第二张卡片出现(位于 Todo 之后、Queue 之前)。已 armed active goal 提供暂停动作;active-but-disarmed 或 paused 的 goal 提供恢复;编辑重写目标文本;清除移除目标,并在投影追上之前抑制条带。
29
29
 
30
30
  ### 指令输入气泡
31
31
 
@@ -43,7 +43,7 @@ kind: "package-reference"
43
43
  <details>
44
44
  <summary>实现细节——点击展开</summary>
45
45
 
46
- 条带是投影模式:活目标经 `useProjection('goal')` 到达(由历史尾页播种、`session/projection` 帧更新),因此插件不持有领域 store、不设刷新链、不挂事件监听。注入面只携带四个变更动词,经 `ctx.remote.goals` 调用;每个动词在调用时从会话当前投影值读取 CAS ref,比较并交换(RPC 的 CAS)就是陈旧性护栏。由于 React 的 pending 渲染无法拦住同一帧内的点击,条带会同步为变更建立 single-flight 防护。指令输入投影是独立的 Conversation Definition,在通用命令结果 Node 之前构建 `command-input` Chat Node;它绝不创建 `user/message` 或模型轮次。
46
+ 持久 goal 经 `useProjection('goal')` 到达(由历史尾页播种、`session/projection` 帧更新)。注入面携带 registrant-private 的 activation hook source 与四个变更动词。该 source 仅在框架 hook 观察期间订阅,读取 `ctx.remote.goals.get`、订阅 `goal/activation-changed`,并在 running 状态或连接 reset 时刷新。live event epoch 会让在途读取失效,因此较旧的 HTTP 响应不能覆盖更新的 activation 边界;running 刷新会保留最后一次已知 activation,直到读取完成。条带不持有领域 store 或跨插件缓存。每个变更在调用时从会话当前投影值读取 CAS ref,比较并交换(RPC 的 CAS)就是陈旧性护栏。由于 React 的 pending 渲染无法拦住同一帧内的点击,条带会同步为变更建立 single-flight 防护。指令输入投影是独立的 Conversation Definition,在通用命令结果 Node 之前构建 `command-input` Chat Node;它绝不创建 `user/message` 或模型轮次。
47
47
 
48
48
  </details>
49
49
 
@@ -76,7 +76,6 @@ kind: "package-reference"
76
76
 
77
77
  这些限制界定了当前 goal 表面。它们是当前包约束,不是 goal 领域对比或任务积压。
78
78
 
79
- - **只反映持久阶段**——投影省略进程本地的激活状态,因此条带无法区分已激活但未武装的 goal 与已武装的 goal;恢复经 RPC 侧重新武装。不存在宿主实时激活通道。
80
79
  - **Host 状态与 preset 无关**——把活跃会话切换到 `minimal` 后,Host 拥有的 goal 仍会保留。`/goal` 与 goal 工具会消失,但该条带仍可编辑、暂停、恢复或清除 goal。
81
80
 
82
81
  <a id="dev-note"></a>
@@ -89,4 +88,4 @@ kind: "package-reference"
89
88
 
90
89
  </details>
91
90
 
92
- **运行时不变式:** 不发布伴生入口。插件只注册一个 GoalBar dock,HMR 测试覆盖释放;状态来自 goal projection,本包不持有 store 或跨插件可变状态。
91
+ **运行时不变式:** 不发布伴生入口。插件只注册一个 GoalBar dock,HMR 测试覆盖释放;持久状态来自 goal projection,进程本地 activation 来自入口私有 hook source,且该 source 只在框架 hook 观察期间订阅。
package/lib/client.js CHANGED
@@ -7,6 +7,122 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region lib/types/client/activation-source.js
11
+ /** Goal activation observable that orders Remote reads and live activation events. */
12
+ /** Compare two empty-or-populated activation snapshots by value. */
13
+ function sameSnapshot(left, right) {
14
+ return left.id === right.id && left.revision === right.revision && left.activation === right.activation;
15
+ }
16
+ /** Return the current active CAS ref, or undefined when the goal is not active. */
17
+ function activeRef(projection) {
18
+ return projection?.goal.phase === "active" ? projection.goal : void 0;
19
+ }
20
+ /**
21
+ * Create one registrant-private activation source. The source subscribes only
22
+ * while a framework hook observes it, so unmount releases the Remote event,
23
+ * projection, running-snapshot, and reset listeners.
24
+ * @param deps - projection, session, Remote read, and live-event inputs.
25
+ * @returns stable snapshot source consumed by `useGoalActivation`.
26
+ */
27
+ function createGoalActivationSource(deps) {
28
+ let snapshot = {};
29
+ let subscriptions = 0;
30
+ let disposers = [];
31
+ let running = deps.session.getSnapshot().running;
32
+ let eventEpoch = 0;
33
+ let projectionEpoch = 0;
34
+ let readEpoch = 0;
35
+ const listeners = /* @__PURE__ */ new Set();
36
+ const publish = (next) => {
37
+ if (sameSnapshot(snapshot, next)) return;
38
+ snapshot = next;
39
+ for (const listener of listeners) listener();
40
+ };
41
+ const startRead = (ref) => {
42
+ if (ref === void 0) return;
43
+ const read = ++readEpoch;
44
+ const startedAtEvent = eventEpoch;
45
+ const startedAtProjection = projectionEpoch;
46
+ deps.getGoal().then((result) => {
47
+ if (read !== readEpoch || startedAtEvent !== eventEpoch || startedAtProjection !== projectionEpoch) return;
48
+ if (!result.ok) return;
49
+ const goal = result.value;
50
+ /* v8 ignore next 4 -- projection drive is the authoritative clear edge; an active projection with no live goal is transient. */
51
+ if (goal === void 0) {
52
+ if (activeRef(deps.projection.getSnapshot()) === void 0) publish({});
53
+ return;
54
+ }
55
+ publish({
56
+ id: goal.id,
57
+ revision: goal.revision,
58
+ activation: goal.activation
59
+ });
60
+ });
61
+ };
62
+ const refreshProjection = () => {
63
+ projectionEpoch++;
64
+ const ref = activeRef(deps.projection.getSnapshot());
65
+ if (ref === void 0) {
66
+ /* v8 ignore next -- clearing an already-empty activation snapshot is idempotent. */
67
+ if (snapshot.id !== void 0) publish({});
68
+ return;
69
+ }
70
+ if (snapshot.id !== ref.id || snapshot.revision !== ref.revision) publish({
71
+ id: ref.id,
72
+ revision: ref.revision
73
+ });
74
+ startRead(ref);
75
+ };
76
+ const onActivation = (goal) => {
77
+ eventEpoch++;
78
+ readEpoch++;
79
+ publish(goal === void 0 ? {} : {
80
+ id: goal.id,
81
+ revision: goal.revision,
82
+ activation: goal.activation
83
+ });
84
+ };
85
+ const onRunning = () => {
86
+ const next = deps.session.getSnapshot().running;
87
+ if (next === running) return;
88
+ running = next;
89
+ startRead(activeRef(deps.projection.getSnapshot()));
90
+ };
91
+ const onReset = () => {
92
+ eventEpoch++;
93
+ projectionEpoch++;
94
+ startRead(activeRef(deps.projection.getSnapshot()));
95
+ };
96
+ const start = () => {
97
+ disposers = [
98
+ deps.projection.subscribe(refreshProjection),
99
+ deps.session.subscribe(onRunning),
100
+ deps.subscribeActivation(onActivation),
101
+ deps.subscribeReset(onReset)
102
+ ];
103
+ running = deps.session.getSnapshot().running;
104
+ refreshProjection();
105
+ };
106
+ const stop = () => {
107
+ for (const dispose of disposers) dispose();
108
+ disposers = [];
109
+ readEpoch++;
110
+ };
111
+ return {
112
+ getSnapshot: () => snapshot,
113
+ subscribe(listener) {
114
+ listeners.add(listener);
115
+ if (subscriptions === 0) start();
116
+ subscriptions++;
117
+ return () => {
118
+ listeners.delete(listener);
119
+ subscriptions--;
120
+ if (subscriptions === 0) stop();
121
+ };
122
+ }
123
+ };
124
+ }
125
+ //#endregion
10
126
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-goal/src/client/GoalBar.module.css.mjs
11
127
  const css$1 = ".nLMEza_dock{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset));margin:0 auto}.nLMEza_bar{box-sizing:border-box;width:100%;max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-specific-tip);border-radius:12px;align-items:center;gap:10px;height:36px;margin:0 auto;padding:4px 5px 4px 12px;display:flex}.nLMEza_goalGlyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}.nLMEza_label{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px}.nLMEza_objective{min-width:0;color:var(--dsw-alias-label-primary-dimmed);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:13px;line-height:20px;overflow:hidden}.nLMEza_error{min-width:0;color:var(--dsw-alias-state-error-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px;line-height:20px;overflow:hidden}.nLMEza_objectiveInput{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-base);min-width:0;height:26px;color:var(--dsw-alias-label-primary);border-radius:6px;outline:none;flex:1;padding:0 8px;font-size:13px;line-height:20px}.nLMEza_objectiveInput:focus{border-color:var(--dsw-alias-state-business-primary)}.nLMEza_objectiveInput::placeholder{color:var(--dsw-alias-label-caption)}.nLMEza_actions{flex:none;align-items:center;gap:10px;display:flex}.nLMEza_iconBtn{corner-shape:round;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;justify-content:center;align-items:center;padding:0;display:inline-flex}.nLMEza_iconBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.nLMEza_iconBtn:disabled{opacity:.4;cursor:default}";
12
128
  const tagId$1 = "@deepseek-ai/dsh-client-ui-goal/GoalBar.module.css";
@@ -33,11 +149,11 @@ window.__ModuleLoader__.load({
33
149
  /**
34
150
  * GoalBar: the goal indicator docked above the message composer (input dock
35
151
  * strip). A present goal shows a goal glyph, a phase label, the truncated
36
- * objective, and icon actions — resume when paused, edit (inline form in the
37
- * same strip), and clear. Goal creation lives on the `/goal` command, not
38
- * here: loading (undefined), no goal (null), and complete goals render
39
- * nothing. Live state arrives as the projected whole snapshot; the verbs are
40
- * the injected face.
152
+ * objective, and icon actions — resume when active-disarmed or paused, edit
153
+ * (inline form in the same strip), and clear. Goal creation lives on the
154
+ * `/goal` command, not here: loading (undefined), no goal (null), and complete
155
+ * goals render nothing. Durable state arrives as the projected whole snapshot;
156
+ * process-local activation arrives through the injected activation hook.
41
157
  */
42
158
  /** Strip label keys per visible phase; complete goals render nothing. */
43
159
  const PHASE_LABELS = {
@@ -45,7 +161,12 @@ window.__ModuleLoader__.load({
45
161
  paused: "phase.paused",
46
162
  blocked: "phase.blocked"
47
163
  };
48
- function GoalBar({ goal, onEdit, onPause, onResume, onClear, t }) {
164
+ /** Strip label for an active goal using its process-local activation. */
165
+ function activeLabel(activation, t) {
166
+ if (activation === "disarmed") return t("phase.active.disarmed");
167
+ return t(PHASE_LABELS.active);
168
+ }
169
+ function GoalBar({ goal, activation, onEdit, onPause, onResume, onClear, t }) {
49
170
  const [editing, setEditing] = (0, react.useState)(false);
50
171
  const [draft, setDraft] = (0, react.useState)("");
51
172
  const [pending, setPending] = (0, react.useState)(false);
@@ -143,6 +264,8 @@ window.__ModuleLoader__.load({
143
264
  })
144
265
  });
145
266
  const title = goal.phase === "blocked" ? goal.blockedReason?.message : void 0;
267
+ const label = goal.phase === "active" ? activeLabel(activation, t) : t(PHASE_LABELS[goal.phase]);
268
+ const showResume = goal.phase === "paused" || goal.phase === "active" && activation === "disarmed";
146
269
  return (0, react_jsx_runtime.jsx)("div", {
147
270
  className: GoalBar_module_css_default.dock,
148
271
  "data-goal-bar": true,
@@ -156,7 +279,7 @@ window.__ModuleLoader__.load({
156
279
  }),
157
280
  (0, react_jsx_runtime.jsx)("span", {
158
281
  className: GoalBar_module_css_default.label,
159
- children: t(PHASE_LABELS[goal.phase])
282
+ children: label
160
283
  }),
161
284
  (0, react_jsx_runtime.jsx)("span", {
162
285
  className: GoalBar_module_css_default.objective,
@@ -170,7 +293,7 @@ window.__ModuleLoader__.load({
170
293
  (0, react_jsx_runtime.jsxs)("div", {
171
294
  className: GoalBar_module_css_default.actions,
172
295
  children: [
173
- goal.phase === "active" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
296
+ goal.phase === "active" && activation === "armed" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
174
297
  label: t("action.pause"),
175
298
  side: "bottom",
176
299
  delayMs: 500,
@@ -185,7 +308,7 @@ window.__ModuleLoader__.load({
185
308
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPauseOutline16, { size: 14 })
186
309
  })
187
310
  }),
188
- goal.phase === "paused" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
311
+ showResume && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
189
312
  label: t("action.resume"),
190
313
  side: "bottom",
191
314
  delayMs: 500,
@@ -237,11 +360,16 @@ window.__ModuleLoader__.load({
237
360
  })
238
361
  });
239
362
  }
240
- /** Dock adapter: reads the host-computed 'goal' projection (whole value; absent or null renders nothing). */
241
- function GoalDock({ useProjection, onEdit, onPause, onResume, onClear, t }) {
363
+ /** Dock adapter: overlays process-local activation on the durable goal projection. */
364
+ function GoalDock({ useProjection, useGoalActivation, onEdit, onPause, onResume, onClear, t }) {
242
365
  const projection = useProjection("goal");
366
+ const goal = projection === void 0 || projection === null ? projection : projection.goal;
367
+ const goalId = goal?.id;
368
+ const revision = goal?.revision;
369
+ const activation = useGoalActivation((next) => next.id === goalId && next.revision === revision ? next.activation : void 0);
243
370
  return (0, react_jsx_runtime.jsx)(GoalBar, {
244
- goal: projection === void 0 ? void 0 : projection === null ? null : projection.goal,
371
+ goal,
372
+ ...activation === void 0 ? {} : { activation },
245
373
  onEdit,
246
374
  onPause,
247
375
  onResume,
@@ -346,6 +474,7 @@ window.__ModuleLoader__.load({
346
474
  /** Simplified Chinese dictionary (the key-set source of truth). */
347
475
  const zh = {
348
476
  "phase.active": "进行中的目标",
477
+ "phase.active.disarmed": "未运行的目标",
349
478
  "phase.paused": "已暂停的目标",
350
479
  "phase.blocked": "受阻的目标",
351
480
  "objective.aria": "目标内容",
@@ -360,6 +489,7 @@ window.__ModuleLoader__.load({
360
489
  /** English dictionary, checked complete against the zh key set. */
361
490
  const en = {
362
491
  "phase.active": "Ongoing Goal",
492
+ "phase.active.disarmed": "Inactive Goal",
363
493
  "phase.paused": "Paused Goal",
364
494
  "phase.blocked": "Blocked Goal",
365
495
  "objective.aria": "Goal objective",
@@ -421,28 +551,41 @@ window.__ModuleLoader__.load({
421
551
  id: "goal",
422
552
  order: 10,
423
553
  locale: NS,
424
- inject: (sessionId) => ({
425
- onEdit: async (objective) => {
426
- const ref = refOf(sessionId);
427
- if (ref === void 0) return noCurrentGoal;
428
- return await ctx.remote.goals.edit(sessionId, ref, { objective });
429
- },
430
- onPause: async () => {
431
- const ref = refOf(sessionId);
432
- if (ref === void 0) return noCurrentGoal;
433
- return await ctx.remote.goals.pause(sessionId, ref);
434
- },
435
- onResume: async () => {
436
- const ref = refOf(sessionId);
437
- if (ref === void 0) return noCurrentGoal;
438
- return await ctx.remote.goals.resume(sessionId, ref);
439
- },
440
- onClear: async () => {
441
- const ref = refOf(sessionId);
442
- if (ref === void 0) return noCurrentGoal;
443
- return await ctx.remote.goals.clear(sessionId, ref);
444
- }
445
- })
554
+ inject: (sessionId) => {
555
+ const binding = sessions.binding(sessionId);
556
+ if (binding === void 0) throw new Error(`ui-goal: session "${sessionId}" is unavailable`);
557
+ return {
558
+ hooks: { goalActivation: createGoalActivationSource({
559
+ projection: binding.session.projections.faceOf("goal"),
560
+ session: binding.session,
561
+ getGoal: () => ctx.remote.goals.get(sessionId),
562
+ subscribeActivation: (listener) => ctx.remote.$on("goal/activation-changed", (event) => {
563
+ if (event.sessionId === sessionId) listener(event.goal);
564
+ }),
565
+ subscribeReset: (listener) => ctx.on("connection/reset", listener)
566
+ }) },
567
+ onEdit: async (objective) => {
568
+ const ref = refOf(sessionId);
569
+ if (ref === void 0) return noCurrentGoal;
570
+ return await ctx.remote.goals.edit(sessionId, ref, { objective });
571
+ },
572
+ onPause: async () => {
573
+ const ref = refOf(sessionId);
574
+ if (ref === void 0) return noCurrentGoal;
575
+ return await ctx.remote.goals.pause(sessionId, ref);
576
+ },
577
+ onResume: async () => {
578
+ const ref = refOf(sessionId);
579
+ if (ref === void 0) return noCurrentGoal;
580
+ return await ctx.remote.goals.resume(sessionId, ref);
581
+ },
582
+ onClear: async () => {
583
+ const ref = refOf(sessionId);
584
+ if (ref === void 0) return noCurrentGoal;
585
+ return await ctx.remote.goals.clear(sessionId, ref);
586
+ }
587
+ };
588
+ }
446
589
  }, GoalDock));
447
590
  }
448
591
  //#endregion
@@ -1,22 +1,24 @@
1
1
  /**
2
2
  * GoalBar: the goal indicator docked above the message composer (input dock
3
3
  * strip). A present goal shows a goal glyph, a phase label, the truncated
4
- * objective, and icon actions — resume when paused, edit (inline form in the
5
- * same strip), and clear. Goal creation lives on the `/goal` command, not
6
- * here: loading (undefined), no goal (null), and complete goals render
7
- * nothing. Live state arrives as the projected whole snapshot; the verbs are
8
- * the injected face.
4
+ * objective, and icon actions — resume when active-disarmed or paused, edit
5
+ * (inline form in the same strip), and clear. Goal creation lives on the
6
+ * `/goal` command, not here: loading (undefined), no goal (null), and complete
7
+ * goals render nothing. Durable state arrives as the projected whole snapshot;
8
+ * process-local activation arrives through the injected activation hook.
9
9
  */
10
- import type { GoalSnapshot } from '@deepseek-ai/dsh-goal/client';
11
- import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
- import type { GoalBarActions } from './slots.ts';
10
+ import type { GoalActivation, GoalSnapshot } from '@deepseek-ai/dsh-goal/client';
11
+ import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { GoalBarActions, GoalBarInjected } from './slots.ts';
13
13
  export interface GoalBarProps extends GoalBarActions {
14
14
  /** Current goal snapshot; undefined = capability absent or loading, null = no goal set. */
15
15
  goal: GoalSnapshot | null | undefined;
16
+ /** Process-local continuation activation; absent while the live read is pending. */
17
+ activation?: GoalActivation;
16
18
  }
17
- export declare function GoalBar({ goal, onEdit, onPause, onResume, onClear, t }: GoalBarProps & PropsLocale<'goal'>): import("react").JSX.Element | null;
18
- /** Full props of the dock entry: InputZone owner share + session standard kit + injected verbs + the locale seat. */
19
- export type GoalDockProps = import('@deepseek-ai/dsh-client-ui-slots').PropsRuntime<'conversation.input.dock'> & GoalBarActions & PropsLocale<'goal'>;
20
- /** Dock adapter: reads the host-computed 'goal' projection (whole value; absent or null renders nothing). */
21
- export declare function GoalDock({ useProjection, onEdit, onPause, onResume, onClear, t }: GoalDockProps): import("react").JSX.Element;
19
+ export declare function GoalBar({ goal, activation, onEdit, onPause, onResume, onClear, t }: GoalBarProps & PropsLocale<'goal'>): import("react").JSX.Element | null;
20
+ /** Full props of the dock entry: InputZone owner share + injected verbs/activation hook + the locale seat. */
21
+ export type GoalDockProps = import('@deepseek-ai/dsh-client-ui-slots').PropsRuntime<'conversation.input.dock'> & InjectFace<GoalBarInjected> & PropsLocale<'goal'>;
22
+ /** Dock adapter: overlays process-local activation on the durable goal projection. */
23
+ export declare function GoalDock({ useProjection, useGoalActivation, onEdit, onPause, onResume, onClear, t, }: GoalDockProps): import("react").JSX.Element;
22
24
  //# sourceMappingURL=GoalBar.d.ts.map
@@ -0,0 +1,29 @@
1
+ /** Goal activation observable that orders Remote reads and live activation events. */
2
+ import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client';
3
+ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import type { GoalActivationChanged, GoalProjection, GoalView } from '@deepseek-ai/dsh-goal/client';
5
+ import type { GoalActivationSnapshot } from './slots.ts';
6
+ /** Live inputs for one Session's goal activation source. */
7
+ export interface GoalActivationDeps {
8
+ /** Durable projection used to derive the current active goal ref. */
9
+ readonly projection: HostObservable<GoalProjection | null | undefined>;
10
+ /** Session snapshot; running flips trigger a fresh authoritative read. */
11
+ readonly session: HostObservable<{
12
+ readonly running: boolean;
13
+ }>;
14
+ /** Read the current live goal at call time. */
15
+ readonly getGoal: () => Promise<RemoteResult<GoalView | undefined>>;
16
+ /** Subscribe to activation edges after the transport delivers them. */
17
+ readonly subscribeActivation: (listener: (goal: GoalActivationChanged['goal']) => void) => () => void;
18
+ /** Refresh after a connection-generation reset. */
19
+ readonly subscribeReset: (listener: () => void) => () => void;
20
+ }
21
+ /**
22
+ * Create one registrant-private activation source. The source subscribes only
23
+ * while a framework hook observes it, so unmount releases the Remote event,
24
+ * projection, running-snapshot, and reset listeners.
25
+ * @param deps - projection, session, Remote read, and live-event inputs.
26
+ * @returns stable snapshot source consumed by `useGoalActivation`.
27
+ */
28
+ export declare function createGoalActivationSource(deps: GoalActivationDeps): HostObservable<GoalActivationSnapshot>;
29
+ //# sourceMappingURL=activation-source.d.ts.map
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * Goal surface plugin, browser half: the GoalBar entry in the
3
- * conversation.input.dock strip. Projection-mode surface the live goal
4
- * arrives through `useProjection('goal')` (seeded by the history tail page,
5
- * updated by session/projection frames), so this plugin owns no store, no
6
- * refresh chain, and no event listener. The inject face carries only the
7
- * four mutation verbs through the generated Goal Remote API;
8
- * their CAS ref reads the session's current projected value at call time.
3
+ * conversation.input.dock strip. The durable goal arrives through
4
+ * `useProjection('goal')`. A registrant-private activation hook source owns
5
+ * the live Remote read and event subscription; the inject face carries that
6
+ * hook plus the four mutation verbs through the generated Goal Remote API.
9
7
  * This plugin does not create goals; deployments may expose /goal separately.
10
8
  */
11
9
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
10
  import { type GoalKey } from './locales.ts';
13
11
  export { GoalBar, GoalDock } from './GoalBar.tsx';
14
- export type { GoalActionResult, GoalBarActions } from './slots.ts';
12
+ export type { GoalActionResult, GoalBarActions, } from './slots.ts';
15
13
  export type { GoalKey } from './locales.ts';
16
14
  declare module '@deepseek-ai/dsh-client-ui-slots' {
17
15
  interface LocaleNamespaceMap {
@@ -2,6 +2,7 @@
2
2
  /** Simplified Chinese dictionary (the key-set source of truth). */
3
3
  export declare const zh: {
4
4
  'phase.active': string;
5
+ 'phase.active.disarmed': string;
5
6
  'phase.paused': string;
6
7
  'phase.blocked': string;
7
8
  'objective.aria': string;
@@ -18,6 +19,7 @@ export type GoalKey = keyof typeof zh;
18
19
  /** English dictionary, checked complete against the zh key set. */
19
20
  export declare const en: {
20
21
  'phase.active': string;
22
+ 'phase.active.disarmed': string;
21
23
  'phase.paused': string;
22
24
  'phase.blocked': string;
23
25
  'objective.aria': string;
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * GoalBar's injected face. The target 'conversation.input.dock' slot is
3
3
  * declared (children table) and typed by ui-conversation; this package only
4
- * contributes the entry, so no SlotMap merge lives here. The live goal value
5
- * is NOT part of this face — it arrives through `useProjection('goal')`
6
- * (the framework standard kit); inject carries only the mutation verbs
7
- * (callbacks from inject, live state from useProjection).
4
+ * contributes the entry, so no SlotMap merge lives here. The durable goal
5
+ * value arrives through `useProjection('goal')` (the framework standard kit);
6
+ * the injected face carries mutation verbs and a registrant-private
7
+ * activation hook source.
8
8
  */
9
9
  import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client';
10
+ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
11
+ import type { GoalActivation, GoalId } from '@deepseek-ai/dsh-goal/client';
10
12
  /**
11
13
  * The one failure the strip reports without a wire call: the session projects
12
14
  * no goal, so no CAS ref exists to address a mutation to.
@@ -24,6 +26,21 @@ export interface GoalLocalFailure {
24
26
  * projection — so the success value stays unread here.
25
27
  */
26
28
  export type GoalActionResult = RemoteResult<unknown> | GoalLocalFailure;
29
+ /** Process-local activation matched to one exact goal revision. */
30
+ export interface GoalActivationSnapshot {
31
+ /** Exact current goal id, absent before create or after clear. */
32
+ readonly id?: GoalId;
33
+ /** Exact current revision for the activation edge. */
34
+ readonly revision?: number;
35
+ /** Process-local continuation state; absent while no matching edge is known. */
36
+ readonly activation?: GoalActivation;
37
+ }
38
+ /** Registrant-private observable source bound by the slot renderer. */
39
+ export interface GoalActivationInjected {
40
+ readonly hooks: {
41
+ readonly goalActivation: HostObservable<GoalActivationSnapshot>;
42
+ };
43
+ }
27
44
  /** Injected business face of the GoalBar dock entry: the mutation verbs (function properties: the strip destructures them freely). */
28
45
  export interface GoalBarActions {
29
46
  /**
@@ -38,4 +55,6 @@ export interface GoalBarActions {
38
55
  /** Clear the current goal (tombstone). */
39
56
  onClear: () => Promise<GoalActionResult>;
40
57
  }
58
+ /** Injected business face of the GoalBar dock entry. */
59
+ export type GoalBarInjected = GoalBarActions & GoalActivationInjected;
41
60
  //# sourceMappingURL=slots.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-goal",
3
3
  "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
4
- "version": "0.1.3-alpha.2",
4
+ "version": "0.1.5-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -48,21 +48,21 @@
48
48
  "@types/react": "~18.3.1",
49
49
  "react": "^18.2.0",
50
50
  "react-dom": "^18.2.0",
51
- "@deepseek-ai/dsh-api-remotes": "^0.1.3-alpha.2",
52
- "@deepseek-ai/dsh-api-session-controller": "^0.1.3-alpha.2",
53
- "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
54
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.3-alpha.2",
55
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.3-alpha.2",
56
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.3-alpha.2",
57
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.3-alpha.2",
58
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
59
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.3-alpha.2",
60
- "@deepseek-ai/dsh-commands": "^0.1.3-alpha.2",
61
- "@deepseek-ai/dsh-goal": "^0.1.3-alpha.2",
51
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.5-alpha.2",
52
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.2",
53
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.5-alpha.2",
54
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.5-alpha.2",
55
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.2",
56
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.2",
57
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.2",
58
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.2",
59
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-alpha.2",
60
+ "@deepseek-ai/dsh-commands": "^0.1.5-alpha.2",
61
+ "@deepseek-ai/dsh-goal": "^0.1.5-alpha.2",
62
62
  "@deepseek-ai/cordis": "^4.0.2",
63
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
64
- "@deepseek-ai/dsh-client-ui-session": "^0.1.3-alpha.2",
65
- "@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2"
63
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.2",
64
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.5-alpha.2",
65
+ "@deepseek-ai/dsh-api-remotes": "^0.1.5-alpha.2"
66
66
  },
67
67
  "files": [
68
68
  "lib/index.js",