@climber47/dsh-step-clock 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.
package/README.md CHANGED
@@ -63,7 +63,7 @@ A dsh bundle, mounted as one inserted row:
63
63
  - `cordis.patch.yml` inserts the `step-clock` row.
64
64
  - `lib/index.js` is the (intentionally empty) host half. A bundle's row resolves the package root, so the package must be importable; this plugin has no host behaviour.
65
65
  - `lib/client.js` is the browser half, in the `window.__ModuleLoader__.load({ id, factory })` registration shape a client bundle must have. React is taken from the module loader via `require('react')` rather than bundled.
66
- - It registers two additive entries `conversation.input.dock` above the composer and `conversation.composer.dock` below it — both `replaceRisk: none`, so every shipped todo / goal / queue / stats entry is untouched. It owns its stylesheet through `ctx.styles.insert`, disposed with its fiber.
66
+ - It registers one additive entry in `conversation.input.dock` (`replaceRisk: none`, so every shipped todo / goal / queue entry is untouched) and injects its stylesheet by creating a tagged `<style>` element, removed when the plugin unloads.
67
67
  - The shipped bundle is generated from `src/client/` by `npm run build`, so the readable source and the artifact cannot drift; `npm test` rebuilds and drives the bundle through its real loader contract.
68
68
 
69
69
  It reads only facts the engine already publishes — the Chat timeline snapshot and the live running-call list — and polls nothing itself.
package/README.zh.md CHANGED
@@ -63,7 +63,8 @@ dsh plugin --profile web add @climber47/dsh-step-clock
63
63
  - `cordis.patch.yml` 插入 `step-clock` 行。
64
64
  - `lib/index.js` 是(有意为空的)宿主半边。bundle 的 insert 行会解析包根,所以包必须可被导入;本插件没有宿主行为。
65
65
  - `lib/client.js` 是浏览器半边,采用客户端 bundle 必须的 `window.__ModuleLoader__.load({ id, factory })` 注册形状。React 通过 `require('react')` 从模块加载器取得,不打包进产物。
66
- - 它注册**两个纯新增**条目——输入框上方的 `conversation.input.dock` 与下方的 `conversation.composer.dock`,两者都是 `replaceRisk: none`,不会动自带的 todo / goal / queue / stats 条目。样式通过 `ctx.styles.insert` 归属自身,随 fiber 一起销毁。
66
+ - 它只注册**一个纯新增**条目:输入框上方的 `conversation.input.dock`(`replaceRisk: none`,不会动自带的 todo / goal / queue 条目)。样式通过创建一个带标记的 `<style>` 元素注入,插件卸载时移除。
67
+ - 它只声明**真实存在**的服务(`slots`、`timer`)。客户端插件没有样式服务——`styles` 只存在于动态插件沙箱里。声明一个无人提供的服务会让 Cordis 无限期等待,插件会显示"已加载"、不报任何错,却永远不渲染。
67
68
  - 发布的 bundle 由 `npm run build` 从 `src/client/` 生成,因此可读源码与产物不会漂移;`npm test` 会先重新构建,再把 bundle 喂进它真实的加载器契约里跑行为测试。
68
69
 
69
70
  它只读取引擎**已经发布**的事实——Chat 时间线快照与运行中调用列表——自身不做任何轮询。
package/lib/client.js CHANGED
@@ -267,7 +267,7 @@ function StepClock(props) {
267
267
  if (typeof began === 'number' && typeof ended === 'number' && ended >= began) {
268
268
  if (record.seenTurn !== candidate.turn || record.seenStep !== last.step) {
269
269
  record.final = record.pending
270
- record.pending = { turn: candidate.turn, step: last.step, ms: ended - began, closedAt: now }
270
+ record.pending = { turn: candidate.turn, step: last.step, ms: ended - began }
271
271
  record.seenTurn = candidate.turn
272
272
  record.seenStep = last.step
273
273
  }
@@ -300,7 +300,9 @@ function StepClock(props) {
300
300
  say = '已提交,正在等待模型响应,已等待 ' + humanDuration(elapsedMs)
301
301
  where = '第 ' + stepNumber + ' 步'
302
302
  }
303
- clock = clockOf(elapsedMs)
303
+ // An unknown anchor must not render as `0:00`, which would read as
304
+ // "just started" when in fact the start is not known.
305
+ clock = anchor === null ? '--:--' : clockOf(elapsedMs)
304
306
  } else if (shown !== null) {
305
307
  // No step is running (yet). Keep reporting the previous step, so the gap
306
308
  // between a Turn closing and the next step starting — and a fresh page load
@@ -346,13 +348,44 @@ function StepClock(props) {
346
348
  }
347
349
 
348
350
  /**
349
- * Services this plugin waits for before applying.
351
+ * Cordis services this plugin waits for before applying.
350
352
  *
351
- * Declared rather than assumed: `styles` and `slots` are owned by the renderer
352
- * module, and Cordis parks this plugin until they exist instead of calling
353
- * `apply` with a half-built context. `timer` supplies the ticking interval.
353
+ * Only real services belong here. Cordis parks a plugin whose declared service
354
+ * is missing and **waits indefinitely** it does not throw so adding a name
355
+ * that no provider offers silently produces a plugin that loads, reports no
356
+ * error, and never runs. `slots` owns registration; `timer` supplies the
357
+ * ticking interval.
358
+ *
359
+ * Notably absent: there is no `styles` service. Stylesheet insertion is done
360
+ * directly on the DOM below, which is what the shipped client plugins do.
361
+ */
362
+ const inject = ['slots', 'timer']
363
+
364
+ /** Style-tag identity, so re-mounting a second copy reuses one tag. */
365
+ const STYLE_TAG = '@climber47/dsh-step-clock/step-clock.css'
366
+
367
+ /**
368
+ * Insert the plugin's stylesheet once, returning its disposer.
369
+ *
370
+ * A client plugin has no stylesheet service to hand this to: the style element
371
+ * is created and owned by this plugin, tagged so a second instance reuses it,
372
+ * and removed when the plugin unloads.
373
+ * @returns a disposer removing the tag, or a no-op outside a browser.
354
374
  */
355
- const inject = ['slots', 'styles', 'timer']
375
+ function insertStyles() {
376
+ if (typeof document === 'undefined') return function () {}
377
+ if (document.querySelector('style[data-plugin-css="' + STYLE_TAG + '"]') !== null) {
378
+ return function () {}
379
+ }
380
+ const tag = document.createElement('style')
381
+ tag.dataset.plugin = '@climber47/dsh-step-clock'
382
+ tag.dataset.pluginCss = STYLE_TAG
383
+ tag.textContent = CSS
384
+ document.head.appendChild(tag)
385
+ return function () {
386
+ tag.remove()
387
+ }
388
+ }
356
389
 
357
390
  /**
358
391
  * Register the dock entry.
@@ -362,9 +395,7 @@ const inject = ['slots', 'styles', 'timer']
362
395
  * `id` is added beside the shipped entries rather than replacing any of them.
363
396
  */
364
397
  function apply(ctx) {
365
- ctx.effect(function () {
366
- return ctx.styles.insert(CSS)
367
- })
398
+ ctx.effect(insertStyles)
368
399
  const mount = function (slot, id, order, label) {
369
400
  ctx.slots.inject(slot, function () {
370
401
  return ctx.slots.register({ name: slot, id: id, order: order }, function (props) {
@@ -382,6 +413,7 @@ function apply(ctx) {
382
413
  //#endregion
383
414
  exports.StepClock = StepClock;
384
415
  exports.CSS = CSS;
416
+ exports.insertStyles = insertStyles;
385
417
  exports.apply = apply;
386
418
  exports.inject = inject;
387
419
  return module.exports;
package/lib/index.js CHANGED
@@ -13,7 +13,12 @@
13
13
  * @module @climber47/dsh-step-clock
14
14
  */
15
15
 
16
- /** No services are required: the host half registers nothing. */
16
+ /**
17
+ * No host services are required.
18
+ *
19
+ * The browser half declares its own (`slots`, `timer`); see
20
+ * `src/client/index.js`. Nothing here should be read as describing it.
21
+ */
17
22
  export const inject = []
18
23
 
19
24
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@climber47/dsh-step-clock",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Live per-step elapsed-time clock for the DeepSeek Harness web GUI: shows which step is running, which tools it is running, and how long it has been going.",
5
5
  "keywords": [
6
6
  "deepseek",
@@ -231,7 +231,7 @@ export function StepClock(props) {
231
231
  if (typeof began === 'number' && typeof ended === 'number' && ended >= began) {
232
232
  if (record.seenTurn !== candidate.turn || record.seenStep !== last.step) {
233
233
  record.final = record.pending
234
- record.pending = { turn: candidate.turn, step: last.step, ms: ended - began, closedAt: now }
234
+ record.pending = { turn: candidate.turn, step: last.step, ms: ended - began }
235
235
  record.seenTurn = candidate.turn
236
236
  record.seenStep = last.step
237
237
  }
@@ -264,7 +264,9 @@ export function StepClock(props) {
264
264
  say = '已提交,正在等待模型响应,已等待 ' + humanDuration(elapsedMs)
265
265
  where = '第 ' + stepNumber + ' 步'
266
266
  }
267
- clock = clockOf(elapsedMs)
267
+ // An unknown anchor must not render as `0:00`, which would read as
268
+ // "just started" when in fact the start is not known.
269
+ clock = anchor === null ? '--:--' : clockOf(elapsedMs)
268
270
  } else if (shown !== null) {
269
271
  // No step is running (yet). Keep reporting the previous step, so the gap
270
272
  // between a Turn closing and the next step starting — and a fresh page load
@@ -310,13 +312,44 @@ export function StepClock(props) {
310
312
  }
311
313
 
312
314
  /**
313
- * Services this plugin waits for before applying.
315
+ * Cordis services this plugin waits for before applying.
314
316
  *
315
- * Declared rather than assumed: `styles` and `slots` are owned by the renderer
316
- * module, and Cordis parks this plugin until they exist instead of calling
317
- * `apply` with a half-built context. `timer` supplies the ticking interval.
317
+ * Only real services belong here. Cordis parks a plugin whose declared service
318
+ * is missing and **waits indefinitely** it does not throw so adding a name
319
+ * that no provider offers silently produces a plugin that loads, reports no
320
+ * error, and never runs. `slots` owns registration; `timer` supplies the
321
+ * ticking interval.
322
+ *
323
+ * Notably absent: there is no `styles` service. Stylesheet insertion is done
324
+ * directly on the DOM below, which is what the shipped client plugins do.
325
+ */
326
+ export const inject = ['slots', 'timer']
327
+
328
+ /** Style-tag identity, so re-mounting a second copy reuses one tag. */
329
+ const STYLE_TAG = '@climber47/dsh-step-clock/step-clock.css'
330
+
331
+ /**
332
+ * Insert the plugin's stylesheet once, returning its disposer.
333
+ *
334
+ * A client plugin has no stylesheet service to hand this to: the style element
335
+ * is created and owned by this plugin, tagged so a second instance reuses it,
336
+ * and removed when the plugin unloads.
337
+ * @returns a disposer removing the tag, or a no-op outside a browser.
318
338
  */
319
- export const inject = ['slots', 'styles', 'timer']
339
+ function insertStyles() {
340
+ if (typeof document === 'undefined') return function () {}
341
+ if (document.querySelector('style[data-plugin-css="' + STYLE_TAG + '"]') !== null) {
342
+ return function () {}
343
+ }
344
+ const tag = document.createElement('style')
345
+ tag.dataset.plugin = '@climber47/dsh-step-clock'
346
+ tag.dataset.pluginCss = STYLE_TAG
347
+ tag.textContent = CSS
348
+ document.head.appendChild(tag)
349
+ return function () {
350
+ tag.remove()
351
+ }
352
+ }
320
353
 
321
354
  /**
322
355
  * Register the dock entry.
@@ -326,9 +359,7 @@ export const inject = ['slots', 'styles', 'timer']
326
359
  * `id` is added beside the shipped entries rather than replacing any of them.
327
360
  */
328
361
  export function apply(ctx) {
329
- ctx.effect(function () {
330
- return ctx.styles.insert(CSS)
331
- })
362
+ ctx.effect(insertStyles)
332
363
  const mount = function (slot, id, order, label) {
333
364
  ctx.slots.inject(slot, function () {
334
365
  return ctx.slots.register({ name: slot, id: id, order: order }, function (props) {