@huanlin/dsh-plugin-yet-another-subagent 0.1.5 → 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.
package/lib/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import z from "schemastery";
2
2
  import { assertSubagentMaxDepth, settleRun } from "@deepseek-ai/dsh-subagent";
3
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
4
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
4
  import { constants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
6
5
  import { copyFile, readFile, readdir, stat, writeFile } from "node:fs/promises";
@@ -521,17 +520,29 @@ function buildTool(profiles, ctx) {
521
520
  //#endregion
522
521
  //#region src/repair.ts
523
522
  /**
524
- * One-shot session-log repair: stamp `"ignorable": true` onto legacy
525
- * `ya-subagent/started` events so the harness persistence read path
526
- * (`assertEventsSupported`) will skip them instead of refusing the whole log.
523
+ * One-shot session-log repair: physically REMOVE legacy `ya-subagent/started`
524
+ * event rows so the harness persistence read path (`assertEventsSupported`)
525
+ * loads the log again.
527
526
  *
528
- * Background: older plugin versions wrote `ya-subagent/started` via
529
- * `session.append(...)`, but `session.append` cannot set the `ignorable`
530
- * envelope flag, and `KNOWN_SESSION_EVENT_TYPES` is code-generated with no
531
- * plugin registration surface. The read path therefore refuses any log
532
- * containing the type unless each occurrence carries `ignorable: true`.
533
- * This module rewrites on-disk artifacts in place (after a `.bak` backup) to
534
- * add that flag to every `ya-subagent/started` row missing it.
527
+ * Background: plugin versions ≤0.1.2 appended `ya-subagent/started` via
528
+ * `session.append(...)`. `KNOWN_SESSION_EVENT_TYPES` is code-generated with no
529
+ * plugin registration surface, and v0.1.2-alpha.1 refuses EVERY log row whose
530
+ * type is outside that set the old `ignorable` envelope flag no longer
531
+ * exists, so stamping it (the ≤0.1.5 repair) cannot help. The only repair is
532
+ * removal.
533
+ *
534
+ * Rows cannot simply be deleted: the read path enforces contiguous `seq`
535
+ * numbers. This module therefore rewrites the log in place (after a `.bak`
536
+ * backup):
537
+ *
538
+ * - drops every `ya-subagent/started` row;
539
+ * - decrements the `seq` of every later ordinary event row (packed
540
+ * `text-chunks` / `reasoning-chunks` / `tool-call-chunks` storage rows
541
+ * shift their `seq0` instead);
542
+ * - shifts every `sourceEventSeqs` citation by the number of dropped rows
543
+ * ahead of it (dropped rows are never cited: only surface events carry
544
+ * provenance and they cite assistant chunks / surface nodes, which a
545
+ * plugin row never is).
535
546
  *
536
547
  * Two physical encodings (mirrors `session-persistence-jsonl`):
537
548
  * - `.jsonl` — plaintext, one JSON record per line.
@@ -539,17 +550,28 @@ function buildTool(profiles, ctx) {
539
550
  * frame holds the session header line, subsequent
540
551
  * frames each hold one append batch of event lines.
541
552
  * Each frame is independently decodable + checksummed.
542
- * Only frames whose decoded plaintext contains a target
543
- * row are recompressed; untouched frames are copied
544
- * verbatim so byte-identity is preserved where possible.
553
+ * The first frame containing a dropped row and every
554
+ * frame after it are recompressed (their rows renumber);
555
+ * untouched earlier frames are copied verbatim.
545
556
  *
546
- * Idempotent: rows already carrying `ignorable: true` are skipped; files with
547
- * no target rows are left untouched (no backup, no rewrite).
557
+ * Modified rows are re-encoded with `JSON.stringify`, which reproduces the
558
+ * write path's canonical single-line form and preserves the parsed key order;
559
+ * untouched lines stay byte-identical.
560
+ *
561
+ * Idempotent: a log with no target rows is left untouched (no backup, no
562
+ * rewrite). A corrupt (unparsable) line is left untouched — that is the
563
+ * harness's refusal job, not ours.
548
564
  *
549
565
  * @module @huanlin/dsh-plugin-yet-another-subagent/repair
550
566
  */
551
567
  /** The event type this module targets. */
552
568
  const TARGET_TYPE = "ya-subagent/started";
569
+ /** Packed chunk-run storage row tags (their span start rides `seq0`). */
570
+ const CHUNK_ROW_TYPES = /* @__PURE__ */ new Set([
571
+ "text-chunks",
572
+ "reasoning-chunks",
573
+ "tool-call-chunks"
574
+ ]);
553
575
  /** Zstandard magic number (little-endian 0xFD2FB528). */
554
576
  const ZSTD_MAGIC = 4247762216;
555
577
  /** Compression options matching the harness's `CHECKSUM_OPTIONS`. */
@@ -634,8 +656,9 @@ async function repairSessions(sessionsRoot) {
634
656
  * needed and no backup exists yet.
635
657
  */
636
658
  async function repairPlaintextFile(path) {
637
- const { lines, changed } = patchPlaintextLines(await readFile(path, "utf8"));
638
- if (!changed) return { kind: "clean" };
659
+ const raw = await readFile(path, "utf8");
660
+ const lines = stripLegacyRows(raw, []);
661
+ if (lines === raw) return { kind: "clean" };
639
662
  await ensureBackup(path);
640
663
  return {
641
664
  kind: "repaired",
@@ -644,21 +667,23 @@ async function repairPlaintextFile(path) {
644
667
  }
645
668
  /**
646
669
  * Repair one `.jsonl.zstd` concatenated-frame file. The header frame is
647
- * decoded to check for a target row (current harness writes the header as its
648
- * own frame, so a target there is theoretically possible but unlikely); event
649
- * frames are decoded and patched individually. Only frames with a patch are
650
- * recompressed; untouched frames are copied verbatim.
670
+ * decoded but never carries event rows; once a dropped row is found, that
671
+ * frame and every later frame are renumbered and recompressed (later rows'
672
+ * seqs shift even when their own text is otherwise unchanged). Frames before
673
+ * the first change are copied verbatim.
651
674
  */
652
675
  async function repairZstdFile(path) {
653
676
  const buffer = await readFile(path);
654
677
  const frames = scanZstdFrames(buffer);
655
678
  if (frames.length === 0) return { kind: "clean" };
656
679
  const rebuilt = [];
680
+ const droppedSeqs = [];
657
681
  let changed = false;
658
682
  for (const frame of frames) {
659
683
  const frameBytes = buffer.subarray(frame.start, frame.end);
660
- const { lines, changed: frameChanged } = patchPlaintextLines(zstdDecompressSync(frameBytes).toString("utf8"));
661
- if (frameChanged) {
684
+ const plaintext = zstdDecompressSync(frameBytes).toString("utf8");
685
+ const lines = stripLegacyRows(plaintext, droppedSeqs);
686
+ if (lines !== plaintext) {
662
687
  changed = true;
663
688
  rebuilt.push(zstdCompressSync(Buffer.from(lines, "utf8"), CHECKSUM_OPTIONS));
664
689
  } else rebuilt.push(Buffer.from(frameBytes));
@@ -671,38 +696,93 @@ async function repairZstdFile(path) {
671
696
  };
672
697
  }
673
698
  /**
674
- * Patch every `ya-subagent/started` line missing `ignorable` by inserting
675
- * `"ignorable":true` into the JSON object. Returns the new text and whether
676
- * any line changed. Lines that fail to parse as JSON are left untouched
677
- * (a corrupt line is the harness's refusal job, not ours).
699
+ * Strip every `ya-subagent/started` row from JSONL text and renumber the
700
+ * surviving rows so the read path's contiguity check still passes. The
701
+ * dropped-seq accumulator is shared across calls (zstd frames of one file are
702
+ * transformed sequentially) so later frames renumber against earlier drops.
703
+ * Returns the new text, or the input reference when nothing changed.
678
704
  */
679
- function patchPlaintextLines(text) {
680
- const lines = text.split("\n");
705
+ function stripLegacyRows(text, droppedSeqs) {
706
+ const source = text.split("\n");
707
+ const kept = [];
681
708
  let changed = false;
682
- for (let i = 0; i < lines.length; i++) {
683
- const line = lines[i];
684
- if (!line) continue;
685
- if (!line.includes(TARGET_TYPE)) continue;
686
- let parsed;
687
- try {
688
- parsed = JSON.parse(line);
689
- } catch {
709
+ for (const line of source) {
710
+ if (line === "") {
711
+ kept.push(line);
690
712
  continue;
691
713
  }
692
- if (typeof parsed !== "object" || parsed === null) continue;
693
- const record = parsed;
694
- if (record["type"] !== TARGET_TYPE) continue;
695
- if (record["ignorable"] === true) continue;
696
- const trimmed = line.trimEnd();
697
- if (trimmed.endsWith("}")) {
698
- lines[i] = trimmed.slice(0, -1) + ",\"ignorable\":true}";
714
+ const rewritten = rewriteLine(line, droppedSeqs);
715
+ if (rewritten === void 0) {
699
716
  changed = true;
717
+ continue;
700
718
  }
719
+ if (rewritten !== line) changed = true;
720
+ kept.push(rewritten);
701
721
  }
702
- return {
703
- lines: lines.join("\n"),
704
- changed
722
+ return changed ? kept.join("\n") : text;
723
+ }
724
+ /**
725
+ * Rewrite one JSONL line under the current dropped-seq prefix.
726
+ * @returns the (possibly original) line text, or `undefined` when the line is
727
+ * a dropped target row.
728
+ */
729
+ function rewriteLine(line, droppedSeqs) {
730
+ let parsed;
731
+ try {
732
+ parsed = JSON.parse(line);
733
+ } catch {
734
+ return line;
735
+ }
736
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return line;
737
+ const record = parsed;
738
+ if (record["type"] === TARGET_TYPE && typeof record["seq"] === "number") {
739
+ droppedSeqs.push(record["seq"]);
740
+ return;
741
+ }
742
+ const shiftOf = (seq) => {
743
+ let shift = 0;
744
+ for (const dropped of droppedSeqs) if (dropped < seq) shift += 1;
745
+ return shift;
705
746
  };
747
+ let modified = false;
748
+ if (CHUNK_ROW_TYPES.has(String(record["type"])) && typeof record["seq0"] === "number") {
749
+ const seq0 = record["seq0"];
750
+ const shifted = seq0 - shiftOf(seq0);
751
+ if (shifted !== seq0) {
752
+ record["seq0"] = shifted;
753
+ modified = true;
754
+ }
755
+ } else if (typeof record["seq"] === "number") {
756
+ const seq = record["seq"];
757
+ const shifted = seq - shiftOf(seq);
758
+ if (shifted !== seq) {
759
+ record["seq"] = shifted;
760
+ modified = true;
761
+ }
762
+ }
763
+ if (Array.isArray(record["sourceEventSeqs"])) {
764
+ const shiftedEntries = [];
765
+ let provenanceModified = false;
766
+ for (const entry of record["sourceEventSeqs"]) if (typeof entry === "number") {
767
+ const shifted = entry - shiftOf(entry);
768
+ if (shifted !== entry) provenanceModified = true;
769
+ shiftedEntries.push(shifted);
770
+ } else if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "number" && typeof entry[1] === "number") {
771
+ const start = entry[0] - shiftOf(entry[0]);
772
+ const end = entry[1] - shiftOf(entry[1]);
773
+ if (start !== entry[0] || end !== entry[1]) provenanceModified = true;
774
+ shiftedEntries.push([start, end]);
775
+ } else {
776
+ shiftedEntries.length = 0;
777
+ provenanceModified = false;
778
+ break;
779
+ }
780
+ if (provenanceModified) {
781
+ record["sourceEventSeqs"] = shiftedEntries;
782
+ modified = true;
783
+ }
784
+ }
785
+ return modified ? JSON.stringify(record) : line;
706
786
  }
707
787
  /**
708
788
  * Locate complete Zstandard frames in a concatenated stream. A structurally
@@ -809,7 +889,9 @@ function fail(message) {
809
889
  * Register the ya-subagent RPC channel on the host's connection service.
810
890
  * `connection` is in the plugin's inject list, so `ctx.connection` is
811
891
  * directly available; the channel route rolls back on fiber disposal
812
- * (the inner `owner.effect` owns cleanup).
892
+ * (the inner `owner.effect` owns cleanup). Trust and browser authentication
893
+ * moved to the physical `/api` carrier in v0.1.2-alpha.1, so channels no
894
+ * longer carry an `authority` option.
813
895
  * @param ctx - host context.
814
896
  * @param store - profile store.
815
897
  */
@@ -852,7 +934,7 @@ function registerRpc(ctx, store) {
852
934
  }
853
935
  default: return fail(`unknown endpoint: ${endpoint}`);
854
936
  }
855
- }, { authority: "trusted-host" });
937
+ });
856
938
  }
857
939
  //#endregion
858
940
  //#region node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
@@ -5031,6 +5113,67 @@ function refine(fn, _params = {}) {
5031
5113
  function superRefine(fn, params) {
5032
5114
  return /* @__PURE__ */ _superRefine(fn, params);
5033
5115
  }
5116
+ //#endregion
5117
+ //#region src/projection.ts
5118
+ /**
5119
+ * Two session projections (design doc §3.6):
5120
+ *
5121
+ * - `subagentProfile` (parent session): fold `tool/call` (name `subagent`,
5122
+ * profile in `arguments.profile`) + the matching `tool/result.subagentId`,
5123
+ * building a `childId → profileId` map. Used as a cross-check / fallback
5124
+ * for SubagentCard (which usually reads `profileLabel` straight from the
5125
+ * result content).
5126
+ *
5127
+ * - `yaSubagentProgress` (child session): toolcall count, token usage,
5128
+ * and lifecycle state. Pushed over the projection frame so the parent's
5129
+ * SubagentCard can subscribe even though client runtime drops non-current
5130
+ * `session/event` frames (single-stage model).
5131
+ *
5132
+ * Both units are pure synchronous folds; the framework drives them and the
5133
+ * host wire layer ships the validated views.
5134
+ *
5135
+ * Alpha.3 change-feed contract (`@deepseek-ai/dsh-session-projection`): the
5136
+ * drive publishes a client view only when its raw output changes by
5137
+ * `Object.is`, so an object-valued view MUST reuse its reference while the
5138
+ * wire content is unchanged — a fresh object per call republishes on every
5139
+ * internal-only state change (e.g. the excluded `streamingText`
5140
+ * accumulator). Both `view`s below go through {@link memoizeView} for that
5141
+ * reference-stability guarantee.
5142
+ *
5143
+ * @module @huanlin/dsh-plugin-yet-another-subagent/projection
5144
+ */
5145
+ /**
5146
+ * Wrap a pure view builder so equal content reuses the SAME object reference.
5147
+ *
5148
+ * The alpha.3 projection drive keeps the two latest raw `view` results per
5149
+ * session cell and publishes only when they differ by `Object.is`. A view
5150
+ * that allocates a fresh object per call defeats that gate; this wrapper
5151
+ * returns the previous object whenever the newly built one is content-equal
5152
+ * (`equal`) or the state reference is unchanged (repeated reads of the same
5153
+ * cell). The memo is shared across sessions folded by the unit — returning a
5154
+ * content-equal reference from another session's earlier state is still
5155
+ * correct, since each session's gate only compares its own consecutive
5156
+ * results and a stable reference satisfies it identically.
5157
+ */
5158
+ function memoizeView(build, equal) {
5159
+ let lastState;
5160
+ let lastView;
5161
+ return (state) => {
5162
+ if (lastView !== void 0 && Object.is(state, lastState)) return lastView;
5163
+ const candidate = build(state);
5164
+ if (lastView !== void 0 && equal(candidate, lastView)) return lastView;
5165
+ lastState = state;
5166
+ lastView = candidate;
5167
+ return candidate;
5168
+ };
5169
+ }
5170
+ /** Content equality for flat `Record<string, string>` views. */
5171
+ function recordEqual(a, b) {
5172
+ const keys = Object.keys(a);
5173
+ if (keys.length !== Object.keys(b).length) return false;
5174
+ for (const key of keys) if (a[key] !== b[key]) return false;
5175
+ return true;
5176
+ }
5034
5177
  /**
5035
5178
  * Fold the parent session's `tool/call` + `tool/result` for tool name
5036
5179
  * `subagent`. The profile id is carried in `tool/call.arguments.profile`
@@ -5040,53 +5183,52 @@ function superRefine(fn, params) {
5040
5183
  */
5041
5184
  const subagentProfileProjection = {
5042
5185
  key: "subagentProfile",
5043
- schema: object({
5044
- children: record(string(), string()),
5045
- calls: record(string(), string())
5186
+ stateSchema: object({
5187
+ pending: record(string(), string()),
5188
+ mapping: record(string(), string()),
5189
+ callToChild: record(string(), string())
5046
5190
  }).strict(),
5047
- stateVersion: 3,
5191
+ stateVersion: 4,
5048
5192
  init: () => ({
5049
- pending: /* @__PURE__ */ new Map(),
5193
+ pending: {},
5050
5194
  mapping: {},
5051
5195
  callToChild: {}
5052
5196
  }),
5053
5197
  apply: (state, event) => {
5054
5198
  if (event.type === "ya-subagent/started") {
5055
5199
  const { callId, childId, profileId } = event.data;
5056
- const nextMapping = {
5057
- ...state.mapping,
5058
- [childId]: profileId
5059
- };
5060
- const nextCallToChild = {
5061
- ...state.callToChild,
5062
- [callId]: childId
5063
- };
5064
- const nextPending = new Map(state.pending);
5065
- nextPending.delete(callId);
5200
+ const nextPending = { ...state.pending };
5201
+ delete nextPending[callId];
5066
5202
  return {
5067
5203
  pending: nextPending,
5068
- mapping: nextMapping,
5069
- callToChild: nextCallToChild
5204
+ mapping: {
5205
+ ...state.mapping,
5206
+ [childId]: profileId
5207
+ },
5208
+ callToChild: {
5209
+ ...state.callToChild,
5210
+ [callId]: childId
5211
+ }
5070
5212
  };
5071
5213
  }
5072
5214
  if (event.type === "tool/call" && event.data.name === "subagent") {
5073
5215
  const profileId = readProfileId(event.data.arguments);
5074
5216
  if (profileId === void 0) return state;
5075
- const nextPending = new Map(state.pending);
5076
- nextPending.set(event.data.callId, profileId);
5077
5217
  return {
5078
5218
  ...state,
5079
- pending: nextPending
5219
+ pending: {
5220
+ ...state.pending,
5221
+ [event.data.callId]: profileId
5222
+ }
5080
5223
  };
5081
5224
  }
5082
5225
  if (event.type === "tool/result") {
5083
- const callId = event.data.message.content[0]?.toolCallId;
5084
- if (callId === void 0) return state;
5085
- const profileId = state.pending.get(callId);
5226
+ const callId = event.data.message.source.callId;
5227
+ const profileId = state.pending[callId];
5086
5228
  if (profileId === void 0) return state;
5087
5229
  const childId = readChildId(event.data.message);
5088
- const nextPending = new Map(state.pending);
5089
- nextPending.delete(callId);
5230
+ const nextPending = { ...state.pending };
5231
+ delete nextPending[callId];
5090
5232
  if (childId === void 0) return {
5091
5233
  ...state,
5092
5234
  pending: nextPending
@@ -5105,10 +5247,16 @@ const subagentProfileProjection = {
5105
5247
  }
5106
5248
  return state;
5107
5249
  },
5108
- view: (state) => ({
5109
- children: state.mapping,
5110
- calls: state.callToChild
5111
- })
5250
+ wire: {
5251
+ viewSchema: object({
5252
+ children: record(string(), string()),
5253
+ calls: record(string(), string())
5254
+ }).strict(),
5255
+ view: memoizeView((state) => ({
5256
+ children: state.mapping,
5257
+ calls: state.callToChild
5258
+ }), (a, b) => recordEqual(a.children, b.children) && recordEqual(a.calls, b.calls))
5259
+ }
5112
5260
  };
5113
5261
  /** Parse the `profile` field from a `tool/call` arguments JSON string. */
5114
5262
  function readProfileId(argumentsRaw) {
@@ -5213,6 +5361,39 @@ const activitySchema = union([object({
5213
5361
  name: string(),
5214
5362
  args: string().optional()
5215
5363
  }).strict()]);
5364
+ const progressViewSchema = object({
5365
+ toolCallCount: number().int().nonnegative(),
5366
+ tokens: object({
5367
+ input: number().int().nonnegative(),
5368
+ output: number().int().nonnegative(),
5369
+ cacheRead: number().int().nonnegative(),
5370
+ cacheWrite: number().int().nonnegative(),
5371
+ reasoning: number().int().nonnegative()
5372
+ }).strict(),
5373
+ state: union([
5374
+ literal("running"),
5375
+ literal("idle"),
5376
+ literal("settled")
5377
+ ]),
5378
+ activity: activitySchema.optional()
5379
+ }).strict();
5380
+ /** The fold state is the wire view plus the in-flight streaming accumulator. */
5381
+ const progressStateSchema = progressViewSchema.extend({ streamingText: string() });
5382
+ /** Content equality for the progress view's token totals. */
5383
+ function tokensEqual(a, b) {
5384
+ return a.input === b.input && a.output === b.output && a.cacheRead === b.cacheRead && a.cacheWrite === b.cacheWrite && a.reasoning === b.reasoning;
5385
+ }
5386
+ /** Content equality for the progress view's activity union. */
5387
+ function activityEqual(a, b) {
5388
+ if (a === void 0 || b === void 0) return a === b;
5389
+ if (a.kind === "text" && b.kind === "text") return a.text === b.text;
5390
+ if (a.kind === "tool" && b.kind === "tool") return a.name === b.name && a.args === b.args;
5391
+ return false;
5392
+ }
5393
+ /** Content equality for the whole progress view (per-field, no identity). */
5394
+ function progressViewEqual(a, b) {
5395
+ return a.toolCallCount === b.toolCallCount && a.state === b.state && tokensEqual(a.tokens, b.tokens) && activityEqual(a.activity, b.activity);
5396
+ }
5216
5397
  /**
5217
5398
  * Fold the child session's own events into a compact progress view. Token
5218
5399
  * usage accumulates from `assistant/message.usage` (cache fields are
@@ -5220,23 +5401,8 @@ const activitySchema = union([object({
5220
5401
  */
5221
5402
  const yaSubagentProgressProjection = {
5222
5403
  key: "yaSubagentProgress",
5223
- schema: object({
5224
- toolCallCount: number().int().nonnegative(),
5225
- tokens: object({
5226
- input: number().int().nonnegative(),
5227
- output: number().int().nonnegative(),
5228
- cacheRead: number().int().nonnegative(),
5229
- cacheWrite: number().int().nonnegative(),
5230
- reasoning: number().int().nonnegative()
5231
- }).strict(),
5232
- state: union([
5233
- literal("running"),
5234
- literal("idle"),
5235
- literal("settled")
5236
- ]),
5237
- activity: activitySchema.optional()
5238
- }).strict(),
5239
- stateVersion: 2,
5404
+ stateSchema: progressStateSchema,
5405
+ stateVersion: 3,
5240
5406
  init: () => ({
5241
5407
  toolCallCount: 0,
5242
5408
  tokens: {
@@ -5319,9 +5485,12 @@ const yaSubagentProgressProjection = {
5319
5485
  }
5320
5486
  return state;
5321
5487
  },
5322
- view: (state) => {
5323
- const { streamingText: _, ...rest } = state;
5324
- return rest;
5488
+ wire: {
5489
+ viewSchema: progressViewSchema,
5490
+ view: memoizeView((state) => {
5491
+ const { streamingText: _, ...rest } = state;
5492
+ return rest;
5493
+ }, progressViewEqual)
5325
5494
  }
5326
5495
  };
5327
5496
  //#endregion
@@ -5335,7 +5504,7 @@ const inject = [
5335
5504
  "connection"
5336
5505
  ];
5337
5506
  /** Settings namespace under which profile state persists (`$DSH_HOME/settings.yaml`). */
5338
- const SETTINGS_NAMESPACE = settingsNamespace("ya-subagent");
5507
+ const SETTINGS_NAMESPACE = "ya-subagent";
5339
5508
  /** Schemastery schema for one profile (config layer). */
5340
5509
  const SubagentProfileSchema = z.object({
5341
5510
  id: z.string().required().description("Unique profile id (lowercase letters, digits, hyphens; 1-32 chars)."),
@@ -35,15 +35,33 @@ export interface YaSubagentSettingsInjected {
35
35
  readonly rpc: ClientConnectionRpc;
36
36
  /** Refetch the profile list from the host. */
37
37
  readonly fetchProfiles: () => Promise<readonly SubagentProfile[]>;
38
+ /** Refetch the routable model catalog (provider groups); undefined on failure. */
39
+ readonly fetchModelCatalog: () => Promise<ModelCatalogData | undefined>;
38
40
  /** Bound locale translator for the ya-subagent namespace. */
39
41
  readonly t: (key: string) => string;
40
42
  }
41
43
  /** Full props: settings.section runtime share + locale seat + inject. */
42
44
  type SettingsPageProps = PropsRuntime<'settings.section'> & PropsLocale<'ya-subagent'> & YaSubagentSettingsInjected;
45
+ /** One model inside a provider group (mirrors `ModelCatalogModel`). */
46
+ interface ModelEntry {
47
+ readonly id: string;
48
+ readonly name: string;
49
+ readonly description?: string;
50
+ }
51
+ /** One provider group (mirrors `ModelProviderGroup`). */
52
+ interface ModelGroup {
53
+ readonly id: string;
54
+ readonly name: string;
55
+ readonly models: readonly ModelEntry[];
56
+ }
57
+ /** Model catalog wire shape (mirror of the host `ModelCatalog` read face). */
58
+ interface ModelCatalogData {
59
+ readonly groups: readonly ModelGroup[];
60
+ }
43
61
  /**
44
62
  * Render the subagent profiles settings page.
45
63
  * @param props - settings.section runtime share + locale + inject.
46
64
  * @returns the page element.
47
65
  */
48
- export declare function SettingsPage({ rpc, fetchProfiles, t }: SettingsPageProps): import("react").JSX.Element;
66
+ export declare function SettingsPage({ rpc, fetchProfiles, fetchModelCatalog, t }: SettingsPageProps): import("react").JSX.Element;
49
67
  export {};
@@ -20,10 +20,11 @@ export interface SubagentCardSessions {
20
20
  binding(id: string): {
21
21
  session: {
22
22
  projections: {
23
+ /** Absence of a value is an `undefined` snapshot, never a missing face. */
23
24
  faceOf(key: string): {
24
25
  getSnapshot(): unknown;
25
26
  subscribe(fn: () => void): () => void;
26
- } | undefined;
27
+ };
27
28
  };
28
29
  };
29
30
  } | undefined;
@@ -26,10 +26,11 @@ interface TreeSessions {
26
26
  binding(id: string): {
27
27
  session: {
28
28
  projections: {
29
+ /** Absence of a value is an `undefined` snapshot, never a missing face. */
29
30
  faceOf(key: string): {
30
31
  getSnapshot(): unknown;
31
32
  subscribe(fn: () => void): () => void;
32
- } | undefined;
33
+ };
33
34
  };
34
35
  };
35
36
  } | undefined;
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * @module @huanlin/dsh-plugin-yet-another-subagent/client
14
14
  */
15
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
15
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
16
16
  import { type YaSubagentKey } from './locales.ts';
17
17
  declare module '@deepseek-ai/dsh-client-ui-slots' {
18
18
  interface LocaleNamespaceMap {
@@ -4,7 +4,7 @@
4
4
  * @module @huanlin/dsh-plugin-yet-another-subagent/client/locales
5
5
  */
6
6
  /** All copy keys for the ya-subagent namespace. */
7
- export type YaSubagentKey = 'nav' | 'page.title' | 'page.empty' | 'page.add' | 'page.add.placeholder.id' | 'page.add.placeholder.label' | 'page.add.submit' | 'page.add.error' | 'page.add.cancel' | 'row.label' | 'row.id' | 'row.model.kind.auto' | 'row.model.kind.manual' | 'row.model.provider' | 'row.model.model' | 'row.model.provider.placeholder' | 'row.model.model.placeholder' | 'row.model.noModels' | 'row.persona' | 'row.persona.kind.inherit' | 'row.persona.kind.custom' | 'row.persona.text' | 'row.toolFilter' | 'row.toolFilter.kind.none' | 'row.toolFilter.kind.allow' | 'row.toolFilter.kind.deny' | 'row.toolFilter.tools' | 'row.toolFilter.tools.search' | 'row.toolFilter.tools.empty' | 'row.toolFilter.tools.selected' | 'row.toolFilter.tools.selectAll' | 'row.toolFilter.tools.clear' | 'row.maxDepth' | 'row.delete' | 'row.delete.confirm' | 'row.save' | 'row.saved' | 'row.error' | 'row.expand' | 'row.collapse' | 'badge.builtin' | 'card.starting' | 'card.waiting' | 'card.idle' | 'card.running' | 'card.completed' | 'card.child-running' | 'card.child-idle' | 'card.toolcalls' | 'card.tokens' | 'card.calling' | 'card.open' | 'card.unavailable' | 'tree.tab' | 'tree.empty' | 'tree.rootHint' | 'tree.toolcalls' | 'tree.tokens' | 'tree.calling' | 'tree.state.running' | 'tree.state.idle' | 'tree.state.settled' | 'repair.button' | 'repair.confirm.title' | 'repair.confirm.body' | 'repair.confirm.warning' | 'repair.confirm.cancel' | 'repair.confirm.proceed' | 'repair.running' | 'repair.result.title' | 'repair.result.scanned' | 'repair.result.repaired' | 'repair.result.skipped' | 'repair.result.errors' | 'repair.result.errorEntry' | 'repair.result.close' | 'repair.error';
7
+ export type YaSubagentKey = 'common.close' | 'nav' | 'page.title' | 'page.empty' | 'page.add' | 'page.add.placeholder.id' | 'page.add.placeholder.label' | 'page.add.submit' | 'page.add.error' | 'page.add.cancel' | 'row.label' | 'row.id' | 'row.model.kind.auto' | 'row.model.kind.manual' | 'row.model.provider' | 'row.model.model' | 'row.model.provider.placeholder' | 'row.model.model.placeholder' | 'row.model.noModels' | 'row.persona' | 'row.persona.kind.inherit' | 'row.persona.kind.custom' | 'row.persona.text' | 'row.toolFilter' | 'row.toolFilter.kind.none' | 'row.toolFilter.kind.allow' | 'row.toolFilter.kind.deny' | 'row.toolFilter.tools' | 'row.toolFilter.tools.search' | 'row.toolFilter.tools.empty' | 'row.toolFilter.tools.selected' | 'row.toolFilter.tools.selectAll' | 'row.toolFilter.tools.clear' | 'row.maxDepth' | 'row.delete' | 'row.delete.confirm' | 'row.save' | 'row.saved' | 'row.error' | 'row.expand' | 'row.collapse' | 'badge.builtin' | 'card.starting' | 'card.waiting' | 'card.idle' | 'card.running' | 'card.completed' | 'card.child-running' | 'card.child-idle' | 'card.toolcalls' | 'card.tokens' | 'card.calling' | 'card.open' | 'card.unavailable' | 'tree.tab' | 'tree.empty' | 'tree.rootHint' | 'tree.toolcalls' | 'tree.tokens' | 'tree.calling' | 'tree.state.running' | 'tree.state.idle' | 'tree.state.settled' | 'repair.button' | 'repair.confirm.title' | 'repair.confirm.body' | 'repair.confirm.warning' | 'repair.confirm.cancel' | 'repair.confirm.proceed' | 'repair.running' | 'repair.result.title' | 'repair.result.scanned' | 'repair.result.repaired' | 'repair.result.skipped' | 'repair.result.errors' | 'repair.result.errorEntry' | 'repair.result.close' | 'repair.error';
8
8
  /** Locale namespace id. */
9
9
  export declare const NS = "ya-subagent";
10
10
  /** English dictionary. */
@@ -23,7 +23,7 @@ export declare const name = "yet-another-subagent";
23
23
  export declare const inject: string[];
24
24
  export type { SubagentProfile, YaSubagentConfig } from './types.ts';
25
25
  /** Settings namespace under which profile state persists (`$DSH_HOME/settings.yaml`). */
26
- export declare const SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
26
+ export declare const SETTINGS_NAMESPACE = "ya-subagent";
27
27
  export interface Config extends YaSubagentConfig {
28
28
  }
29
29
  export declare const Config: z<Config>;