dsh-ntr 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  * Deliberately framework-free: React binds through `useSyncExternalStore`.
7
7
  */
8
8
 
9
- import type { AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, ApprovalDeadlinesSetPayload, ApprovalDeadlinesResult, DetectedAgentInfo, PermissionTimeoutMode, SessionView } from '../protocol.ts'
9
+ import type { AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, ApprovalDeadlinesSetPayload, ApprovalDeadlinesResult, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, PluginVersionResult, SessionView } from '../protocol.ts'
10
10
  import { DEFAULT_TAG_HALO } from '../protocol.ts'
11
11
 
12
12
  /** One slash command the ACP agent declared, whole row (the menu draws name, description and `input.hint`). */
@@ -298,6 +298,18 @@ export interface UiState {
298
298
  * (`preset/status` answering `null`), in which case the panel hides the block.
299
299
  */
300
300
  preset?: AcpPresetStatus
301
+ /**
302
+ * The installed host package's version (`plugin/version`), rendered as the
303
+ * settings page badge. Absent on a host half without the endpoint, which
304
+ * hides the badge rather than naming a version this browser invented.
305
+ */
306
+ pluginVersion?: string
307
+ /**
308
+ * The running version and its update status (`plugin/update-check`). Absent
309
+ * on a host half without the endpoint, which hides the update entry instead
310
+ * of offering an install this browser cannot perform.
311
+ */
312
+ pluginUpdate?: PluginUpdateState
301
313
  }
302
314
 
303
315
  interface Listener { (): void }
@@ -309,6 +321,13 @@ const MAX_WATCHED = 6
309
321
  /** Raw JSON budget for one event block; the cut is marked. */
310
322
  const RAW_MAX = 256_000
311
323
 
324
+ /**
325
+ * How long a fetched update state stays fresh. The settings panel asks on every
326
+ * mount; without this, every open would hit the registry. A forced check (the
327
+ * card's own button) always goes out.
328
+ */
329
+ const PLUGIN_UPDATE_TTL_MS = 10 * 60 * 1000
330
+
312
331
  /** Pretty-printed JSON of an agent message (never throws, cuts only at {@link RAW_MAX} with a marker). */
313
332
  export function jsonText(value: unknown): string {
314
333
  let text: string
@@ -531,6 +550,14 @@ export const DEFAULT_KNOWN_PRESETS: DetectedAgentInfo[] = [
531
550
  defaultCommand: 'opencode',
532
551
  defaultArgs: ['acp'],
533
552
  },
553
+ {
554
+ id: 'cursor',
555
+ name: 'Cursor CLI',
556
+ description: 'Cursor CLI — run as `agent acp` over stdio (legacy alias `cursor-agent acp`)',
557
+ detected: false,
558
+ defaultCommand: 'agent',
559
+ defaultArgs: ['acp'],
560
+ },
534
561
  {
535
562
  id: 'qwen',
536
563
  name: 'Qwen Code',
@@ -646,6 +673,22 @@ export class AcpStore {
646
673
  private readonly cachedModelReads = new Set<string>()
647
674
  /** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
648
675
  private detectCounts: { total: number; installed: number } | undefined
676
+ /** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
677
+ private pluginUpdateFetchedAt = 0
678
+ /**
679
+ * The shared in-flight `plugin/update-check`. The settings section mounts
680
+ * twice (both entries) and the update popover asks too; without this, two
681
+ * mounts in the same tick each fire their own registry read.
682
+ */
683
+ private pluginUpdateInflight?: Promise<PluginUpdateState | undefined>
684
+ /** Re-entry guard for `plugin/update-apply` (a double click is one install). */
685
+ private pluginUpdateApplying = false
686
+ /**
687
+ * Update read generation. An apply bumps it so a check that was already in
688
+ * flight (started before the install) cannot land afterwards and overwrite
689
+ * the install's verdict with the pre-install `available`.
690
+ */
691
+ private pluginUpdateGeneration = 0
649
692
 
650
693
  constructor(private readonly rpc: RpcCall, private readonly locale: (zh: string, en: string) => string) {}
651
694
 
@@ -783,7 +826,7 @@ export class AcpStore {
783
826
  /** Snapshot refresh used on panel open and after a stream outage. */
784
827
  async resync(): Promise<void> {
785
828
  try {
786
- const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
829
+ const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
787
830
  this.call<{ agents: AgentView[] }>('agents/list'),
788
831
  this.call<{ sessions: SessionView[] }>('sessions/list'),
789
832
  // The plugin's OWN default ACP agent — a plugin-private id, not a DSH
@@ -797,6 +840,9 @@ export class AcpStore {
797
840
  // `undefined` (older half) hides the deadline card instead of showing a
798
841
  // number this browser made up.
799
842
  this.call<ApprovalDeadlinesResult>('ui/approval-deadlines').catch(() => ({ deadlines: undefined })),
843
+ // The settings badge's data source; older halves answer `unknown
844
+ // endpoint`, which hides the badge.
845
+ this.call<PluginVersionResult>('plugin/version').catch(() => null),
800
846
  ])
801
847
  const detectedAgents = (detectResult && detectResult.agents.length > 0)
802
848
  ? detectResult.agents
@@ -811,6 +857,7 @@ export class AcpStore {
811
857
  preset: presetResult?.preset ?? undefined,
812
858
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
813
859
  approvalDeadlines: deadlinesResult.deadlines,
860
+ pluginVersion: versionResult?.version !== undefined && versionResult.version !== '' ? versionResult.version : undefined,
814
861
  })
815
862
  const active = this.state.activeSessionId ?? sessions[0]?.sessionId
816
863
  if (active !== undefined) await this.loadTranscript(active)
@@ -819,6 +866,103 @@ export class AcpStore {
819
866
  }
820
867
  }
821
868
 
869
+ /**
870
+ * Read the plugin's update state (`plugin/update-check`). Throttled to ten
871
+ * minutes unless `force`: the settings panel asks on mount, and every
872
+ * remount must not hit the registry.
873
+ *
874
+ * An older host serves no such endpoint (`unknown endpoint`). That is a
875
+ * normal degradation, not an error worth a notice: the answer is `undefined`
876
+ * and the panel simply keeps its update entry hidden.
877
+ */
878
+ async checkPluginUpdate(force = false): Promise<PluginUpdateState | undefined> {
879
+ // One read for every caller while it is in flight, force included: the
880
+ // settings page and the popover mounting together must not hit the
881
+ // registry twice.
882
+ if (this.pluginUpdateInflight !== undefined) return this.pluginUpdateInflight
883
+ const current = this.state.pluginUpdate
884
+ if (!force && current !== undefined && Date.now() - this.pluginUpdateFetchedAt < PLUGIN_UPDATE_TTL_MS) {
885
+ return current
886
+ }
887
+ const generation = this.pluginUpdateGeneration
888
+ const inflight = this.readPluginUpdate(generation)
889
+ this.pluginUpdateInflight = inflight
890
+ try {
891
+ return await inflight
892
+ } finally {
893
+ this.pluginUpdateInflight = undefined
894
+ }
895
+ }
896
+
897
+ private async readPluginUpdate(generation: number): Promise<PluginUpdateState | undefined> {
898
+ try {
899
+ const pluginUpdate = await this.call<PluginUpdateState>('plugin/update-check')
900
+ // An install started while this read was in flight: its answer is
901
+ // already stale, and the install's verdict owns the state now.
902
+ if (generation !== this.pluginUpdateGeneration) return undefined
903
+ this.pluginUpdateFetchedAt = Date.now()
904
+ this.mutate({ pluginUpdate })
905
+ return pluginUpdate
906
+ } catch {
907
+ return undefined
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Install one exact version (`plugin/update-apply`). The status flips to
913
+ * `applying` first so the badge answers the click; the host's answer then
914
+ * becomes the state.
915
+ *
916
+ * A failed call does NOT prove the install did not happen: on an HMR host
917
+ * the update uninstalls the plugin before reinstalling it, which can cut the
918
+ * response. Whatever the error, the disk may already carry the new version,
919
+ * so the state becomes `restart-required` (the one neutral verdict for "the
920
+ * running code is not the disk's") with `installed` dropped (unknown, not a
921
+ * fact to display) and the notice asks for a reload rather than restoring
922
+ * `available` and pretending nothing changed.
923
+ */
924
+ async applyPluginUpdate(version: string): Promise<PluginUpdateState | undefined> {
925
+ if (this.pluginUpdateApplying) return undefined
926
+ this.pluginUpdateApplying = true
927
+ // Any check already in flight predates the install; its answer must not
928
+ // land on top of the install's verdict.
929
+ this.pluginUpdateGeneration += 1
930
+ const previous = this.state.pluginUpdate
931
+ if (previous !== undefined) {
932
+ this.mutate({ pluginUpdate: { ...previous, status: 'applying' } })
933
+ }
934
+ try {
935
+ const pluginUpdate = await this.call<PluginUpdateState>('plugin/update-apply', { version })
936
+ this.pluginUpdateFetchedAt = Date.now()
937
+ this.mutate({ pluginUpdate })
938
+ return pluginUpdate
939
+ } catch (error) {
940
+ const message = error instanceof Error ? error.message : String(error)
941
+ // The disk state is UNKNOWN here: the install may have completed before
942
+ // the response was cut. Keep `latest` for display, drop `installed` so no
943
+ // surface can claim a disk version we never read, and reset the throttle
944
+ // so the next check actually asks the host instead of serving this
945
+ // unknown verdict for ten minutes.
946
+ const interrupted: PluginUpdateState = previous !== undefined
947
+ ? { ...previous, status: 'restart-required', installed: undefined, message }
948
+ : { status: 'restart-required', current: this.state.pluginVersion ?? '?', message }
949
+ this.pluginUpdateFetchedAt = 0
950
+ this.mutate({
951
+ pluginUpdate: interrupted,
952
+ notice: this.locale(
953
+ '更新请求已中断;请刷新页面确认当前版本。',
954
+ 'The update request was interrupted. Reload the page to confirm the installed version.',
955
+ ),
956
+ })
957
+ return undefined
958
+ } finally {
959
+ // Invalidate reads that STARTED during the install: their "applying"
960
+ // answer would otherwise land on top of this call's final verdict.
961
+ this.pluginUpdateGeneration += 1
962
+ this.pluginUpdateApplying = false
963
+ }
964
+ }
965
+
822
966
  async detectAgents(): Promise<void> {
823
967
  this.mutate({ detecting: true })
824
968
  try {
package/src/impl.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { randomUUID } from './ids.ts'
9
9
  import type {
10
10
  AcpAgentCatalog, AcpAgentDef, AcpCatalogOption, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult,
11
- AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, RpcResult,
11
+ AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult,
12
12
  } from './protocol.ts'
13
13
  import { DEFAULT_TAG_HALO } from './protocol.ts'
14
14
 
@@ -535,6 +535,22 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
535
535
  if (typeof staleCancelMaxRetries !== 'number' || !Number.isInteger(staleCancelMaxRetries) || staleCancelMaxRetries < 0 || staleCancelMaxRetries > 5) {
536
536
  throw new AcpError('bad_config', 'acp: staleCancelMaxRetries must be an integer in [0, 5]')
537
537
  }
538
+ // Stall-recovery windows (inactivity watchdog). Two lengths on purpose: a
539
+ // silent turn with the floor is a stuck agent, but a turn waiting on a TOOL
540
+ // call is a working agent, and the tool half may take as long as the guard's
541
+ // own tool-output wait. `0` means "off" / "fall back", never "expire now".
542
+ const turnStallMs = value.turnStallMs ?? 600_000
543
+ if (typeof turnStallMs !== 'number' || !Number.isInteger(turnStallMs) || turnStallMs < 0 || turnStallMs > 86_400_000) {
544
+ throw new AcpError('bad_config', 'acp: turnStallMs must be an integer in [0, 86400000] (0 = disable the watchdog)')
545
+ }
546
+ const turnStallToolMs = value.turnStallToolMs ?? 1_800_000
547
+ if (typeof turnStallToolMs !== 'number' || !Number.isInteger(turnStallToolMs) || turnStallToolMs < 0 || turnStallToolMs > 86_400_000) {
548
+ throw new AcpError('bad_config', 'acp: turnStallToolMs must be an integer in [0, 86400000] (0 = fall back to turnStallMs)')
549
+ }
550
+ const turnStallTickMs = value.turnStallTickMs ?? 30_000
551
+ if (typeof turnStallTickMs !== 'number' || !Number.isInteger(turnStallTickMs) || turnStallTickMs < 1 || turnStallTickMs > 60_000) {
552
+ throw new AcpError('bad_config', 'acp: turnStallTickMs must be an integer in [1, 60000]')
553
+ }
538
554
  return {
539
555
  agents,
540
556
  transcriptMaxEntries: max,
@@ -545,6 +561,9 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
545
561
  cancelFallbackMs,
546
562
  staleCancelRetryDelayMs,
547
563
  staleCancelMaxRetries,
564
+ turnStallMs,
565
+ turnStallToolMs,
566
+ turnStallTickMs,
548
567
  }
549
568
  }
550
569
 
@@ -1649,6 +1668,19 @@ export class PermissionLedger {
1649
1668
  return [...this.pending.values()].map(e => ({ requestId: e.requestId, sessionId: e.sessionId }))
1650
1669
  }
1651
1670
 
1671
+ /**
1672
+ * Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
1673
+ * (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
1674
+ * stall watchdog: a turn blocked on a human is not a silent turn, so the
1675
+ * session with a pending permission must not be finalized as stalled.
1676
+ */
1677
+ hasPendingFor(sessionId: string): boolean {
1678
+ for (const entry of this.pending.values()) {
1679
+ if (entry.sessionId === sessionId) return true
1680
+ }
1681
+ return false
1682
+ }
1683
+
1652
1684
  /**
1653
1685
  * Outstanding asks WITH the view the browser was shown, for the reconnect
1654
1686
  * replay. Entries created without a view are skipped: replaying a frame the
@@ -1874,6 +1906,24 @@ export interface AcpManager {
1874
1906
  * the registry service), which hides the panel block instead of guessing.
1875
1907
  */
1876
1908
  presetStatus?(): Promise<AcpPresetStatus | undefined>
1909
+ /**
1910
+ * The installed host package's version, reported for the settings page's
1911
+ * badge. `undefined` on a host half that predates the endpoint, which hides
1912
+ * the badge rather than showing a number the browser invented.
1913
+ */
1914
+ pluginVersion?(): string | undefined
1915
+ /**
1916
+ * Ask the host's plugin manager what is published for this package. Read-only
1917
+ * toward the installation: never downloads, never swaps files, never restarts
1918
+ * the host. Absent on a host half from before the updater existed.
1919
+ */
1920
+ pluginUpdateCheck?(): Promise<PluginUpdateState>
1921
+ /**
1922
+ * Install one exact stable version THROUGH the host's plugin manager. The
1923
+ * manager owns download/restart semantics; this plugin only relays. Absent on
1924
+ * an older host half.
1925
+ */
1926
+ pluginUpdateApply?(version: string): Promise<PluginUpdateState>
1877
1927
  }
1878
1928
 
1879
1929
  function ok<T>(value: T): RpcResult<T> {
@@ -2038,6 +2088,21 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
2038
2088
  }
2039
2089
  case 'preset/status':
2040
2090
  return ok({ preset: await manager.presetStatus?.() ?? null })
2091
+ case 'plugin/version':
2092
+ // Absent on a host half from before the badge existed: the browser
2093
+ // hides it instead of naming a version it cannot know.
2094
+ return ok({ version: manager.pluginVersion?.() })
2095
+ case 'plugin/update-check':
2096
+ // An older host half answers `unsupported` rather than failing: the
2097
+ // settings card then hides the update section.
2098
+ return ok(await (manager.pluginUpdateCheck?.() ?? { status: 'unsupported' as const, current: '' }))
2099
+ case 'plugin/update-apply': {
2100
+ const version = str(body, 'version', 64)
2101
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
2102
+ throw new AcpError('bad_payload', 'acp: version must be a stable semver (x.y.z)')
2103
+ }
2104
+ return ok(await (manager.pluginUpdateApply?.(version) ?? { status: 'unsupported' as const, current: '' }))
2105
+ }
2041
2106
  case 'agent/stop':
2042
2107
  await manager.stopAgent(str(body, 'agentId', 64)); return ok({ done: true })
2043
2108
  case 'default/agent':
package/src/index.ts CHANGED
@@ -8,10 +8,12 @@
8
8
  import type { Context } from '@deepseek-ai/cordis'
9
9
  import type {} from '@deepseek-ai/dsh-host-webserver'
10
10
  import type { ConnectionRpcHandler, HostConnectionHandle } from '@deepseek-ai/dsh-client-connection'
11
- import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from './bridge.ts'
11
+ import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager, PLUGIN_VERSION } from './bridge.ts'
12
12
  import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from './impl.ts'
13
13
  import type { AcpEvent, ApprovalDeadlineKey } from './protocol.ts'
14
14
  import type { RosterPresetRow, SessionLogLike } from './impl.ts'
15
+ import { UpdateService } from './update.ts'
16
+ import type { UpdateManagerLike } from './update.ts'
15
17
 
16
18
  export const name = 'dsh-ntr'
17
19
  // Deliberately minimal: every other host service is probed structurally
@@ -86,9 +88,22 @@ export function apply(ctx: Context, config?: unknown): void {
86
88
  cancelFallbackMs: resolved.cancelFallbackMs,
87
89
  staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
88
90
  staleCancelMaxRetries: resolved.staleCancelMaxRetries,
91
+ turnStallMs: resolved.turnStallMs,
92
+ turnStallToolMs: resolved.turnStallToolMs,
93
+ turnStallTickMs: resolved.turnStallTickMs,
89
94
  },
90
95
  )
91
96
  const adapter = new AcpLlmAdapter(manager)
97
+ // Built-in update: the plugin never downloads or swaps files itself. The
98
+ // harness plugin manager owns the install; this only relays its facts.
99
+ // `resolveManager` re-probes on every call so a late-loading service is
100
+ // picked up without an `inject` dependency (a host without it must still
101
+ // load this plugin, and then reports `unsupported`).
102
+ manager.setUpdateService(new UpdateService({
103
+ resolveManager: () => (typeof ctx.get === 'function' ? ctx.get('pluginManager') : undefined) as UpdateManagerLike | undefined,
104
+ currentVersion: PLUGIN_VERSION,
105
+ isBusy: () => manager.hasActiveTurns(),
106
+ }))
92
107
  // The RAW patch-file deadlines, before validation filled the defaults in.
93
108
  // `validateConfig` returns a number for every knob, so the validated object
94
109
  // cannot tell "the deployment asked for 10 minutes" from "the deployment said
package/src/protocol.ts CHANGED
@@ -238,6 +238,50 @@ export interface AcpPluginConfig {
238
238
  * up and reports the `cancelled` stop as-is. Default 2.
239
239
  */
240
240
  staleCancelMaxRetries?: number
241
+ /**
242
+ * Inactivity watchdog for a turn the agent owes an answer: how long the turn
243
+ * may stay SILENT before the host finalizes it as stalled.
244
+ *
245
+ * The clock is `LiveSession.lastAgentActivityAt`, refreshed by ANY
246
+ * `session/update`, at every turn start, at each segment start (the host
247
+ * handing the turn back after a card), and when a permission ask arrives or
248
+ * is resolved. The watchdog only READS it while the turn is watched: an OPEN
249
+ * agent segment (always true on the `/acp`-RPC path, which has no driver
250
+ * stream) and no pending approval. The host-card execution phase and an
251
+ * unanswered ask are exempt by that gate, not because the clock is frozen —
252
+ * they legitimately move it too.
253
+ *
254
+ * Firing finalizes the TURN only (an `agent/notice` plus the usual turn
255
+ * end); it does NOT kill the agent process. The wedged `session/prompt` is
256
+ * abandoned, and the next message reclaims the session: parked with
257
+ * `releaseAll` and resumed with `session/load`, or reset outright when the
258
+ * agent declares no `loadSession` (the context is gone and the user is
259
+ * told).
260
+ *
261
+ * Default 600000 ms (10 minutes); `0` disables the WHOLE watchdog, the tool
262
+ * window included.
263
+ */
264
+ turnStallMs?: number
265
+ /**
266
+ * The longer silence window applied while ANY ACP tool call is still active
267
+ * by `toolCallId` — parallel calls are tracked as a set, so the window lasts
268
+ * until the LAST of them finishes. The agent is working on its own clock in
269
+ * there, so the plain turn window would call a healthy long-running tool
270
+ * stalled.
271
+ *
272
+ * Default 1800000 ms (30 minutes); `0` falls back to
273
+ * {@link AcpPluginConfig.turnStallMs}. It matches
274
+ * `TOOL_OUTPUT_WAIT_MS = 30 * 60 * 1000` in `presets/acp/acp-guard.mjs`, so
275
+ * the two halves of one turn agree on how long a tool may take.
276
+ */
277
+ turnStallToolMs?: number
278
+ /**
279
+ * How often the watchdog scans for silent turns, in milliseconds. A tuning /
280
+ * test knob only: NO settings-page control exists for it — and none for the
281
+ * two windows above either; the whole watchdog is configured in the patch
282
+ * file. Default 30000 ms.
283
+ */
284
+ turnStallTickMs?: number
241
285
  }
242
286
 
243
287
  // ---- Host → browser events (`/api/acp/stream`, SSE) ----
@@ -564,6 +608,38 @@ export interface AgentIdPayload { agentId: string }
564
608
  export interface DefaultAgentSetPayload { agentId?: string }
565
609
  /** `default/agent`: the plugin's own default agent id, or `undefined` when unset. */
566
610
  export type DefaultAgentResult = { agentId?: string }
611
+ /** `plugin/version`: the installed host package's version, for the settings badge. */
612
+ export type PluginVersionResult = { version?: string }
613
+
614
+ /**
615
+ * Where the plugin's own update check stands. The host half only ever reports
616
+ * facts it can read from the harness plugin manager and the npm registry — it
617
+ * never downloads, swaps files, or restarts itself.
618
+ */
619
+ export type PluginUpdateStatus =
620
+ | 'unsupported' | 'not-installed' | 'source-install' | 'managed'
621
+ | 'current' | 'available' | 'applying' | 'busy'
622
+ | 'applied' | 'restart-required' | 'failed'
623
+
624
+ /**
625
+ * One update-check answer, shared by `plugin/update-check` and
626
+ * `plugin/update-apply`.
627
+ */
628
+ export interface PluginUpdateState {
629
+ status: PluginUpdateStatus
630
+ /** 运行中(内存里)的插件版本 */
631
+ current: string
632
+ /** 磁盘上装着的版本(listBundles().version),可读时才有 */
633
+ installed?: string
634
+ /** profile registry 链上最新的稳定版 */
635
+ latest?: string
636
+ /** latest 的 npm 发布时间 ISO */
637
+ publishedAt?: string
638
+ /** 实际按顺序问过的 registry */
639
+ registries?: string[]
640
+ /** 失败/额外说明(英文技术信息,UI 自己负责本地化) */
641
+ message?: string
642
+ }
567
643
 
568
644
  /**
569
645
  * How the composer-edge agent tag shows liveness (the blue halo). `breath`