dsh-ntr 0.1.0 → 0.1.1

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.
@@ -179,9 +179,10 @@ function PlanCard({ entries, t }) {
179
179
  return (_jsxs("div", { className: "ntr-acp-plan", children: [_jsx("div", { className: "ntr-acp-plan-head", children: t(`计划 ${done}/${entries.length}`, `Plan ${done}/${entries.length}`) }), entries.map((entry, index) => (_jsxs("div", { className: `ntr-acp-plan-row is-${entry.status}`, children: [_jsx("span", { className: "ntr-acp-plan-glyph", "aria-hidden": true, children: entry.status === 'completed' ? '✓' : entry.status === 'in_progress' ? '▸' : '○' }), _jsx("span", { children: entry.content }), entry.priority !== undefined && _jsx("span", { className: "ntr-acp-plan-prio", children: entry.priority })] }, index)))] }));
180
180
  }
181
181
  /**
182
- * One stream block, dispatched by kind. Memoized on its props — `block` and
183
- * the store's instance-bound `label` — so a folded batch re-renders only the
184
- * rows whose block object changed (`foldUpdate` replaces blocks immutably)
182
+ * One stream block, dispatched by kind. Memoized on its props — `block`, the
183
+ * store's instance-bound `label`, and `localeRev` (it moves on a language
184
+ * switch, so translated copy re-renders) — so a folded batch re-renders only
185
+ * the rows whose block object changed (`foldUpdate` replaces blocks immutably)
185
186
  * instead of the whole transcript on every store notification. `key` stays the
186
187
  * index on purpose: blocks fold append-only, and identity is the block object.
187
188
  */
@@ -327,5 +328,5 @@ export function AcpConversationView(props) {
327
328
  pinned.current = node.scrollHeight - node.scrollTop - node.clientHeight < 40;
328
329
  }, children: transcript.blocks.length === 0
329
330
  ? _jsx("div", { className: "ntr-acp-empty-hint", children: t('还没有消息:在下面的输入框里发给它。', 'Nothing yet — send it a message below.') })
330
- : transcript.blocks.map((block, index) => _jsx(BlockView, { block: block, label: acp.label }, index)) })] }));
331
+ : transcript.blocks.map((block, index) => _jsx(BlockView, { block: block, label: acp.label, localeRev: ui.localeRev }, index)) })] }));
331
332
  }
@@ -15,7 +15,7 @@ import { acpModeFromPresetEvent, rowAgentPreset, stagedIsAcpMode, stagedSessionO
15
15
  import { createSlashIsolation, slashAcpVerdict } from "./slash-isolation.js";
16
16
  export const name = 'dsh-ntr-client';
17
17
  export const inject = ['slots', 'connection'];
18
- const zh = typeof navigator !== 'undefined' && /^zh\b/i.test(navigator.language);
18
+ const browserPrefersZh = typeof navigator !== 'undefined' && /^zh\b/i.test(navigator.language);
19
19
  /**
20
20
  * How long a host verdict may stand as the gate's ONLY fact source before the
21
21
  * gate asks the host again.
@@ -50,8 +50,6 @@ export function apply(ctx) {
50
50
  throw new Error('dsh-ntr: browser connection.rpc is unavailable');
51
51
  }
52
52
  const call = (channel, endpoint, payload) => rpc.call(channel, endpoint, payload);
53
- const store = new AcpStore(call, (zhText, enText) => (zh ? zhText : enText));
54
- store.start();
55
53
  /**
56
54
  * Resolve one optional service without injecting it. A plain `ctx.get` at
57
55
  * call time keeps the plugin loadable when the service is absent AND picks it
@@ -59,6 +57,35 @@ export function apply(ctx) {
59
57
  * also making its absence fatal.
60
58
  */
61
59
  const service = (name) => ctx.get(name);
60
+ /**
61
+ * Resolve a Chinese/English pair against the shell's active locale. Read per
62
+ * call, so a language switch reaches every render-time `label()` call without
63
+ * rebuilding the store. A host without the locale service keeps the
64
+ * browser-derived choice the pairs shipped with.
65
+ */
66
+ const label = (zhText, enText) => {
67
+ const resolved = service('locale')?.resolveText?.({ zh: zhText, en: enText });
68
+ return typeof resolved === 'string' && resolved !== '' ? resolved : browserPrefersZh ? zhText : enText;
69
+ };
70
+ const store = new AcpStore(call, label);
71
+ store.start();
72
+ // A locale switch changes no data, only which side of each pair renders;
73
+ // republishing the snapshot is what re-runs them. Attached through
74
+ // `internal/service` so a late-arriving locale service is still hooked.
75
+ ctx.effect(() => {
76
+ let stop;
77
+ const attach = () => {
78
+ if (stop !== undefined)
79
+ return;
80
+ stop = service('locale')?.subscribe?.(() => { store.refreshLocale(); });
81
+ };
82
+ attach();
83
+ const stopService = ctx.on('internal/service', attach);
84
+ return () => {
85
+ stopService();
86
+ stop?.();
87
+ };
88
+ }, 'dsh-ntr: locale switch re-render');
62
89
  /** This session's command/projection face (`ctx.sessions`); `undefined` before it materializes. */
63
90
  const faceFor = (sessionId) => {
64
91
  try {
@@ -284,6 +284,7 @@ export class AcpStore {
284
284
  locale;
285
285
  state = {
286
286
  open: false,
287
+ localeRev: 0,
287
288
  stream: 'init',
288
289
  agents: [],
289
290
  sessions: [],
@@ -357,6 +358,8 @@ export class AcpStore {
357
358
  seatCatalogReads = new Set();
358
359
  /** In-flight `agent/cached-models` reads, keyed by agent id. */
359
360
  cachedModelReads = new Set();
361
+ /** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
362
+ detectCounts;
360
363
  constructor(rpc, locale) {
361
364
  this.rpc = rpc;
362
365
  this.locale = locale;
@@ -535,10 +538,11 @@ export class AcpStore {
535
538
  const result = await this.call('agents/detect');
536
539
  const agents = result.agents.length > 0 ? result.agents : [...DEFAULT_KNOWN_PRESETS];
537
540
  const installed = agents.filter(agent => agent.detected).length;
541
+ this.detectCounts = { total: agents.length, installed };
538
542
  this.mutate({
539
543
  detectedAgents: agents,
540
544
  detecting: false,
541
- detectSummary: this.locale(`已检测 ${String(agents.length)} 个预设:${String(installed)} 个已安装,其余可从「添加智能体」启用。`, `Detected ${String(agents.length)} presets: ${String(installed)} installed, enable the rest via "Add agent".`),
545
+ detectSummary: this.detectSummaryOf(agents.length, installed),
542
546
  });
543
547
  }
544
548
  catch (error) {
@@ -546,6 +550,7 @@ export class AcpStore {
546
550
  const notice = msg.includes('unknown endpoint agents/detect')
547
551
  ? this.locale('宿主服务运行着旧版本代码,请在终端重启 dsh web 服务以激活主机检测功能。', 'Host running older version. Please restart dsh web to enable agent detection.')
548
552
  : msg;
553
+ this.detectCounts = undefined;
549
554
  this.mutate({
550
555
  detecting: false,
551
556
  notice,
@@ -555,6 +560,9 @@ export class AcpStore {
555
560
  });
556
561
  }
557
562
  }
563
+ detectSummaryOf(total, installed) {
564
+ return this.locale(`已检测 ${String(total)} 个预设:${String(installed)} 个已安装,其余可从「添加智能体」启用。`, `Detected ${String(total)} presets: ${String(installed)} installed, enable the rest via "Add agent".`);
565
+ }
558
566
  async saveAgent(payload) {
559
567
  try {
560
568
  await this.call('agent/save', payload);
@@ -1366,6 +1374,19 @@ export class AcpStore {
1366
1374
  * renders or every row re-renders on every store notification.
1367
1375
  */
1368
1376
  label = (zh, en) => this.locale(zh, en);
1377
+ /**
1378
+ * Republish the snapshot after a locale switch. Every `label()` resolves
1379
+ * against the current language while rendering, so a notification is all
1380
+ * that is needed — except the detect summary, which the store owns as an
1381
+ * already-resolved string and rebuilds from its counts here.
1382
+ */
1383
+ refreshLocale() {
1384
+ const counts = this.detectCounts;
1385
+ this.mutate({
1386
+ localeRev: this.state.localeRev + 1,
1387
+ ...(counts === undefined ? {} : { detectSummary: this.detectSummaryOf(counts.total, counts.installed) }),
1388
+ });
1389
+ }
1369
1390
  clearNotice() {
1370
1391
  this.mutate({ notice: undefined });
1371
1392
  }
package/lib/client.js CHANGED
@@ -270,6 +270,7 @@ window.__ModuleLoader__.load({
270
270
  locale;
271
271
  state = {
272
272
  open: false,
273
+ localeRev: 0,
273
274
  stream: "init",
274
275
  agents: [],
275
276
  sessions: [],
@@ -343,6 +344,8 @@ window.__ModuleLoader__.load({
343
344
  seatCatalogReads = /* @__PURE__ */ new Set();
344
345
  /** In-flight `agent/cached-models` reads, keyed by agent id. */
345
346
  cachedModelReads = /* @__PURE__ */ new Set();
347
+ /** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
348
+ detectCounts;
346
349
  constructor(rpc, locale) {
347
350
  this.rpc = rpc;
348
351
  this.locale = locale;
@@ -495,14 +498,19 @@ window.__ModuleLoader__.load({
495
498
  const result = await this.call("agents/detect");
496
499
  const agents = result.agents.length > 0 ? result.agents : [...DEFAULT_KNOWN_PRESETS];
497
500
  const installed = agents.filter((agent) => agent.detected).length;
501
+ this.detectCounts = {
502
+ total: agents.length,
503
+ installed
504
+ };
498
505
  this.mutate({
499
506
  detectedAgents: agents,
500
507
  detecting: false,
501
- detectSummary: this.locale(`已检测 ${String(agents.length)} 个预设:${String(installed)} 个已安装,其余可从「添加智能体」启用。`, `Detected ${String(agents.length)} presets: ${String(installed)} installed, enable the rest via "Add agent".`)
508
+ detectSummary: this.detectSummaryOf(agents.length, installed)
502
509
  });
503
510
  } catch (error) {
504
511
  const msg = error instanceof Error ? error.message : String(error);
505
512
  const notice = msg.includes("unknown endpoint agents/detect") ? this.locale("宿主服务运行着旧版本代码,请在终端重启 dsh web 服务以激活主机检测功能。", "Host running older version. Please restart dsh web to enable agent detection.") : msg;
513
+ this.detectCounts = void 0;
506
514
  this.mutate({
507
515
  detecting: false,
508
516
  notice,
@@ -511,6 +519,9 @@ window.__ModuleLoader__.load({
511
519
  });
512
520
  }
513
521
  }
522
+ detectSummaryOf(total, installed) {
523
+ return this.locale(`已检测 ${String(total)} 个预设:${String(installed)} 个已安装,其余可从「添加智能体」启用。`, `Detected ${String(total)} presets: ${String(installed)} installed, enable the rest via "Add agent".`);
524
+ }
514
525
  async saveAgent(payload) {
515
526
  try {
516
527
  await this.call("agent/save", payload);
@@ -1254,6 +1265,19 @@ window.__ModuleLoader__.load({
1254
1265
  * renders or every row re-renders on every store notification.
1255
1266
  */
1256
1267
  label = (zh, en) => this.locale(zh, en);
1268
+ /**
1269
+ * Republish the snapshot after a locale switch. Every `label()` resolves
1270
+ * against the current language while rendering, so a notification is all
1271
+ * that is needed — except the detect summary, which the store owns as an
1272
+ * already-resolved string and rebuilds from its counts here.
1273
+ */
1274
+ refreshLocale() {
1275
+ const counts = this.detectCounts;
1276
+ this.mutate({
1277
+ localeRev: this.state.localeRev + 1,
1278
+ ...counts === void 0 ? {} : { detectSummary: this.detectSummaryOf(counts.total, counts.installed) }
1279
+ });
1280
+ }
1257
1281
  clearNotice() {
1258
1282
  this.mutate({ notice: void 0 });
1259
1283
  }
@@ -5436,9 +5460,10 @@ window.__ModuleLoader__.load({
5436
5460
  });
5437
5461
  }
5438
5462
  /**
5439
- * One stream block, dispatched by kind. Memoized on its props — `block` and
5440
- * the store's instance-bound `label` — so a folded batch re-renders only the
5441
- * rows whose block object changed (`foldUpdate` replaces blocks immutably)
5463
+ * One stream block, dispatched by kind. Memoized on its props — `block`, the
5464
+ * store's instance-bound `label`, and `localeRev` (it moves on a language
5465
+ * switch, so translated copy re-renders) — so a folded batch re-renders only
5466
+ * the rows whose block object changed (`foldUpdate` replaces blocks immutably)
5442
5467
  * instead of the whole transcript on every store notification. `key` stays the
5443
5468
  * index on purpose: blocks fold append-only, and identity is the block object.
5444
5469
  */
@@ -5685,7 +5710,8 @@ window.__ModuleLoader__.load({
5685
5710
  children: t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
5686
5711
  }) : transcript.blocks.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockView, {
5687
5712
  block,
5688
- label: acp.label
5713
+ label: acp.label,
5714
+ localeRev: ui.localeRev
5689
5715
  }, index))
5690
5716
  })]
5691
5717
  });
@@ -6237,7 +6263,7 @@ window.__ModuleLoader__.load({
6237
6263
  //#region src/client/index.tsx
6238
6264
  const name = "dsh-ntr-client";
6239
6265
  const inject = ["slots", "connection"];
6240
- const zh = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language);
6266
+ const browserPrefersZh = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language);
6241
6267
  /**
6242
6268
  * How long a host verdict may stand as the gate's ONLY fact source before the
6243
6269
  * gate asks the host again.
@@ -6269,8 +6295,6 @@ window.__ModuleLoader__.load({
6269
6295
  const rpc = ctx.get("connection")?.rpc;
6270
6296
  if (rpc === void 0) throw new Error("dsh-ntr: browser connection.rpc is unavailable");
6271
6297
  const call = (channel, endpoint, payload) => rpc.call(channel, endpoint, payload);
6272
- const store = new AcpStore(call, (zhText, enText) => zh ? zhText : enText);
6273
- store.start();
6274
6298
  /**
6275
6299
  * Resolve one optional service without injecting it. A plain `ctx.get` at
6276
6300
  * call time keeps the plugin loadable when the service is absent AND picks it
@@ -6278,6 +6302,36 @@ window.__ModuleLoader__.load({
6278
6302
  * also making its absence fatal.
6279
6303
  */
6280
6304
  const service = (name) => ctx.get(name);
6305
+ /**
6306
+ * Resolve a Chinese/English pair against the shell's active locale. Read per
6307
+ * call, so a language switch reaches every render-time `label()` call without
6308
+ * rebuilding the store. A host without the locale service keeps the
6309
+ * browser-derived choice the pairs shipped with.
6310
+ */
6311
+ const label = (zhText, enText) => {
6312
+ const resolved = service("locale")?.resolveText?.({
6313
+ zh: zhText,
6314
+ en: enText
6315
+ });
6316
+ return typeof resolved === "string" && resolved !== "" ? resolved : browserPrefersZh ? zhText : enText;
6317
+ };
6318
+ const store = new AcpStore(call, label);
6319
+ store.start();
6320
+ ctx.effect(() => {
6321
+ let stop;
6322
+ const attach = () => {
6323
+ if (stop !== void 0) return;
6324
+ stop = service("locale")?.subscribe?.(() => {
6325
+ store.refreshLocale();
6326
+ });
6327
+ };
6328
+ attach();
6329
+ const stopService = ctx.on("internal/service", attach);
6330
+ return () => {
6331
+ stopService();
6332
+ stop?.();
6333
+ };
6334
+ }, "dsh-ntr: locale switch re-render");
6281
6335
  /** This session's command/projection face (`ctx.sessions`); `undefined` before it materializes. */
6282
6336
  const faceFor = (sessionId) => {
6283
6337
  try {