@matamt-dbg/dsh-studio 0.4.0 → 0.5.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/client.js CHANGED
@@ -97,16 +97,16 @@ function parsePreviewRequest(value) {
97
97
  const row = value;
98
98
  const strings = (input) => Array.isArray(input) && input.every((item) => typeof item === "string");
99
99
  if (typeof row.preset !== "string" || !strings(row.providers) || !strings(row.tools) || typeof row.prompt !== "string" || typeof row.model !== "string") throw new Error("Invalid preview request");
100
- const record2 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
100
+ const record3 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
101
101
  if (row.persona !== void 0 && typeof row.persona !== "string") throw new Error("Invalid persona");
102
- if (row.sections !== void 0 && (!record2(row.sections) || Object.values(row.sections).some((v) => typeof v !== "string"))) throw new Error("Invalid section overrides");
103
- if (row.toolOverrides !== void 0 && (!record2(row.toolOverrides) || Object.values(row.toolOverrides).some((v) => !record2(v) || Object.keys(v).some((k) => !["description", "parameters"].includes(k)) || v.description !== void 0 && typeof v.description !== "string" || v.parameters !== void 0 && (!record2(v.parameters) || Object.values(v.parameters).some((p) => typeof p !== "string"))))) throw new Error("Invalid tool overrides");
104
- if (row.providerConfigs !== void 0 && (!record2(row.providerConfigs) || Object.values(row.providerConfigs).some((v) => !record2(v)))) throw new Error("Invalid provider configs");
102
+ if (row.sections !== void 0 && (!record3(row.sections) || Object.values(row.sections).some((v) => typeof v !== "string"))) throw new Error("Invalid section overrides");
103
+ if (row.toolOverrides !== void 0 && (!record3(row.toolOverrides) || Object.values(row.toolOverrides).some((v) => !record3(v) || Object.keys(v).some((k) => !["description", "parameters"].includes(k)) || v.description !== void 0 && typeof v.description !== "string" || v.parameters !== void 0 && (!record3(v.parameters) || Object.values(v.parameters).some((p) => typeof p !== "string"))))) throw new Error("Invalid tool overrides");
104
+ if (row.providerConfigs !== void 0 && (!record3(row.providerConfigs) || Object.values(row.providerConfigs).some((v) => !record3(v)))) throw new Error("Invalid provider configs");
105
105
  if (row.configOverrides !== void 0) validateConfigOverrides(row.configOverrides);
106
106
  if (row.execution !== void 0) validateExecutionDefaults(row.execution);
107
107
  if (row.configExclusions !== void 0 && (!strings(row.configExclusions) || row.configExclusions.some((id) => !id))) throw new Error("Invalid Config exclusions");
108
108
  const filter = row.subagentToolFilter;
109
- if (filter !== void 0 && (!record2(filter) || !strings(filter.deny) || filter.deny.some((name) => !name))) throw new Error("Invalid subagent tool filter");
109
+ if (filter !== void 0 && (!record3(filter) || !strings(filter.deny) || filter.deny.some((name) => !name))) throw new Error("Invalid subagent tool filter");
110
110
  return {
111
111
  ...row.configOverrides !== void 0 ? { configOverrides: row.configOverrides } : {},
112
112
  ...row.execution !== void 0 ? { execution: row.execution } : {},
@@ -1264,7 +1264,7 @@ var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
1264
1264
  var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
1265
1265
  var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
1266
1266
  var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
1267
- var STUDIO_VERSION = "0.4.0";
1267
+ var STUDIO_VERSION = "0.5.0";
1268
1268
 
1269
1269
  // src/delegation.ts
1270
1270
  var ROLE_TOOL = "subagent_role";
@@ -3437,7 +3437,7 @@ function GateResultDetails({ result, locale, durations = {}, skipped = [], compa
3437
3437
  ] }),
3438
3438
  result.records.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { className: "studio-gate-block", "aria-label": t("helperCalls"), children: [
3439
3439
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SectionHead, { title: t("helperCalls") }),
3440
- result.records.map((record2) => stage(record2.id, record2.result))
3440
+ result.records.map((record3) => stage(record3.id, record3.result))
3441
3441
  ] }),
3442
3442
  result.forwardError?.verdict === "error" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(Notice, { children: [
3443
3443
  t("error"),
@@ -3469,7 +3469,7 @@ function GateResultDetails({ result, locale, durations = {}, skipped = [], compa
3469
3469
  ] }),
3470
3470
  result.records.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { "aria-label": t("helperCalls"), children: [
3471
3471
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SectionHead, { title: t("helperCalls") }),
3472
- result.records.map((record2) => stage(record2.id, record2.result))
3472
+ result.records.map((record3) => stage(record3.id, record3.result))
3473
3473
  ] }),
3474
3474
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SectionHead, { title: t("forward"), actions: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Badge, { children: t(result.forward.kind) }) }),
3475
3475
  result.forward.kind !== "none" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "studio-pixie-answer", children: result.forward.text }),
@@ -3580,6 +3580,8 @@ var messages2 = {
3580
3580
  "var-data": ["\uC2A4\uD06C\uB9BD\uD2B8 \uCD9C\uB825 \uB370\uC774\uD130(JSON)", "script output data (JSON)", "\u811A\u672C\u8F93\u51FA\u6570\u636E\uFF08JSON\uFF09"],
3581
3581
  confirmReplace: ["\uBC14\uAFB8\uAE30 \uC804\uC5D0 \uD655\uC778", "Confirm before replacing", "\u66FF\u6362\u524D\u786E\u8BA4"],
3582
3582
  confirmReplaceHint: ["\uBC14\uAFC0 \uB0B4\uC6A9\uC744 \uBC14\uB85C \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 Pixie \uCE74\uB4DC\uC5D0\uC11C \uD655\uC778 \uD6C4 \uBCF4\uB0C5\uB2C8\uB2E4.", "Holds the replacement on a Pixie card until you confirm.", "\u66FF\u6362\u5185\u5BB9\u5148\u4FDD\u7559\u5728 Pixie \u5361\u7247\uFF0C\u786E\u8BA4\u540E\u518D\u53D1\u9001\u3002"],
3583
+ showInChat: ["\uACB0\uACFC\uB97C \uB300\uD654\uC5D0 \uD45C\uC2DC", "Show the result in the chat", "\u5728\u5BF9\u8BDD\u4E2D\u663E\u793A\u7ED3\u679C"],
3584
+ showInChatHint: ["\uAC8C\uC774\uD2B8\uAC00 \uB05D\uB098\uBA74 \uACB0\uACFC\uB97C \uAC80\uC0AC\uD55C \uB300\uC0C1(\uBA54\uC2DC\uC9C0\xB7\uB3C4\uAD6C \uD638\uCD9C\xB7\uB2F5\uBCC0) \uBC14\uB85C \uC544\uB798\uC5D0 \uBCF4\uC5EC \uC90D\uB2C8\uB2E4. \uBA54\uBAA8\uAC00 \uC788\uC73C\uBA74 \uBA54\uBAA8\uB97C, \uC5C6\uC73C\uBA74 \uD310\uC815\uACFC \uC774\uC720\uB97C \uBCF4\uC5EC \uC90D\uB2C8\uB2E4. \uC138\uC158 \uAE30\uB85D\uC5D0\uB294 \uB0A8\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "Shows the result right under what the gate checked (message, tool call, or answer) when it finishes: the note if there is one, otherwise the verdict and reason. Nothing is added to the session log.", "\u95E8\u63A7\u7ED3\u675F\u540E\u5728\u6240\u68C0\u67E5\u7684\u5BF9\u8C61\uFF08\u6D88\u606F\u3001\u5DE5\u5177\u8C03\u7528\u6216\u56DE\u7B54\uFF09\u4E0B\u65B9\u663E\u793A\u7ED3\u679C\uFF1A\u6709\u5907\u6CE8\u65F6\u663E\u793A\u5907\u6CE8\uFF0C\u5426\u5219\u663E\u793A\u5224\u5B9A\u548C\u539F\u56E0\u3002\u4E0D\u4F1A\u5199\u5165\u4F1A\u8BDD\u8BB0\u5F55\u3002"],
3583
3585
  onError: ["Pixie\uAC00 \uC2E4\uD328\uD558\uBA74", "If Pixie fails", "\u82E5 Pixie \u5931\u8D25"],
3584
3586
  errorPass: ["\uADF8\uB300\uB85C \uC9C4\uD589", "Continue as usual", "\u7167\u5E38\u7EE7\u7EED"],
3585
3587
  errorBlock: ["\uB9C9\uC74C", "Stop it", "\u62E6\u4E0B"],
@@ -3640,7 +3642,7 @@ var messages2 = {
3640
3642
  };
3641
3643
  function gateTaskText(locale, key, values = {}) {
3642
3644
  const text = messages2[key][locale.startsWith("ko") ? 0 : locale.startsWith("zh") ? 2 : 1];
3643
- return text.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
3645
+ return text.replace(/\{(\w+)\}/g, (match2, name) => name in values ? String(values[name]) : match2);
3644
3646
  }
3645
3647
 
3646
3648
  // src/pixie-gate-runs.tsx
@@ -3717,9 +3719,9 @@ function GateRunsPanel({ actions, locale }) {
3717
3719
  const request = ++generation;
3718
3720
  updates = /* @__PURE__ */ new Map();
3719
3721
  try {
3720
- const record2 = await actions.gateRuns();
3722
+ const record3 = await actions.gateRuns();
3721
3723
  if (!active || request !== generation) return;
3722
- const merged2 = new Map(record2.runs.map((run) => [run.id, run]));
3724
+ const merged2 = new Map(record3.runs.map((run) => [run.id, run]));
3723
3725
  for (const [id, run] of updates) merged2.set(id, run);
3724
3726
  setRuns([...merged2.values()]);
3725
3727
  setError("");
@@ -4232,11 +4234,11 @@ function pixieText(locale, key) {
4232
4234
  var PIXIE_ERROR_CODES = { "pixie/no-model": "errNoModel", "pixie/busy": "errBusy", "pixie/empty": "errEmpty" };
4233
4235
  var PIXIE_DETAILED_ERROR_CODES = { "pixie/credentials-refs": "errCredentialsRefs" };
4234
4236
  function pixieError(locale, message4) {
4235
- const match = /^\[(pixie\/[\w-]+)\]\s*/.exec(message4);
4236
- const key = match ? PIXIE_ERROR_CODES[match[1]] : void 0;
4237
+ const match2 = /^\[(pixie\/[\w-]+)\]\s*/.exec(message4);
4238
+ const key = match2 ? PIXIE_ERROR_CODES[match2[1]] : void 0;
4237
4239
  if (key) return pixieText(locale, key);
4238
- const detailed = match ? PIXIE_DETAILED_ERROR_CODES[match[1]] : void 0;
4239
- return detailed ? `${pixieText(locale, detailed)} (${message4.slice(match[0].length)})` : message4;
4240
+ const detailed = match2 ? PIXIE_DETAILED_ERROR_CODES[match2[1]] : void 0;
4241
+ return detailed ? `${pixieText(locale, detailed)} (${message4.slice(match2[0].length)})` : message4;
4240
4242
  }
4241
4243
 
4242
4244
  // src/pixie-tab.tsx
@@ -4343,9 +4345,9 @@ function PixieTab({ actions, locale }) {
4343
4345
  void refresh();
4344
4346
  }, children: t("retry") }), children: loadError }) });
4345
4347
  if (!view) return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StudioSurface, { className: "studio-pixie", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "studio-muted", children: t("loading") }) });
4346
- const record2 = view.record;
4347
- const opened = record2.threads.find((thread) => thread.id === openId);
4348
- const shown = opened ?? record2.threads.find((thread) => thread.id === record2.activeThreadId) ?? record2.threads.at(-1);
4348
+ const record3 = view.record;
4349
+ const opened = record3.threads.find((thread) => thread.id === openId);
4350
+ const shown = opened ?? record3.threads.find((thread) => thread.id === record3.activeThreadId) ?? record3.threads.at(-1);
4349
4351
  const running = live !== void 0 || view.running;
4350
4352
  const configured = Boolean(view.settings.model);
4351
4353
  const answering = (thread) => live?.threadId === thread.id || thread.turns.some((turn) => turn.status === "streaming");
@@ -4377,7 +4379,7 @@ function PixieTab({ actions, locale }) {
4377
4379
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "studio-pixie-list-head", children: [
4378
4380
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "studio-pixie-list-title", children: [
4379
4381
  t("allThreads"),
4380
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-count", children: record2.threads.length })
4382
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-count", children: record3.threads.length })
4381
4383
  ] }),
4382
4384
  configured && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IconButton, { label: t("newThread"), onClick: () => setCreating((open) => !open), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PlusIcon, {}) })
4383
4385
  ] }),
@@ -4399,14 +4401,14 @@ function PixieTab({ actions, locale }) {
4399
4401
  void createThread();
4400
4402
  }, children: t("create") })
4401
4403
  ] }),
4402
- record2.threads.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", className: "studio-muted", children: t("noThreads") }),
4403
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "studio-pixie-thread-list", "aria-label": t("threads"), children: [...record2.threads].reverse().map((thread) => {
4404
+ record3.threads.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", className: "studio-muted", children: t("noThreads") }),
4405
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "studio-pixie-thread-list", "aria-label": t("threads"), children: [...record3.threads].reverse().map((thread) => {
4404
4406
  const last = thread.id === live?.threadId && !thread.turns.some((turn) => turn.id === live.turnId) ? live.question : thread.turns.at(-1)?.question;
4405
4407
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", className: "studio-pixie-thread", "aria-current": thread.id === shown?.id ? "true" : void 0, onClick: () => setOpenId(thread.id), children: [
4406
4408
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "studio-card-head", children: [
4407
4409
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "studio-pixie-title", children: [
4408
4410
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: thread.title }),
4409
- thread.id === record2.activeThreadId && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-pixie-pin", role: "img", "aria-label": t("activeBadge"), title: t("activeBadge"), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PinIcon, { filled: true }) })
4411
+ thread.id === record3.activeThreadId && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-pixie-pin", role: "img", "aria-label": t("activeBadge"), title: t("activeBadge"), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PinIcon, { filled: true }) })
4410
4412
  ] }),
4411
4413
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ThreadBadges, { thread, answering: answering(thread), locale })
4412
4414
  ] }),
@@ -4421,15 +4423,15 @@ function PixieTab({ actions, locale }) {
4421
4423
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "studio-card-head", children: [
4422
4424
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "studio-pixie-title", children: [
4423
4425
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: shown.title }),
4424
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-pixie-pin-toggle", "data-target": shown.id === record2.activeThreadId, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4426
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "studio-pixie-pin-toggle", "data-target": shown.id === record3.activeThreadId, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4425
4427
  IconButton,
4426
4428
  {
4427
- label: shown.id === record2.activeThreadId ? t("activeBadge") : t("makeTarget"),
4428
- pressed: shown.id === record2.activeThreadId,
4429
+ label: shown.id === record3.activeThreadId ? t("activeBadge") : t("makeTarget"),
4430
+ pressed: shown.id === record3.activeThreadId,
4429
4431
  onClick: () => {
4430
- if (shown.id !== record2.activeThreadId) void target(shown);
4432
+ if (shown.id !== record3.activeThreadId) void target(shown);
4431
4433
  },
4432
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PinIcon, { filled: shown.id === record2.activeThreadId })
4434
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PinIcon, { filled: shown.id === record3.activeThreadId })
4433
4435
  }
4434
4436
  ) })
4435
4437
  ] }),
@@ -4547,72 +4549,206 @@ function PixieCommandRow({ question, outcome, locale, onOpen, actions }) {
4547
4549
 
4548
4550
  // src/pixie-gate-tail.tsx
4549
4551
  var import_react15 = __toESM(require("react"), 1);
4552
+
4553
+ // src/pixie-gate-chat-node.ts
4554
+ var GATE_RESULT_NODE = "studio-pixie-gate-result";
4555
+ var ANCHOR_OFFSET = 0.2;
4556
+ var SESSION_LOCATION = Object.freeze({ kind: "session" });
4557
+ var record2 = (value) => !!value && typeof value === "object";
4558
+ var appended = (event) => event.surfaceOp === void 0 || event.surfaceOp === "append";
4559
+ function eventAnchor(event) {
4560
+ if (!appended(event) || !record2(event.data)) return void 0;
4561
+ if (event.type === "tool/result") {
4562
+ const message4 = event.data.message;
4563
+ return record2(message4) && typeof message4.toolCallId === "string" ? { kind: "tool", callId: message4.toolCallId } : void 0;
4564
+ }
4565
+ const source = event.data.source;
4566
+ if (event.type === "user/message" && typeof event.data.id === "string" && record2(source) && (source.kind === "user" || source.kind === "agent-message")) return { kind: "message", messageId: event.data.id };
4567
+ return void 0;
4568
+ }
4569
+ var anchorId = (anchor) => anchor.kind === "tool" ? `tool:${anchor.callId}` : `message:${anchor.messageId}`;
4570
+ var match = (event) => {
4571
+ const anchor = eventAnchor(event);
4572
+ return anchor ? { id: anchorId(anchor), role: "start" } : null;
4573
+ };
4574
+ var gateResultDefinition = {
4575
+ kind: GATE_RESULT_NODE,
4576
+ target: "chat",
4577
+ // Function form: DSH 0.2.0-rc.2 does not accept the event-type table form.
4578
+ match,
4579
+ start: (_context, start) => ({ anchor: eventAnchor(start.event), seq: start.event.seq }),
4580
+ update: (context) => context.state,
4581
+ buildViewNode: (context) => {
4582
+ const state = context.state;
4583
+ if (!state) return null;
4584
+ const location = state.anchor.kind === "message" ? SESSION_LOCATION : context.start?.location ?? context.matches[0]?.location ?? { kind: "unresolved" };
4585
+ const anchorSeq = state.seq + ANCHOR_OFFSET;
4586
+ const current = context.current.get("chat");
4587
+ if (current && current.anchorSeq === anchorSeq && current.location === location) return current;
4588
+ return { key: context.key, kind: GATE_RESULT_NODE, id: context.id, target: "chat", anchorSeq, location, visibility: "visible", data: { anchor: state.anchor } };
4589
+ }
4590
+ };
4591
+ function applyGateFrame(record3, frame) {
4592
+ return frame.type === "session" ? { ...record3, disabledGateIds: frame.disabledGateIds } : { ...record3, runs: [...record3.runs.filter((run) => run.id !== frame.run.id), frame.run] };
4593
+ }
4594
+ function bufferGateFrame(frames, frame) {
4595
+ const same = frames.findIndex((old) => old.type === frame.type && (frame.type === "session" || old.type === "run" && old.run.id === frame.run.id));
4596
+ if (same >= 0) frames.splice(same, 1);
4597
+ frames.push(frame);
4598
+ }
4599
+ function gateRunHub(fetch, connect) {
4600
+ let state;
4601
+ let pending;
4602
+ let generation = 0;
4603
+ let stop;
4604
+ const listeners = /* @__PURE__ */ new Set();
4605
+ const load = () => {
4606
+ const id = ++generation;
4607
+ const frames = [];
4608
+ const promise = fetch().then((value) => {
4609
+ const next = frames.reduce(applyGateFrame, value);
4610
+ if (id === generation) {
4611
+ state = next;
4612
+ pending = void 0;
4613
+ }
4614
+ return next;
4615
+ }, (error) => {
4616
+ if (id === generation) pending = void 0;
4617
+ throw error;
4618
+ });
4619
+ promise.catch(() => {
4620
+ });
4621
+ pending = { generation: id, promise, frames };
4622
+ return promise;
4623
+ };
4624
+ const apply2 = (frame) => {
4625
+ if (frame.type === "sync") {
4626
+ state = void 0;
4627
+ void load();
4628
+ } else {
4629
+ if (state) state = applyGateFrame(state, frame);
4630
+ if (pending) bufferGateFrame(pending.frames, frame);
4631
+ }
4632
+ for (const listener of [...listeners]) listener(frame);
4633
+ };
4634
+ return {
4635
+ gateRuns: () => state ? Promise.resolve(state) : pending ? pending.promise : load(),
4636
+ followGates(listener) {
4637
+ listeners.add(listener);
4638
+ if (listeners.size === 1) stop = connect(apply2);
4639
+ return () => {
4640
+ if (!listeners.delete(listener) || listeners.size) return;
4641
+ stop?.();
4642
+ stop = void 0;
4643
+ state = void 0;
4644
+ pending = void 0;
4645
+ generation++;
4646
+ };
4647
+ }
4648
+ };
4649
+ }
4650
+
4651
+ // src/pixie-gate-tail.tsx
4550
4652
  var import_jsx_runtime17 = require("react/jsx-runtime");
4551
4653
  var UNVERIFIED = /미확인|not verified|unverified|未确认/i;
4552
- function tailRun(runs, turn) {
4553
- return runs.filter((run) => run.point === "turn-end" && run.turn === turn && run.status === "done" && run.result?.concludedBy === "script" && typeof run.output?.note === "string" && run.output.note.trim() !== "").sort((a, b) => a.startedAt - b.startedAt).at(-1);
4654
+ var atAnchor = (run, anchor) => anchor.kind === "turn" ? run.point === "turn-end" && run.turn === anchor.turn : anchor.kind === "tool" ? (run.point === "tool-before" || run.point === "tool-after") && run.tool?.callId === anchor.callId : run.point === "input" && run.messageId === anchor.messageId;
4655
+ function anchorRuns(runs, anchor) {
4656
+ const last = /* @__PURE__ */ new Map();
4657
+ for (const run of runs.filter((run2) => atAnchor(run2, anchor) && run2.showInChat !== false && (run2.status === "done" || run2.status === "error") && run2.result?.concludedBy !== "skipped").sort((a, b) => a.startedAt - b.startedAt)) last.set(run.point, run);
4658
+ return [...last.values()].sort((a, b) => a.startedAt - b.startedAt);
4659
+ }
4660
+ function tailContent(run) {
4661
+ const output = run.output;
4662
+ if (output?.note?.trim()) return { kind: "note", ...splitNote(output.note) };
4663
+ if (run.status === "error") return { kind: "error", reason: output?.reason ?? run.result?.forwardError?.reason ?? run.reason ?? "" };
4664
+ return {
4665
+ kind: "verdict",
4666
+ verdict: output?.verdict ?? "pass",
4667
+ ...output?.reason?.trim() ? { reason: output.reason } : {},
4668
+ ...output?.replace?.trim() ? { replace: output.replace } : {},
4669
+ ...output?.append?.trim() ? { append: output.append } : {}
4670
+ };
4554
4671
  }
4555
4672
  function splitNote(note) {
4556
- const match = /^\s*\[([^\]\n]{1,80})\]\s*\n?/.exec(note);
4557
- return match ? { label: match[1], body: note.slice(match[0].length) } : { body: note };
4673
+ const match2 = /^\s*\[([^\]\n]{1,80})\]\s*\n?/.exec(note);
4674
+ return match2 ? { label: match2[1], body: note.slice(match2[0].length) } : { body: note };
4558
4675
  }
4559
- function GateTail({ actions, turn, locale, Surface = import_react15.default.Fragment }) {
4560
- const t = (key) => gateText(locale, key);
4676
+ function GateResults({ actions, anchor, locale, Surface = import_react15.default.Fragment }) {
4561
4677
  const [runs, setRuns] = import_react15.default.useState([]);
4562
4678
  const [disabled, setDisabled] = import_react15.default.useState([]);
4563
- const [open, setOpen] = import_react15.default.useState(true);
4564
- const [busy, setBusy] = import_react15.default.useState(false);
4565
- const [error, setError] = import_react15.default.useState("");
4566
4679
  import_react15.default.useEffect(() => {
4567
4680
  let active = true;
4568
- const refresh = () => actions.gateRuns().then((record2) => {
4569
- if (active) {
4570
- setRuns(record2.runs);
4571
- setDisabled(record2.disabledGateIds ?? []);
4572
- }
4573
- }, () => {
4574
- });
4681
+ let refreshing;
4682
+ let latest = 0;
4683
+ const refresh = () => {
4684
+ const id = ++latest;
4685
+ const own2 = { id, frames: [] };
4686
+ refreshing = own2;
4687
+ actions.gateRuns().then((snapshot) => {
4688
+ if (!active || id !== latest) return;
4689
+ const record3 = own2.frames.reduce(applyGateFrame, snapshot);
4690
+ refreshing = void 0;
4691
+ setRuns(record3.runs);
4692
+ setDisabled(record3.disabledGateIds ?? []);
4693
+ }, () => {
4694
+ if (id === latest) refreshing = void 0;
4695
+ });
4696
+ };
4575
4697
  const stop = actions.followGates((frame) => {
4576
4698
  if (!active) return;
4577
4699
  if (frame.type === "sync") {
4578
- void refresh();
4700
+ refresh();
4579
4701
  return;
4580
4702
  }
4703
+ if (refreshing) bufferGateFrame(refreshing.frames, frame);
4581
4704
  if (frame.type === "session") {
4582
4705
  setDisabled(frame.disabledGateIds);
4583
4706
  return;
4584
4707
  }
4585
- setRuns((current) => [...current.filter((run2) => run2.id !== frame.run.id), frame.run]);
4708
+ setRuns((current) => [...current.filter((run) => run.id !== frame.run.id), frame.run]);
4586
4709
  });
4587
- void refresh();
4710
+ refresh();
4588
4711
  return () => {
4589
4712
  active = false;
4590
4713
  stop();
4591
4714
  };
4592
4715
  }, [actions]);
4593
- const run = tailRun(runs, turn);
4594
- if (!run) return null;
4595
- const { label, body } = splitNote(run.output.note);
4596
- const off = disabled.includes(run.gateId);
4716
+ const shown = anchorRuns(runs, anchor);
4717
+ if (!shown.length) return null;
4718
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Surface, { children: shown.map((run) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(GateResultBlock, { run, actions, locale, off: disabled.includes(run.gateId), onDisabled: setDisabled }, run.id)) });
4719
+ }
4720
+ function GateTail({ turn, ...props }) {
4721
+ const anchor = import_react15.default.useMemo(() => ({ kind: "turn", turn }), [turn]);
4722
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(GateResults, { ...props, anchor });
4723
+ }
4724
+ function GateResultBlock({ run, actions, locale, off, onDisabled }) {
4725
+ const t = (key) => gateText(locale, key);
4726
+ const [open, setOpen] = import_react15.default.useState(true);
4727
+ const [busy, setBusy] = import_react15.default.useState(false);
4728
+ const [error, setError] = import_react15.default.useState("");
4729
+ const content = tailContent(run);
4730
+ const label = content.kind === "note" ? content.label : void 0;
4597
4731
  const unverified = !!label && UNVERIFIED.test(label);
4598
4732
  const turnOff = async () => {
4599
4733
  setBusy(true);
4600
4734
  setError("");
4601
4735
  try {
4602
- setDisabled(await actions.setSessionGate(run.gateId, false));
4736
+ onDisabled(await actions.setSessionGate(run.gateId, false));
4603
4737
  } catch (cause) {
4604
4738
  setError(`${t("sessionGateFailed")} (${cause instanceof Error ? cause.message : String(cause)})`);
4605
4739
  } finally {
4606
4740
  setBusy(false);
4607
4741
  }
4608
4742
  };
4609
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Surface, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "studio-gate-tail-body", "aria-label": t("tailTitle").replace("{name}", run.gateName), "data-gate-tail": run.gateId, "data-unverified": unverified ? "true" : void 0, children: [
4743
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "studio-gate-tail-body", "aria-label": t("tailTitle").replace("{name}", run.gateName), "data-gate-tail": run.gateId, "data-gate-point": run.point, "data-tail-kind": content.kind, "data-unverified": unverified ? "true" : void 0, children: [
4610
4744
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "studio-gate-tail-head", children: [
4611
4745
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: label ?? run.gateName }),
4612
4746
  unverified && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Badge, { tone: "warning", children: t("tailUnverified") }),
4613
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "studio-card-meta", children: t("tailTitle").replace("{name}", run.gateName) }),
4747
+ content.kind === "verdict" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Badge, { tone: content.verdict === "stop" ? "warning" : "success", children: t(content.verdict) }),
4748
+ content.kind === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Badge, { tone: "warning", children: t("error") }),
4749
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "studio-card-meta", children: `${t("tailTitle").replace("{name}", run.gateName)} \xB7 ${gatePointText(locale, run.point)}` }),
4614
4750
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "studio-inline-actions", children: [
4615
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { small: true, onClick: () => setOpen((value) => !value), children: t(open ? "tailHide" : "tailShow") }),
4751
+ content.kind !== "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { small: true, onClick: () => setOpen((value) => !value), children: t(open ? "tailHide" : "tailShow") }),
4616
4752
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { small: true, onClick: actions.openTab, children: t("tailOpen") }),
4617
4753
  !off && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { small: true, disabled: busy, onClick: () => {
4618
4754
  void turnOff();
@@ -4621,8 +4757,14 @@ function GateTail({ actions, turn, locale, Surface = import_react15.default.Frag
4621
4757
  ] }),
4622
4758
  error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Notice, { children: error }),
4623
4759
  off && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "status", className: "studio-muted", children: t("tailOffDone") }),
4624
- open && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ModelText, { text: body, locale })
4625
- ] }) });
4760
+ content.kind === "error" && content.reason && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "studio-muted", children: content.reason }),
4761
+ open && content.kind === "note" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ModelText, { text: content.body, locale }),
4762
+ open && content.kind === "verdict" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
4763
+ content.reason && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ModelText, { text: content.reason, locale }),
4764
+ content.replace && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Disclosure, { title: t("replace"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ModelText, { text: content.replace, locale }) }),
4765
+ content.append && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Disclosure, { title: t("append"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ModelText, { text: content.append, locale }) })
4766
+ ] })
4767
+ ] });
4626
4768
  }
4627
4769
 
4628
4770
  // src/pixie-settings.tsx
@@ -4645,6 +4787,7 @@ var GATE_GUIDE_URL = "https://github.com/matamt-dbg/dsh-studio/blob/main/docs/ap
4645
4787
  var enabledFor = (task, preset) => task.enabled && task.presetIds.includes(preset) && !task.disabledPresetIds?.includes(preset);
4646
4788
  var isTool = (when) => when === "tool-before" || when === "tool-after";
4647
4789
  var isMessage = (when) => when === "user-message" || when === "agent-message";
4790
+ var showsInChat = (task) => task.advanced.showInChat !== false;
4648
4791
  function makeTask(name, when = "user-message", instruction = "") {
4649
4792
  return {
4650
4793
  id: globalThis.crypto.randomUUID(),
@@ -4652,7 +4795,7 @@ function makeTask(name, when = "user-message", instruction = "") {
4652
4795
  enabled: true,
4653
4796
  when,
4654
4797
  instruction,
4655
- advanced: { confirmReplace: false, onError: "pass", agents: when === "agent-message" ? "child" : "root", timeoutMs: 6e4, maxSteers: 1 },
4798
+ advanced: { confirmReplace: false, onError: "pass", agents: when === "agent-message" ? "child" : "root", timeoutMs: 6e4, maxSteers: 1, showInChat: true },
4656
4799
  presetIds: []
4657
4800
  };
4658
4801
  }
@@ -4888,6 +5031,7 @@ function GateTaskEditor({ task: initial2, open, locale, actions, onApply, onClos
4888
5031
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "studio-muted", children: t("scriptEnv") }),
4889
5032
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "studio-muted", children: withLink(t("scriptShell"), /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("a", { href: GATE_GUIDE_URL, target: "_blank", rel: "noreferrer", children: t("guideLink") })) })
4890
5033
  ] }),
5034
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SettingsRow, { title: t("showInChat"), description: t("showInChatHint"), control: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Toggle, { label: t("showInChat"), checked: showsInChat(task), onChange: (showInChat) => adv({ showInChat }) }) }),
4891
5035
  isMessage(task.when) && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SettingsRow, { title: t("confirmReplace"), description: t("confirmReplaceHint"), control: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Toggle, { label: t("confirmReplace"), checked: task.advanced.confirmReplace, onChange: (confirmReplace) => adv({ confirmReplace }) }) }),
4892
5036
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SettingsRow, { title: t("onError"), control: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Segmented, { label: t("onError"), value: task.advanced.onError, options: [{ value: "pass", label: t("errorPass") }, { value: "block", label: t("errorBlock") }], onChange: (onError) => adv({ onError }) }) }),
4893
5037
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SettingsRow, { title: t("agents"), control: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Segmented, { label: t("agents"), value: task.advanced.agents, options: ["root", "child", "any"].map((value) => ({ value, label: t(value) })), onChange: (agents) => adv({ agents }) }) }),
@@ -5236,7 +5380,7 @@ var messages3 = {
5236
5380
  };
5237
5381
  function gateComposeText(locale, key, values = {}) {
5238
5382
  const text = messages3[key][locale.startsWith("ko") ? 0 : locale.startsWith("zh") ? 2 : 1];
5239
- return text.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
5383
+ return text.replace(/\{(\w+)\}/g, (match2, name) => name in values ? String(values[name]) : match2);
5240
5384
  }
5241
5385
 
5242
5386
  // src/pixie-settings.tsx
@@ -5656,16 +5800,34 @@ function mountPixie(ctx, options = {}) {
5656
5800
  function GateTailSurface({ children }) {
5657
5801
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StudioSurface, { className: "studio-gate-tail", children });
5658
5802
  }
5803
+ const hubs = /* @__PURE__ */ new Map();
5804
+ const resultActions = (sessionId) => {
5805
+ let hub = hubs.get(sessionId);
5806
+ if (!hub) {
5807
+ const tab = tabActions(sessionId);
5808
+ hub = gateRunHub(tab.gateRuns, tab.followGates);
5809
+ hubs.set(sessionId, hub);
5810
+ }
5811
+ return { ...hub, setSessionGate: (gateId, enabled) => unwrap(remote().setSessionGate(sessionId, gateId, enabled)), openTab };
5812
+ };
5659
5813
  function TurnTail({ sessionId, turn }) {
5660
5814
  const active = useLocale();
5661
- const actions = import_react18.default.useMemo(() => {
5662
- const tab = tabActions(sessionId);
5663
- return { gateRuns: tab.gateRuns, followGates: tab.followGates, setSessionGate: (gateId, enabled) => unwrap(remote().setSessionGate(sessionId, gateId, enabled)), openTab };
5664
- }, [sessionId]);
5815
+ const actions = import_react18.default.useMemo(() => resultActions(sessionId), [sessionId]);
5665
5816
  if (turn.status !== "closed") return null;
5666
5817
  return withMarkdown(/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GateTail, { actions, turn: turn.turn, locale: active, Surface: GateTailSurface }));
5667
5818
  }
5668
5819
  scope.slots.inject("conversation.chat.turnTail", () => scope.slots.register({ name: "conversation.chat.turnTail", id: "studio-pixie-gate-tail" }, TurnTail));
5820
+ function GateResultRow({ sessionId, node }) {
5821
+ const active = useLocale();
5822
+ const actions = import_react18.default.useMemo(() => resultActions(sessionId), [sessionId]);
5823
+ const anchor = node.data.anchor;
5824
+ return withMarkdown(/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GateResults, { actions, anchor, locale: active, Surface: GateTailSurface }));
5825
+ }
5826
+ scope.inject(["uiConversation"], (inner) => {
5827
+ const conversation = inner.uiConversation;
5828
+ conversation.events.register(gateResultDefinition);
5829
+ inner.slots.inject("conversation.chat.node", () => inner.slots.register({ name: "conversation.chat.node", key: GATE_RESULT_NODE }, GateResultRow));
5830
+ });
5669
5831
  });
5670
5832
  }
5671
5833
 
package/index.js CHANGED
@@ -57,7 +57,7 @@ var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
57
57
  function currentPluginName(name) {
58
58
  return name === LEGACY_PACKAGE_NAME || name.startsWith(`${LEGACY_PACKAGE_NAME}/`) ? PACKAGE_NAME + name.slice(LEGACY_PACKAGE_NAME.length) : name;
59
59
  }
60
- var STUDIO_VERSION = "0.4.0";
60
+ var STUDIO_VERSION = "0.5.0";
61
61
 
62
62
  // src/delegation.ts
63
63
  var ROLE_TOOL = "subagent_role";
@@ -1624,7 +1624,7 @@ var text = (value) => typeof value === "string" && !!value.trim();
1624
1624
  function validateGateTask(value) {
1625
1625
  if (!object3(value) || !text(value.id) || !text(value.name) || typeof value.enabled !== "boolean" || !GATE_WHENS.includes(value.when) || typeof value.instruction !== "string") throw new Error("Invalid gate task");
1626
1626
  const advanced = value.advanced;
1627
- if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number") throw new Error("Invalid gate advanced options");
1627
+ if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number" || advanced.showInChat !== void 0 && typeof advanced.showInChat !== "boolean") throw new Error("Invalid gate advanced options");
1628
1628
  if (advanced.script !== void 0 && (typeof advanced.script !== "string" || (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(advanced.script) || advanced.script.includes("..")))) throw new Error("Invalid gate script filename");
1629
1629
  if (!Array.isArray(value.presetIds) || !value.presetIds.every(text)) throw new Error("Invalid gate presetIds");
1630
1630
  if (value.disabledPresetIds !== void 0 && (!Array.isArray(value.disabledPresetIds) || !value.disabledPresetIds.every(text))) throw new Error("Invalid gate disabledPresetIds");
@@ -1976,6 +1976,7 @@ var PixieGateStore = class {
1976
1976
  point: snapshot.point,
1977
1977
  ...snapshot.when ? { when: snapshot.when } : {},
1978
1978
  sessionId,
1979
+ ...snapshot.advanced ? { showInChat: snapshot.advanced.showInChat !== false } : {},
1979
1980
  startedAt: Date.now(),
1980
1981
  original,
1981
1982
  finalText: original,
@@ -2012,6 +2013,7 @@ var PixieGateStore = class {
2012
2013
  if (value.reason !== void 0) run.reason = value.reason;
2013
2014
  if (value.turn !== void 0) run.turn = value.turn;
2014
2015
  if (value.tool !== void 0) run.tool = value.tool;
2016
+ if (value.messageId !== void 0) run.messageId = value.messageId;
2015
2017
  if (value.steerCount !== void 0) run.steerCount = value.steerCount;
2016
2018
  if (value.result) {
2017
2019
  run.result = value.result;
@@ -2717,7 +2719,7 @@ function gateInput(deps) {
2717
2719
  deps.emit("pixie-gate/start", structuredClone(run));
2718
2720
  const execution = await executeTask(gate, original, payload.signal, deps.runner, () => deps.hookContext?.(payload.agent, payload.turn) ?? { sessionId: payload.agent.id, cwd: payload.agent.session.header.cwd, turn: payload.turn, agent: root ? "root" : "child" });
2719
2721
  const { result, plan } = execution;
2720
- const item = { run, execution, result, applied: execution.skipped ? "none" : "pass" };
2722
+ const item = { messageId: String(message.id), run, execution, result, applied: execution.skipped ? "none" : "pass" };
2721
2723
  pending.push(item);
2722
2724
  if (payload.signal.aborted || result.verdict === "cancelled") break;
2723
2725
  const suggestion = plan.suggestion;
@@ -2753,9 +2755,9 @@ function gateInput(deps) {
2753
2755
  addition: !cancelled && !rejected && item.execution.plan.addition !== void 0,
2754
2756
  withheld: !cancelled && rejected
2755
2757
  });
2756
- await deps.recorder.finish(payload.agent.id, item.run.id, { status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", result: item.result, output: item.execution.output, usage: item.execution.usage, applied, hooksInput, ...applied === "rejected" ? { rejection: item.rejection } : {} });
2758
+ await deps.recorder.finish(payload.agent.id, item.run.id, { status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", result: item.result, output: item.execution.output, usage: item.execution.usage, applied, hooksInput, messageId: item.messageId, ...applied === "rejected" ? { rejection: item.rejection } : {} });
2757
2759
  const usage = item.execution.usage;
2758
- deps.emit("pixie-gate/end", { ...item.run, result: item.result, output: item.execution.output, finalText: item.result.subject, applied, hooksInput, usage, endedAt: Date.now(), status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", ...applied === "rejected" && item.rejection ? { rejection: { ...item.rejection, original: item.run.original, status: "pending" } } : {} });
2760
+ deps.emit("pixie-gate/end", { ...item.run, messageId: item.messageId, result: item.result, output: item.execution.output, finalText: item.result.subject, applied, hooksInput, usage, endedAt: Date.now(), status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", ...applied === "rejected" && item.rejection ? { rejection: { ...item.rejection, original: item.run.original, status: "pending" } } : {} });
2759
2761
  }
2760
2762
  return committed;
2761
2763
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matamt-dbg/dsh-studio",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "DSH Studio: build, edit and apply DeepSeek Harness agent presets — tools, prompts, Configs and role-based subagents — from the web GUI.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -32,7 +32,7 @@ Help the user build a Pixie Gate: a task the Pixie model runs at one point of an
32
32
  - Write the whole judgment in `--question`: question IDs never reach the model. For several inputs (source and translation, answer and evidence), pipe a JSON object built with `jq` and name its fields in backticks in the question.
33
33
  - The script runs in the session working directory under the session sandbox. Do not write files unless the session can write; never try to escape the sandbox.
34
34
  - In the task, reference only `{{hooks.<name>.text}}` and `{{hooks.<name>.data}}` of the chosen script.
35
- - At `turn-end`, a note concluded with exit 11 shows under that turn's answer in the chat. Start it with a bracketed label line (e.g. `[한국어 번역]`) to title the block; a label containing `not verified`/`미확인` gets a warning badge.
35
+ - At every point, the gate's **Show the result in the chat** setting (`advanced.showInChat`, on by default) decides whether the result shows right under what it checked (the message, the tool call, or the turn's answer), whoever concluded it: the note if any, otherwise the verdict and reason. It is never written to the session log. For a note, start it with a bracketed label line (e.g. `[한국어 번역]`) to title the block; a label containing `not verified`/`미확인` gets a warning badge.
36
36
 
37
37
  Test a script by hand before relying on it: `printf 'sample' | PIXIE_POINT=tool-after PIXIE_TOOL_NAME=grep ./script.sh; echo $?`. The helper only works inside a real gate run.
38
38
 
@@ -42,7 +42,7 @@ Do not edit `definitions.json` unless the user asks you to; prefer guiding them
42
42
 
43
43
  ```json
44
44
  { "id": "<uuid>", "name": "…", "enabled": true, "when": "user-message", "instruction": "…",
45
- "advanced": { "confirmReplace": false, "onError": "pass", "agents": "root", "timeoutMs": 60000, "maxSteers": 1, "script": "optional.sh" },
45
+ "advanced": { "confirmReplace": false, "onError": "pass", "agents": "root", "timeoutMs": 60000, "maxSteers": 1, "showInChat": true, "script": "optional.sh" },
46
46
  "presetIds": ["<preset id>"], "disabledPresetIds": ["<attached preset where it is off>"] }
47
47
  ```
48
48
 
@@ -56,11 +56,12 @@ When the verdict is stop, the replacement and addition never reach the main mode
56
56
  | Option | Default | Description |
57
57
  | --- | --- | --- |
58
58
  | Pixie hook script | None | A script run before the task. See [below](#pixie-hook-scripts) |
59
+ | Show the result in the chat | On | Shows the result right under what the gate checked. See [Chat notes](#chat-notes-and-per-session-switches) |
59
60
  | Confirm before replacing | Off | Stops a real replacement and shows it as a suggestion (message points) |
60
- | On error | Pass | Pass or stop on model or script errors and timeouts |
61
- | Target agents | Root (child for agent messages) | Root, child, or any |
62
- | Time limit | 60 s | Script, helper, and model calls together |
63
- | Redo limit | 1 | Corrections per turn at the answer-end point |
61
+ | If Pixie fails | Continue as usual | Continue as usual or stop it on model or script errors and timeouts |
62
+ | Target agents | Main agent (Subagents for agent messages) | Main agent, Subagents, or All |
63
+ | Time limit (seconds) | 60 | Script, helper, and model calls together |
64
+ | Redo limit per turn | 1 | Corrections per turn at the answer-end point |
64
65
 
65
66
  ## Pixie hook scripts
66
67
 
@@ -236,12 +237,31 @@ On start, Studio installs a skill with this guide and the hook rules into `<DSH
236
237
 
237
238
  ## Chat notes and per-session switches
238
239
 
239
- **Chat notes:** when a hook script at the turn-end point concludes with a note (exit 11), for example a translation, the note appears as a block right under that turn's answer.
240
+ **Chat notes:** a gate with **Show the result in the chat** on (Advanced; on by default) shows its result as a block right under what it checked, whether the script (exit 11) or Pixie concluded it. The setting decides; the output does not. The block is drawn from Studio's gate records and is never written to the session log.
240
241
 
242
+ | Point | Where the block appears |
243
+ | --- | --- |
244
+ | You send an instruction / agents message each other | Right under that message, outside the turn's folded steps. A subagent's message shows in the subagent's chat |
245
+ | Before a tool runs / a tool result arrives | Right under that tool call, inside the turn's steps (expand the turn to see it). Before and after results of one call show together |
246
+ | The agent finishes its answer | Right under that turn's answer |
247
+
248
+ A rejected instruction has no message in the chat, so its result stays in the Pixie tab only.
249
+
250
+ ![Gate results under the instruction, the Read and Bash calls, and the answer](../images/en/pixie-gate-chat-results.png)
251
+
252
+ When the turn is folded, the instruction and answer results stay visible; tool results fold with the steps.
253
+
254
+ ![The same turn folded](../images/en/pixie-gate-chat-results-folded.png)
255
+
256
+ Turn the setting off in the gate editor under **Advanced → Show the result in the chat**. The gate still runs and its record stays in the Pixie tab.
257
+
258
+ ![Show the result in the chat](../images/en/pixie-gate-show-in-chat.png)
259
+
260
+ - The block shows the note when there is one, for example a translation. Without a note it shows the verdict with its reason, and any replacement or addition in a collapsed section. A failed run shows one error line.
241
261
  - If the note's first line starts with a bracketed label such as `[한국어 번역]`, the label becomes the block title. A label containing `not verified` (or `미확인`) gets a **not verified** badge.
242
262
  - The body renders as Markdown. The block offers **Hide**, **Open in Pixie tab**, and **Turn off in this session**.
243
- - It appears after the script finishes; there is no progress indicator. Skipped turns (exit 10), turns Pixie concluded, and turns without a note get no block. If the gate ran more than once in a turn, only the last note shows.
244
- - The original answer stays as it is; it cannot be collapsed or replaced.
263
+ - It appears after the gate finishes; there is no progress indicator. Skipped turns (exit 10), cancelled runs, and runs whose gate had the setting off get no block. Each run keeps the setting it started with. If a gate ran more than once at the same place, only the last result shows.
264
+ - The result block does not change the checked message, tool call, or answer. **Hide** collapses only the gate result.
245
265
 
246
266
  **Per-session switches:** at the top of **Pixie** tab → **Gates**, **Gates in this session** lists every gate that applies to this session's preset with a switch that turns it off for this session only. **Turn off in this session** on a chat block is the same switch.
247
267