@xingular/harness 1.3.1-unstable.2 → 1.3.1-unstable.3

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +1217 -701
  4. package/dist/sdk/computer/screenshot.d.ts +32 -0
  5. package/dist/sdk/computer/screenshot.js +35 -0
  6. package/dist/sdk/context/attribution.d.ts +18 -0
  7. package/dist/sdk/context/attribution.js +295 -0
  8. package/dist/sdk/context/types.d.ts +94 -0
  9. package/dist/sdk/context/types.js +10 -0
  10. package/dist/sdk/index.d.ts +2 -1
  11. package/dist/sdk/index.js +2 -1
  12. package/dist/sdk/plugins/builtin.js +1 -0
  13. package/dist/sdk/prompt/compile.d.ts +42 -2
  14. package/dist/sdk/prompt/compile.js +87 -15
  15. package/dist/sdk/server/methods.js +3 -0
  16. package/dist/sdk/sessions/contracts.d.ts +4 -0
  17. package/dist/sdk/sessions/contracts.js +2 -0
  18. package/dist/sdk/sessions/runner.d.ts +10 -0
  19. package/dist/sdk/sessions/runner.js +17 -0
  20. package/dist/sdk/sessions/subagentService.d.ts +11 -1
  21. package/dist/sdk/sessions/subagentService.js +120 -38
  22. package/dist/sdk/sessions/subagents.d.ts +31 -0
  23. package/dist/sdk/sessions/subagents.js +45 -0
  24. package/dist/sdk/terminal/client.d.ts +1 -1
  25. package/dist/sdk/terminal/client.js +13 -6
  26. package/dist/sdk/terminal/control.d.ts +1 -1
  27. package/dist/sdk/terminal/control.js +2 -2
  28. package/dist/sdk/terminal/controller.d.ts +9 -0
  29. package/dist/sdk/terminal/controller.js +48 -11
  30. package/dist/sdk/terminal/piExtension.js +35 -1
  31. package/dist/sdk/terminal/terminals.d.ts +2 -0
  32. package/dist/sdk/terminal/terminals.js +11 -0
  33. package/dist/sdk/terminal/tui/commands.d.ts +1 -2
  34. package/dist/sdk/terminal/tui/commands.js +1 -4
  35. package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
  36. package/dist/sdk/terminal/tui/dialogs.js +4 -3
  37. package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
  38. package/dist/sdk/terminal/tui/keybindings.js +1 -1
  39. package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
  40. package/dist/sdk/terminal/tui/modeChoices.js +4 -4
  41. package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
  42. package/dist/sdk/terminal/tui/mouseReport.js +80 -0
  43. package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
  44. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
  45. package/dist/sdk/terminal/tui/pages/contextPage.d.ts +54 -0
  46. package/dist/sdk/terminal/tui/pages/contextPage.js +393 -0
  47. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
  48. package/dist/sdk/terminal/tui/runtime.js +8 -2
  49. package/dist/sdk/terminal/tui/search.js +3 -2
  50. package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
  51. package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
  52. package/dist/sdk/tools/defaultSkills.d.ts +19 -0
  53. package/dist/sdk/tools/defaultSkills.js +27 -1
  54. package/dist/sdk/tools/dispatch.js +8 -9
  55. package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
  56. package/dist/sdk/tools/spawnSubagent.js +4 -2
  57. package/dist/terminal-opentui.js +570 -240
  58. package/dist/terminal-pi.js +142 -28
  59. package/docs/README.md +1 -0
  60. package/docs/architecture/02-sessions-and-runs.md +9 -0
  61. package/docs/architecture/04-prompt.md +13 -4
  62. package/docs/architecture/07-tools-and-gate.md +1 -1
  63. package/docs/architecture/11-terminal.md +12 -2
  64. package/docs/architecture/13-world-tools.md +17 -3
  65. package/docs/architecture/14-surfaces-and-parity.md +3 -3
  66. package/docs/architecture/README.md +1 -0
  67. package/docs/architecture/proposals/PROP-0049-context-attribution.md +382 -0
  68. package/docs/architecture/proposals/README.md +1 -0
  69. package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
  70. package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
  71. package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
  72. package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
  73. package/docs/parity.md +6 -6
  74. package/docs/releases/1.3.1-unstable.3.md +54 -0
  75. package/docs/terminal-tui.md +28 -15
  76. package/package.json +1 -1
@@ -70,18 +70,18 @@ function putIdle(path, stream) {
70
70
  entries.add(entry);
71
71
  idle.set(path, entries);
72
72
  }
73
- function request(connection, body, what, timeoutMs) {
73
+ function request(connection, body, what, timeoutMs, signal) {
74
74
  const reused = takeIdle(connection.socket);
75
- return attempt(connection, body, what, timeoutMs, reused).catch((error) => {
75
+ return attempt(connection, body, what, timeoutMs, reused, signal).catch((error) => {
76
76
  if (reused && error instanceof StaleConnection)
77
- return attempt(connection, body, what, timeoutMs, undefined);
77
+ return attempt(connection, body, what, timeoutMs, undefined, signal);
78
78
  throw error;
79
79
  });
80
80
  }
81
81
 
82
82
  class StaleConnection extends Error {
83
83
  }
84
- function attempt(connection, body, what, timeoutMs, reused) {
84
+ function attempt(connection, body, what, timeoutMs, reused, signal) {
85
85
  return new Promise((resolve, reject) => {
86
86
  const stream = reused ?? createConnection(connection.socket);
87
87
  let buffer = "";
@@ -115,6 +115,7 @@ function attempt(connection, body, what, timeoutMs, reused) {
115
115
  return;
116
116
  settled = true;
117
117
  clearTimeout(timer);
118
+ signal?.removeEventListener("abort", onAbort);
118
119
  stream.off("connect", send);
119
120
  stream.off("data", onData);
120
121
  stream.off("error", onError);
@@ -131,6 +132,15 @@ function attempt(connection, body, what, timeoutMs, reused) {
131
132
  stream.destroy();
132
133
  finish(() => reject(new SocketError("timeout", `The harness did not answer ${what}.`)));
133
134
  }, timeoutMs);
135
+ const onAbort = () => {
136
+ stream.destroy();
137
+ finish(() => reject(new SocketError("aborted", `${what} was cancelled.`)));
138
+ };
139
+ if (signal?.aborted) {
140
+ onAbort();
141
+ return;
142
+ }
143
+ signal?.addEventListener("abort", onAbort, { once: true });
134
144
  stream.on("data", onData);
135
145
  stream.on("error", onError);
136
146
  stream.on("close", onClose);
@@ -140,8 +150,8 @@ function attempt(connection, body, what, timeoutMs, reused) {
140
150
  stream.on("connect", send);
141
151
  });
142
152
  }
143
- function socketCall(connection, call, params = {}, timeoutMs = 120000, control) {
144
- return request(connection, { call, params, ...control ? { control } : {} }, call, timeoutMs);
153
+ function socketCall(connection, call, params = {}, timeoutMs = 120000, control, signal) {
154
+ return request(connection, { call, params, ...control ? { control } : {} }, call, timeoutMs, signal);
145
155
  }
146
156
  function terminalCall(event, data = {}, timeoutMs = 600000) {
147
157
  return request(connectionFromEnv(), { event, data }, event, timeoutMs);
@@ -165,10 +175,10 @@ function claimTerminalControl(connection = connectionFromEnv()) {
165
175
  }
166
176
  return claim;
167
177
  }
168
- async function controlCall(call, params = {}, timeoutMs = 120000) {
178
+ async function controlCall(call, params = {}, timeoutMs = 120000, signal) {
169
179
  const connection = connectionFromEnv();
170
180
  const control = await claimTerminalControl(connection);
171
- return socketCall(connection, call, params, timeoutMs, control);
181
+ return signal ? socketCall(connection, call, params, timeoutMs, control, signal) : socketCall(connection, call, params, timeoutMs, control);
172
182
  }
173
183
 
174
184
  // src/terminal/tui/chrome.ts
@@ -584,19 +594,10 @@ function lines(text) {
584
594
  }
585
595
  var compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
586
596
 
587
- // src/wiki/notes.ts
588
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
589
- import { dirname as dirname2, join as join3, relative, sep as sep2 } from "path";
590
-
591
- // src/core/changes.ts
592
- var listeners = new Set;
593
- function noteChange(change) {
594
- for (const listener of listeners) {
595
- try {
596
- listener(change);
597
- } catch {}
598
- }
599
- }
597
+ // src/tools/defaultSkills.ts
598
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
599
+ import { dirname as dirname2, join as join3 } from "path";
600
+ import { fileURLToPath } from "url";
600
601
 
601
602
  // src/core/frontmatter.ts
602
603
  import { parse as parseYaml, stringify } from "yaml";
@@ -625,6 +626,17 @@ function parse(text) {
625
626
  return null;
626
627
  }
627
628
  }
629
+ function bodyOf(text) {
630
+ const parts = split(text);
631
+ if (!parts)
632
+ return text;
633
+ try {
634
+ parseYaml(parts.yaml);
635
+ return parts.body;
636
+ } catch {
637
+ return text;
638
+ }
639
+ }
628
640
  function serialize(front, body) {
629
641
  let yaml;
630
642
  try {
@@ -639,6 +651,57 @@ ${body.trimEnd()}
639
651
  `;
640
652
  }
641
653
 
654
+ // src/tools/defaultSkills.ts
655
+ var SKILL_NAME = /^[a-z0-9][a-z0-9_-]*$/;
656
+ function defaultSkillPath() {
657
+ let dir = dirname2(fileURLToPath(import.meta.url));
658
+ for (;; ) {
659
+ const path = join3(dir, "builtin", "skills", "plugin-authoring", "SKILL.md");
660
+ if (existsSync2(path))
661
+ return path;
662
+ const parent = dirname2(dir);
663
+ if (parent === dir)
664
+ throw new Error("The bundled plugin-authoring skill is missing.");
665
+ dir = parent;
666
+ }
667
+ }
668
+ function defaultSkillPaths(variantDir) {
669
+ return variantDir && existsSync2(join3(variantDir, "skills", "plugin-authoring", "SKILL.md")) ? [] : [defaultSkillPath()];
670
+ }
671
+ function readSkillFile(variantDir, name) {
672
+ const wanted = name.trim().replace(/^skills\//, "").replace(/`/g, "");
673
+ if (!SKILL_NAME.test(wanted))
674
+ return null;
675
+ const variant = variantDir ? join3(variantDir, "skills", wanted, "SKILL.md") : null;
676
+ const builtin = wanted === "plugin-authoring" && defaultSkillPaths(variantDir).length ? defaultSkillPath() : null;
677
+ const path = variant && existsSync2(variant) && statSync2(variant).isFile() ? variant : builtin;
678
+ if (!path)
679
+ return null;
680
+ const baseDir = dirname2(path);
681
+ const scripts = join3(baseDir, "scripts");
682
+ return {
683
+ name: wanted,
684
+ path,
685
+ baseDir,
686
+ body: bodyOf(readFileSync2(path, "utf8")),
687
+ helpers: existsSync2(scripts) ? readdirSync(scripts) : []
688
+ };
689
+ }
690
+
691
+ // src/wiki/notes.ts
692
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3 } from "fs";
693
+ import { dirname as dirname3, join as join4, relative, sep as sep2 } from "path";
694
+
695
+ // src/core/changes.ts
696
+ var listeners = new Set;
697
+ function noteChange(change) {
698
+ for (const listener of listeners) {
699
+ try {
700
+ listener(change);
701
+ } catch {}
702
+ }
703
+ }
704
+
642
705
  // src/core/secrets.ts
643
706
  var SECRET_MARKERS = ["-----BEGIN", "sk-ant-", "sk-proj-", "ghp_", "github_pat_", "AKIA", "xoxb-"];
644
707
  function looksSecret(text) {
@@ -649,10 +712,10 @@ function looksSecret(text) {
649
712
  var LEGACY_FOLDERS = ["blocks", "facts", "raw", "revisions", "inbox", "operations", "trash"];
650
713
  var MAX_NOTE = 400000;
651
714
  function wikiDir(root) {
652
- return join3(root, "wiki");
715
+ return join4(root, "wiki");
653
716
  }
654
717
  function historyDir(root) {
655
- return join3(root, "wiki-history");
718
+ return join4(root, "wiki-history");
656
719
  }
657
720
  function cleanPath(path) {
658
721
  const clean = String(path ?? "").trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.md$/i, "");
@@ -670,7 +733,7 @@ function cleanPath(path) {
670
733
  return clean;
671
734
  }
672
735
  function fileOf(root, path) {
673
- return join3(wikiDir(root), `${cleanPath(path)}.md`);
736
+ return join4(wikiDir(root), `${cleanPath(path)}.md`);
674
737
  }
675
738
  function projectOfPath(path) {
676
739
  const parts = path.split("/");
@@ -695,17 +758,17 @@ function safeParse(text) {
695
758
  function walk(dir, into) {
696
759
  let names;
697
760
  try {
698
- names = readdirSync(dir);
761
+ names = readdirSync2(dir);
699
762
  } catch {
700
763
  return;
701
764
  }
702
765
  for (const name of names.sort()) {
703
766
  if (name.startsWith("."))
704
767
  continue;
705
- const path = join3(dir, name);
768
+ const path = join4(dir, name);
706
769
  let isDir = false;
707
770
  try {
708
- isDir = statSync2(path).isDirectory();
771
+ isDir = statSync3(path).isDirectory();
709
772
  } catch {
710
773
  continue;
711
774
  }
@@ -718,16 +781,16 @@ function walk(dir, into) {
718
781
  function paths(root) {
719
782
  const wiki = wikiDir(root);
720
783
  const files = [];
721
- if (existsSync2(join3(wiki, "home.md")))
722
- files.push(join3(wiki, "home.md"));
723
- walk(join3(wiki, "library"), files);
724
- walk(join3(wiki, "projects"), files);
784
+ if (existsSync3(join4(wiki, "home.md")))
785
+ files.push(join4(wiki, "home.md"));
786
+ walk(join4(wiki, "library"), files);
787
+ walk(join4(wiki, "projects"), files);
725
788
  return files.map((file) => relative(wiki, file).split(sep2).join("/").replace(/\.md$/, ""));
726
789
  }
727
790
  function summary(root, path) {
728
791
  const file = fileOf(root, path);
729
- const text = readFileSync2(file, "utf8");
730
- return { path, title: titleOf(path, text), project: projectOfPath(path), updatedAt: statSync2(file).mtime.toISOString(), size: text.length };
792
+ const text = readFileSync3(file, "utf8");
793
+ return { path, title: titleOf(path, text), project: projectOfPath(path), updatedAt: statSync3(file).mtime.toISOString(), size: text.length };
731
794
  }
732
795
  function list(root, options = {}) {
733
796
  return paths(root).filter((path) => options.project === undefined || projectOfPath(path) === options.project).map((path) => summary(root, path));
@@ -779,7 +842,7 @@ function graph(root) {
779
842
  const titles = new Map(nodes.map((node) => [node.title.toLowerCase(), node.path]));
780
843
  const edges = [];
781
844
  for (const node of nodes) {
782
- const body = readFileSync2(fileOf(root, node.path), "utf8");
845
+ const body = readFileSync3(fileOf(root, node.path), "utf8");
783
846
  for (const to of linksIn(body, node.path, known, titles))
784
847
  edges.push({ from: node.path, to });
785
848
  }
@@ -788,9 +851,9 @@ function graph(root) {
788
851
  function read(root, path) {
789
852
  const clean = cleanPath(path);
790
853
  const file = fileOf(root, clean);
791
- if (!existsSync2(file))
854
+ if (!existsSync3(file))
792
855
  throw notFound(`The note ${clean}`);
793
- const text = readFileSync2(file, "utf8");
856
+ const text = readFileSync3(file, "utf8");
794
857
  const { nodes, edges } = graph(root);
795
858
  const known = new Set(nodes.map((node) => node.path));
796
859
  const titles = new Map(nodes.map((node) => [node.title.toLowerCase(), node.path]));
@@ -813,16 +876,16 @@ function write(root, path, body, options = {}) {
813
876
  if (project && !projectsOrNone(root).some((known) => known.slug === project))
814
877
  throw invalid(`There is no project with the slug ${project}.`);
815
878
  const file = fileOf(root, clean);
816
- if (existsSync2(file)) {
817
- const current = statSync2(file).mtime.toISOString();
879
+ if (existsSync3(file)) {
880
+ const current = statSync3(file).mtime.toISOString();
818
881
  if (options.expectedUpdatedAt && options.expectedUpdatedAt !== current)
819
882
  throw new HarnessError("conflict", "The note changed since it was opened. Reload it and try again.");
820
- const previous = readFileSync2(file, "utf8");
883
+ const previous = readFileSync3(file, "utf8");
821
884
  if (previous === body)
822
885
  return read(root, clean);
823
- atomicWrite(join3(historyDir(root), clean, `${now().replace(/[:.]/g, "-")}.md`), previous);
886
+ atomicWrite(join4(historyDir(root), clean, `${now().replace(/[:.]/g, "-")}.md`), previous);
824
887
  }
825
- mkdirSync2(dirname2(file), { recursive: true });
888
+ mkdirSync2(dirname3(file), { recursive: true });
826
889
  atomicWrite(file, body);
827
890
  noteChange({ wiki: wikiDir(root), id: clean, path: file, text: body, actor: options.actor ?? "user", reason: options.reason ?? "Saved" });
828
891
  return read(root, clean);
@@ -830,15 +893,15 @@ function write(root, path, body, options = {}) {
830
893
  function remove(root, path) {
831
894
  const clean = cleanPath(path);
832
895
  const file = fileOf(root, clean);
833
- if (!existsSync2(file))
896
+ if (!existsSync3(file))
834
897
  throw notFound(`The note ${clean}`);
835
- atomicWrite(join3(historyDir(root), clean, `${now().replace(/[:.]/g, "-")}.md`), readFileSync2(file, "utf8"));
898
+ atomicWrite(join4(historyDir(root), clean, `${now().replace(/[:.]/g, "-")}.md`), readFileSync3(file, "utf8"));
836
899
  rmSync2(file);
837
900
  }
838
901
  function versions(root, path) {
839
- const dir = join3(historyDir(root), cleanPath(path));
902
+ const dir = join4(historyDir(root), cleanPath(path));
840
903
  try {
841
- return readdirSync(dir).filter((name) => name.endsWith(".md")).sort().reverse().map((name) => ({ id: name.replace(/\.md$/, ""), savedAt: statSync2(join3(dir, name)).mtime.toISOString() }));
904
+ return readdirSync2(dir).filter((name) => name.endsWith(".md")).sort().reverse().map((name) => ({ id: name.replace(/\.md$/, ""), savedAt: statSync3(join4(dir, name)).mtime.toISOString() }));
842
905
  } catch {
843
906
  return [];
844
907
  }
@@ -846,10 +909,10 @@ function versions(root, path) {
846
909
  function version(root, path, id) {
847
910
  if (!/^[0-9A-Za-z-]+$/.test(id))
848
911
  throw invalid("Invalid version.");
849
- const file = join3(historyDir(root), cleanPath(path), `${id}.md`);
850
- if (!existsSync2(file))
912
+ const file = join4(historyDir(root), cleanPath(path), `${id}.md`);
913
+ if (!existsSync3(file))
851
914
  throw notFound("That version");
852
- return readFileSync2(file, "utf8");
915
+ return readFileSync3(file, "utf8");
853
916
  }
854
917
  function search(root, query, options = {}) {
855
918
  const words = [...new Set(String(query ?? "").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 1))].slice(0, 16);
@@ -857,7 +920,7 @@ function search(root, query, options = {}) {
857
920
  return [];
858
921
  const hits = [];
859
922
  for (const path of paths(root)) {
860
- const text = readFileSync2(fileOf(root, path), "utf8");
923
+ const text = readFileSync3(fileOf(root, path), "utf8");
861
924
  const lower = text.toLowerCase();
862
925
  const title = titleOf(path, text);
863
926
  let score = 0;
@@ -887,15 +950,15 @@ function search(root, query, options = {}) {
887
950
  }
888
951
  function ensureLayout(root, productName) {
889
952
  const wiki = wikiDir(root);
890
- mkdirSync2(join3(wiki, "library"), { recursive: true });
891
- mkdirSync2(join3(wiki, "projects"), { recursive: true });
892
- if (!existsSync2(join3(wiki, "home.md")))
893
- atomicWrite(join3(wiki, "home.md"), `# Home
953
+ mkdirSync2(join4(wiki, "library"), { recursive: true });
954
+ mkdirSync2(join4(wiki, "projects"), { recursive: true });
955
+ if (!existsSync3(join4(wiki, "home.md")))
956
+ atomicWrite(join4(wiki, "home.md"), `# Home
894
957
 
895
958
  Notes about your projects for ${productName}. Link notes with [[library/name]] or [[projects/<slug>/name]].
896
959
  `);
897
960
  for (const project of projectsOrNone(root))
898
- mkdirSync2(join3(wiki, "projects", project.slug), { recursive: true });
961
+ mkdirSync2(join4(wiki, "projects", project.slug), { recursive: true });
899
962
  }
900
963
 
901
964
  // src/world/untrusted.ts
@@ -953,6 +1016,7 @@ var contextSnapshotPayload = z.object({
953
1016
  systemPrompt: z.string().max(1e5),
954
1017
  contextUpdate: z.string().max(1e5).optional(),
955
1018
  tools: z.array(z.object({ name: z.string(), description: z.string().max(2000), parameters: z.unknown() })).max(120),
1019
+ compaction: z.object({ enabled: z.boolean(), reserveTokens: z.number().nonnegative() }).optional(),
956
1020
  systemPromptTruncated: z.boolean().optional(),
957
1021
  contextUpdateTruncated: z.boolean().optional(),
958
1022
  toolsTruncated: z.boolean().optional()
@@ -1491,9 +1555,28 @@ function sessionNameTool(current) {
1491
1555
 
1492
1556
  // src/prompt/compile.ts
1493
1557
  var SESSION_STATE_BUDGET = 1000;
1494
- var SECTION_ORDER = { harnessIdentity: 5, system: 25, sessionState: 30, guardrails: 40, memory: 45, references: 50, subagents: 64, scripts: 66, wiki: 80, tools: 90, reporting: 95 };
1558
+ var REQUIRED_SKILLS_BUDGET = 12000;
1559
+ var REQUIRED_SKILLS_POINTER = "Some required skill text was omitted to fit. Load the rest with skill_load.";
1560
+ var SECTION_ORDER = { harnessIdentity: 5, system: 25, sessionState: 30, guardrails: 40, memory: 45, references: 50, requiredSkills: 52, subagents: 64, scripts: 66, wiki: 80, tools: 90, reporting: 95 };
1495
1561
  var NATIVE_GROUP = ["read", "grep", "find", "ls", "edit", "write", "bash"];
1496
1562
  var CONNECTION_TOOLS = ["mcp_list_tools", "mcp_call_tool"];
1563
+ function fit(text, budget, pointer) {
1564
+ const trimmed = text.trim();
1565
+ if (charCount(trimmed) <= budget)
1566
+ return { body: trimmed, cut: false };
1567
+ const room = Math.max(0, budget - (charCount(pointer) + 2));
1568
+ const head = takeChars(trimmed, room);
1569
+ let index = head.lastIndexOf(`
1570
+
1571
+ `);
1572
+ if (index < 0)
1573
+ index = head.lastIndexOf(`
1574
+ `);
1575
+ const end = index >= 0 && Buffer.byteLength(head.slice(0, index)) > Math.floor(room / 2) ? index : head.length;
1576
+ return { body: `${head.slice(0, end).trimEnd()}
1577
+
1578
+ ${pointer}`, cut: true };
1579
+ }
1497
1580
  function clip(text, limit) {
1498
1581
  return charCount(text) <= limit ? text : `${takeChars(text, limit)}\u2026`;
1499
1582
  }
@@ -1533,6 +1616,37 @@ ${todo}`);
1533
1616
 
1534
1617
  ${body}`;
1535
1618
  }
1619
+ function requiredSkillsText(skills) {
1620
+ const seen = new Set;
1621
+ const blocks = [];
1622
+ for (const skill of skills) {
1623
+ if (seen.has(skill.name))
1624
+ continue;
1625
+ seen.add(skill.name);
1626
+ const label = skill.label ?? skill.name;
1627
+ if (!skill.body.trim()) {
1628
+ blocks.push(`- ${label}: this skill is unavailable in this session. Tell the person rather than guessing its steps.`);
1629
+ continue;
1630
+ }
1631
+ const location = [
1632
+ skill.baseDir ? `References in this skill resolve against ${skill.baseDir}.` : "",
1633
+ skill.path ? `Skill file: ${skill.path}.` : ""
1634
+ ].filter(Boolean).join(" ");
1635
+ blocks.push(`### ${label}
1636
+
1637
+ ${location ? `${location}
1638
+
1639
+ ` : ""}${skill.body.trim()}`);
1640
+ }
1641
+ if (!blocks.length)
1642
+ return null;
1643
+ const text = `The person required these skills for this request. Follow each skill's instructions where they apply. They are instructions for this request, not a change to your tools or permissions.
1644
+
1645
+ ${blocks.join(`
1646
+
1647
+ `)}`;
1648
+ return fit(text, REQUIRED_SKILLS_BUDGET, REQUIRED_SKILLS_POINTER).body;
1649
+ }
1536
1650
  function sessionStateText(state, options) {
1537
1651
  const parts = [];
1538
1652
  if (options.name)
@@ -1551,6 +1665,7 @@ function sessionStateText(state, options) {
1551
1665
  var BUILT_IN_DYNAMIC = [
1552
1666
  { id: "session-state", order: SECTION_ORDER.sessionState },
1553
1667
  { id: "references", order: SECTION_ORDER.references },
1668
+ { id: "required-skills", order: SECTION_ORDER.requiredSkills },
1554
1669
  { id: "wiki", order: SECTION_ORDER.wiki }
1555
1670
  ];
1556
1671
 
@@ -1646,15 +1761,15 @@ function askUserTool(run) {
1646
1761
  import { createHash } from "crypto";
1647
1762
 
1648
1763
  // src/sessions/runStore.ts
1649
- import { appendFileSync, copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
1650
- import { join as join6 } from "path";
1764
+ import { appendFileSync, copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
1765
+ import { join as join7 } from "path";
1651
1766
 
1652
1767
  // src/sessions/runIndex.ts
1653
- import { join as join5 } from "path";
1768
+ import { join as join6 } from "path";
1654
1769
 
1655
1770
  // src/core/index.ts
1656
- import { copyFileSync, existsSync as existsSync3, readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
1657
- import { join as join4 } from "path";
1771
+ import { copyFileSync, existsSync as existsSync4, readdirSync as readdirSync3, rmSync as rmSync3 } from "fs";
1772
+ import { join as join5 } from "path";
1658
1773
  import { DatabaseSync } from "node:sqlite";
1659
1774
  var RECALL_SCHEMA = `CREATE TABLE recall_runs (run_id TEXT PRIMARY KEY, passages INTEGER NOT NULL, status TEXT NOT NULL);
1660
1775
  CREATE VIRTUAL TABLE recall_fts USING fts5(run_id UNINDEXED, seq UNINDEXED, role UNINDEXED, at UNINDEXED,
@@ -1714,7 +1829,7 @@ var MIGRATIONS = [
1714
1829
  ];
1715
1830
  var connections = new Map;
1716
1831
  function dbPath(root) {
1717
- return join4(root, "index.sqlite");
1832
+ return join5(root, "index.sqlite");
1718
1833
  }
1719
1834
  function sqlError(error) {
1720
1835
  return new HarnessError("index_error", error instanceof Error ? error.message : String(error), "Settings \u2192 Projects and Storage \u2192 Repair index.");
@@ -1750,7 +1865,7 @@ function migrate(path, migrations) {
1750
1865
  if (current >= migrations.length)
1751
1866
  return database;
1752
1867
  const backup = `${path.replace(/\.sqlite$/, "")}.sqlite.bak-v${current}`;
1753
- const hadFile = existsSync3(path) && current > 0;
1868
+ const hadFile = existsSync4(path) && current > 0;
1754
1869
  if (hadFile) {
1755
1870
  try {
1756
1871
  database.exec("PRAGMA wal_checkpoint(TRUNCATE);");
@@ -1798,7 +1913,7 @@ function all(database, sql, ...params) {
1798
1913
  }
1799
1914
  function runFolders(root) {
1800
1915
  try {
1801
- return readdirSync2(join4(root, "runs"));
1916
+ return readdirSync3(join5(root, "runs"));
1802
1917
  } catch {
1803
1918
  return [];
1804
1919
  }
@@ -1809,7 +1924,7 @@ function readAllRuns(root) {
1809
1924
  const records = [];
1810
1925
  for (const name of runFolders(root)) {
1811
1926
  try {
1812
- const parsed = runRecordSchema.safeParse(upgradeRunRecord(readJson(join5(root, "runs", name, "run.json"))));
1927
+ const parsed = runRecordSchema.safeParse(upgradeRunRecord(readJson(join6(root, "runs", name, "run.json"))));
1813
1928
  if (parsed.success)
1814
1929
  records.push(parsed.data);
1815
1930
  } catch {}
@@ -1819,9 +1934,9 @@ function readAllRuns(root) {
1819
1934
 
1820
1935
  // src/sessions/runStore.ts
1821
1936
  function runDir(root, runId) {
1822
- return join6(root, "runs", safeId(runId));
1937
+ return join7(root, "runs", safeId(runId));
1823
1938
  }
1824
- var recordPath = (root, runId) => join6(runDir(root, runId), "run.json");
1939
+ var recordPath = (root, runId) => join7(runDir(root, runId), "run.json");
1825
1940
  function readRecord(path) {
1826
1941
  const parsed = runRecordSchema.safeParse(upgradeRunRecord(readJson(path)));
1827
1942
  return parsed.success ? parsed.data : null;
@@ -1861,7 +1976,7 @@ function storedEvent(line) {
1861
1976
  function readEvents(root, runId) {
1862
1977
  let text;
1863
1978
  try {
1864
- text = readFileSync3(join6(runDir(root, runId), "events.jsonl"), "utf8");
1979
+ text = readFileSync4(join7(runDir(root, runId), "events.jsonl"), "utf8");
1865
1980
  } catch (error) {
1866
1981
  if (error.code === "ENOENT")
1867
1982
  return [];
@@ -2197,6 +2312,8 @@ function reported(parentRunId, results, spawned) {
2197
2312
  return { text: JSON.stringify({ results: included, ...results.length > included.length ? { remaining: results.slice(included.length).map((result) => result.runId) } : {} }), details: { [SUBAGENT_RECEIPT]: receipt } };
2198
2313
  }
2199
2314
  function subagentTools(service, parentRunId) {
2315
+ const quota = service.quota(parentRunId);
2316
+ const budget = `This run allows at most ${quota.children} children: ${Object.entries(quota.profiles).map(([name, value]) => `${value} ${name}`).join(", ")}, ${quota.other} of any other profile and ${quota.unprofiled} unprofiled; a refused spawn means do the work yourself.`;
2200
2317
  const execute = (fn) => async (input) => {
2201
2318
  try {
2202
2319
  return await fn(input);
@@ -2209,7 +2326,7 @@ function subagentTools(service, parentRunId) {
2209
2326
  return [{
2210
2327
  name: SPAWN_SUBAGENT,
2211
2328
  label: "Start a subagent",
2212
- description: "Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. The optional tools field only narrows the profile and parent intersection; in plan mode, omit it unless you need a stricter subset. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children. To resume a child that stopped before finishing (interrupted or failed), pass continue with its run ID and no title, agent or tools: it reopens that child's own conversation with the same profile and tools, and brief becomes an optional extra instruction.",
2329
+ description: "Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. The optional tools field only narrows the profile and parent intersection; in plan mode, omit it unless you need a stricter subset. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children. To resume a child that stopped before finishing (interrupted or failed), pass continue with its run ID and no title, agent or tools: it reopens that child's own conversation with the same profile and tools, and brief becomes an optional extra instruction. " + budget,
2213
2330
  planModeSafe: (input) => service.planSpawnRefusal(parentRunId, input) === null,
2214
2331
  parameters: (Type) => Type.Object({
2215
2332
  title: Type.Optional(Type.String({ description: `Required for a new child: a short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` })),
@@ -3244,8 +3361,8 @@ function browserPluginPart(engine) {
3244
3361
  }
3245
3362
 
3246
3363
  // src/annotations/store.ts
3247
- import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync4 } from "fs";
3248
- import { join as join7 } from "path";
3364
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, rmSync as rmSync4 } from "fs";
3365
+ import { join as join8 } from "path";
3249
3366
  var MAX_NOTES = 1e4;
3250
3367
  var MAX_HISTORY = 100;
3251
3368
  var codepoints = (value) => Array.from(value).length;
@@ -3378,17 +3495,17 @@ function visibleTo(note, caller) {
3378
3495
  return note.grants.some((grant) => grant.principal.sessionId && grant.principal.sessionId === caller.sessionId || grant.principal.agentId && grant.principal.agentId === caller.agentId);
3379
3496
  }
3380
3497
  function notesDir(root) {
3381
- return join7(root, "annotations");
3498
+ return join8(root, "annotations");
3382
3499
  }
3383
3500
  function folder(root) {
3384
3501
  const dir = notesDir(root);
3385
- if (existsSync5(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory()))
3502
+ if (existsSync6(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory()))
3386
3503
  throw new HarnessError("invalid_data", "Note storage must be a plain folder.");
3387
3504
  mkdirSync4(dir, { recursive: true });
3388
3505
  return dir;
3389
3506
  }
3390
3507
  function file(root, id) {
3391
- return join7(folder(root), `${safeId(id)}.json`);
3508
+ return join8(folder(root), `${safeId(id)}.json`);
3392
3509
  }
3393
3510
  function validNote(note, id) {
3394
3511
  if (!note || note.id !== id || note.author !== "user" || !Number.isSafeInteger(note.version) || note.version < 1 || !Array.isArray(note.grants) || !Array.isArray(note.history)) {
@@ -3414,7 +3531,7 @@ function upgradeDelivery(note) {
3414
3531
  }
3415
3532
  function read2(root, id) {
3416
3533
  const path = file(root, id);
3417
- if (!existsSync5(path))
3534
+ if (!existsSync6(path))
3418
3535
  throw notFound("Note");
3419
3536
  if (lstatSync(path).isSymbolicLink())
3420
3537
  throw new HarnessError("invalid_data", "Note records must not be links.");
@@ -3422,7 +3539,7 @@ function read2(root, id) {
3422
3539
  }
3423
3540
  function all2(root) {
3424
3541
  const notes = [];
3425
- for (const name of readdirSync3(folder(root))) {
3542
+ for (const name of readdirSync4(folder(root))) {
3426
3543
  if (!name.endsWith(".json"))
3427
3544
  continue;
3428
3545
  try {
@@ -3465,7 +3582,7 @@ function create(root, input) {
3465
3582
  const target = validateTarget(input?.target);
3466
3583
  const body = normalizeBody(input?.body);
3467
3584
  const tags = normalizeTags(input?.tags);
3468
- if (readdirSync3(folder(root)).filter((name) => name.endsWith(".json")).length >= MAX_NOTES) {
3585
+ if (readdirSync4(folder(root)).filter((name) => name.endsWith(".json")).length >= MAX_NOTES) {
3469
3586
  throw new HarnessError("annotation_limit", `At most ${MAX_NOTES} notes may be kept.`);
3470
3587
  }
3471
3588
  const at = now();
@@ -3531,7 +3648,7 @@ function importNotes(root, notes) {
3531
3648
  if (!id)
3532
3649
  throw invalid("A note needs an id.");
3533
3650
  safeId(id);
3534
- if (existsSync5(file(root, id))) {
3651
+ if (existsSync6(file(root, id))) {
3535
3652
  skipped.push({ id, reason: "exists" });
3536
3653
  continue;
3537
3654
  }
@@ -4739,7 +4856,7 @@ function hubServerMethods(harness) {
4739
4856
  }
4740
4857
 
4741
4858
  // src/refine/runner.ts
4742
- import { join as join8 } from "path";
4859
+ import { join as join9 } from "path";
4743
4860
 
4744
4861
  // src/variant/surface.ts
4745
4862
  var SYSTEM_PROMPT = "systemprompt.md";
@@ -4795,10 +4912,10 @@ var CONFORMANCE_BLOCK = `2. CONFORMANCE. Is it ONLY what was asked for? Where th
4795
4912
 
4796
4913
  // src/refine/runner.ts
4797
4914
  function refineDir(root, id) {
4798
- return join8(root, "refine", safeId(id));
4915
+ return join9(root, "refine", safeId(id));
4799
4916
  }
4800
4917
  function readSummary(root, id) {
4801
- return readJson(join8(refineDir(root, id), "summary.json"));
4918
+ return readJson(join9(refineDir(root, id), "summary.json"));
4802
4919
  }
4803
4920
 
4804
4921
  // src/refine/serverMethods.ts
@@ -4979,15 +5096,15 @@ function historyServerMethods(harness) {
4979
5096
  }
4980
5097
 
4981
5098
  // src/agents/profiles.ts
4982
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync5, rmSync as rmSync5, statSync as statSync3 } from "fs";
4983
- import { basename as basename2, dirname as dirname3, join as join10 } from "path";
5099
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync6, readFileSync as readFileSync6, rmSync as rmSync5, statSync as statSync4 } from "fs";
5100
+ import { basename as basename2, dirname as dirname4, join as join11 } from "path";
4984
5101
 
4985
5102
  // src/core/effort.ts
4986
5103
  var EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
4987
5104
 
4988
5105
  // src/variant/materialise.ts
4989
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
4990
- import { join as join9 } from "path";
5106
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync5 } from "fs";
5107
+ import { join as join10 } from "path";
4991
5108
  function frontmatter(text, field) {
4992
5109
  const m = new RegExp(`^${field}:\\s*(.+)$`, "m").exec(text.slice(0, 600));
4993
5110
  return m?.[1]?.trim() ?? null;
@@ -4999,10 +5116,10 @@ function bodyAfterFrontmatter(text) {
4999
5116
  function subagentOf(dir, name) {
5000
5117
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(name))
5001
5118
  return null;
5002
- const path = join9(dir, "sub_agents", name, "AGENT.md");
5003
- if (!existsSync6(path))
5119
+ const path = join10(dir, "sub_agents", name, "AGENT.md");
5120
+ if (!existsSync7(path))
5004
5121
  return null;
5005
- const text = readFileSync4(path, "utf8");
5122
+ const text = readFileSync5(path, "utf8");
5006
5123
  return { name, description: frontmatter(text, "description") ?? "", instructions: bodyAfterFrontmatter(text) };
5007
5124
  }
5008
5125
 
@@ -5109,16 +5226,16 @@ var PROFILE_COLORS = ["blue", "green", "yellow", "red", "magenta", "cyan", "oran
5109
5226
  function projectRoot(cwd) {
5110
5227
  let dir = cwd;
5111
5228
  for (;; ) {
5112
- if (existsSync7(join10(dir, ".git")))
5229
+ if (existsSync8(join11(dir, ".git")))
5113
5230
  return dir;
5114
- const parent = dirname3(dir);
5231
+ const parent = dirname4(dir);
5115
5232
  if (parent === dir)
5116
5233
  return cwd;
5117
5234
  dir = parent;
5118
5235
  }
5119
5236
  }
5120
5237
  function profileDirs(root, cwd) {
5121
- return { global: join10(root, "agents"), project: cwd ? join10(projectRoot(cwd), ".xingular", "agents") : null };
5238
+ return { global: join11(root, "agents"), project: cwd ? join11(projectRoot(cwd), ".xingular", "agents") : null };
5122
5239
  }
5123
5240
  function stringList(value, field) {
5124
5241
  if (value === undefined || value === null)
@@ -5203,25 +5320,25 @@ function profileText(profile) {
5203
5320
  return serialize(front, profile.instructions);
5204
5321
  }
5205
5322
  function readFolder(dir, source, into, problems) {
5206
- if (!dir || !existsSync7(dir))
5323
+ if (!dir || !existsSync8(dir))
5207
5324
  return;
5208
- for (const file of readdirSync5(dir).filter((entry) => entry.endsWith(".md")).sort()) {
5209
- const path = join10(dir, file);
5325
+ for (const file of readdirSync6(dir).filter((entry) => entry.endsWith(".md")).sort()) {
5326
+ const path = join11(dir, file);
5210
5327
  const name = file.slice(0, -3);
5211
5328
  try {
5212
- if (!statSync3(path).isFile())
5329
+ if (!statSync4(path).isFile())
5213
5330
  continue;
5214
- into.set(name, parseProfile(readFileSync5(path, "utf8"), name, source, path));
5331
+ into.set(name, parseProfile(readFileSync6(path, "utf8"), name, source, path));
5215
5332
  } catch (error) {
5216
5333
  problems.push({ name, source, path, error: error instanceof Error ? error.message : String(error) });
5217
5334
  }
5218
5335
  }
5219
5336
  }
5220
5337
  function variantDefinitions(variantDir) {
5221
- const folder = variantDir ? join10(variantDir, "sub_agents") : null;
5222
- if (!folder || !existsSync7(folder))
5338
+ const folder = variantDir ? join11(variantDir, "sub_agents") : null;
5339
+ if (!folder || !existsSync8(folder))
5223
5340
  return [];
5224
- return readdirSync5(folder).sort().flatMap((name) => {
5341
+ return readdirSync6(folder).sort().flatMap((name) => {
5225
5342
  const definition = RESERVED_PROFILE_NAMES.includes(name) ? null : subagentOf(variantDir, name);
5226
5343
  if (!definition)
5227
5344
  return [];
@@ -5236,7 +5353,7 @@ function variantDefinitions(variantDir) {
5236
5353
  model: null,
5237
5354
  effort: null,
5238
5355
  source: "variant",
5239
- path: join10(folder, name, "AGENT.md")
5356
+ path: join11(folder, name, "AGENT.md")
5240
5357
  }];
5241
5358
  });
5242
5359
  }
@@ -5299,7 +5416,7 @@ function listProfiles(root, cwd, variantDir, packageProfiles = []) {
5299
5416
  profiles.set(definition.name, definition);
5300
5417
  for (const path of packageProfiles) {
5301
5418
  try {
5302
- const profile = parsePackageProfile(readFileSync5(path, "utf8"), path);
5419
+ const profile = parsePackageProfile(readFileSync6(path, "utf8"), path);
5303
5420
  if (!profiles.has(profile.name))
5304
5421
  profiles.set(profile.name, profile);
5305
5422
  else
@@ -5342,8 +5459,8 @@ function createProfile(root, cwd, input) {
5342
5459
  if (input.scope !== "global" && input.scope !== "project")
5343
5460
  throw invalid("scope must be global or project.");
5344
5461
  const dir = scopeDir(root, cwd, input.scope);
5345
- const path = join10(dir, `${name}.md`);
5346
- if (existsSync7(path))
5462
+ const path = join11(dir, `${name}.md`);
5463
+ if (existsSync8(path))
5347
5464
  throw invalid(`There is already a ${input.scope} profile named ${name}.`);
5348
5465
  let source = { ...TEMPLATE, name };
5349
5466
  if (input.from) {
@@ -5362,8 +5479,8 @@ function removeProfile(root, cwd, input) {
5362
5479
  throw invalid("That is not a profile name.");
5363
5480
  if (input.scope !== "global" && input.scope !== "project")
5364
5481
  throw invalid("scope must be global or project.");
5365
- const path = join10(scopeDir(root, cwd, input.scope), `${input.name}.md`);
5366
- if (!existsSync7(path))
5482
+ const path = join11(scopeDir(root, cwd, input.scope), `${input.name}.md`);
5483
+ if (!existsSync8(path))
5367
5484
  throw notFound(`The ${input.scope} profile ${input.name}`);
5368
5485
  rmSync5(path);
5369
5486
  }
@@ -5430,9 +5547,9 @@ function sideQuestionServerMethods(harness) {
5430
5547
 
5431
5548
  // src/core/path.ts
5432
5549
  import { execFileSync } from "child_process";
5433
- import { accessSync, constants, statSync as statSync4 } from "fs";
5550
+ import { accessSync, constants, statSync as statSync5 } from "fs";
5434
5551
  import { homedir } from "os";
5435
- import { delimiter, extname, isAbsolute as isAbsolute2, join as join11 } from "path";
5552
+ import { delimiter, extname, isAbsolute as isAbsolute2, join as join12 } from "path";
5436
5553
  var resolved = null;
5437
5554
  function searchPath() {
5438
5555
  const platform = process.platform;
@@ -5453,7 +5570,7 @@ function searchPath() {
5453
5570
  if (platform !== "win32") {
5454
5571
  const home = homedir();
5455
5572
  if (home)
5456
- entries.push(...[".local/bin", ".bun/bin", ".npm-global/bin", ".cargo/bin"].map((folder) => join11(home, folder)));
5573
+ entries.push(...[".local/bin", ".bun/bin", ".npm-global/bin", ".cargo/bin"].map((folder) => join12(home, folder)));
5457
5574
  entries.push("/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin");
5458
5575
  }
5459
5576
  const path = [...new Set(entries.filter(Boolean))].join(separator);
@@ -5469,7 +5586,7 @@ function pathSeparator(platform) {
5469
5586
  function isExecutable(path) {
5470
5587
  try {
5471
5588
  accessSync(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
5472
- return statSync4(path).isFile();
5589
+ return statSync5(path).isFile();
5473
5590
  } catch {
5474
5591
  return false;
5475
5592
  }
@@ -5486,7 +5603,7 @@ function findBinary(name) {
5486
5603
  }
5487
5604
  for (const directory of searchPath().split(pathSeparator(platform))) {
5488
5605
  for (const candidateName of names) {
5489
- const candidate = join11(directory, candidateName);
5606
+ const candidate = join12(directory, candidateName);
5490
5607
  if (isExecutable(candidate))
5491
5608
  return candidate;
5492
5609
  }
@@ -5632,7 +5749,7 @@ function connectedAppServerMethods(harness) {
5632
5749
  }
5633
5750
 
5634
5751
  // src/tools/dispatch.ts
5635
- import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync6 } from "fs";
5752
+ import { existsSync as existsSync9, readdirSync as readdirSync7, readFileSync as readFileSync7 } from "fs";
5636
5753
  import { join as join16 } from "path";
5637
5754
 
5638
5755
  // src/accounts/store.ts
@@ -5844,10 +5961,10 @@ function readTool2(env, context, params) {
5844
5961
 
5845
5962
  // src/plugins/proposals.ts
5846
5963
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
5847
- import { dirname as dirname4, join as join14 } from "path";
5964
+ import { dirname as dirname5, join as join15 } from "path";
5848
5965
 
5849
5966
  // src/pi/runtime.ts
5850
- import { join as join12 } from "path";
5967
+ import { join as join13 } from "path";
5851
5968
  import { pathToFileURL } from "url";
5852
5969
  var PINNED_PI_VERSION = "1.4.0";
5853
5970
  var HTTP_DISPATCHER_CHUNK = "dist/bundle/chunks/chunk-MM3LKT73.js";
@@ -5876,10 +5993,10 @@ function createPiLoader(importPi, importDispatcher) {
5876
5993
  return loading;
5877
5994
  };
5878
5995
  }
5879
- var loadPi = createPiLoader(() => import("@earendil-works/pi-coding-agent"), async (packageDir) => await import(pathToFileURL(join12(packageDir, HTTP_DISPATCHER_CHUNK)).href));
5996
+ var loadPi = createPiLoader(() => import("@earendil-works/pi-coding-agent"), async (packageDir) => await import(pathToFileURL(join13(packageDir, HTTP_DISPATCHER_CHUNK)).href));
5880
5997
 
5881
5998
  // src/plugins/manifest.ts
5882
- import { basename as basename3, join as join13 } from "path";
5999
+ import { basename as basename3, join as join14 } from "path";
5883
6000
  import { z as z3 } from "zod";
5884
6001
  var connectorTemplateSchema = z3.object({
5885
6002
  id: z3.string().regex(/^[a-z0-9-]+$/),
@@ -5919,14 +6036,14 @@ var PLUGIN_MANIFESTS = ["plugin.json", ".claude-plugin/plugin.json", ".codex-plu
5919
6036
  var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
5920
6037
  function pluginJson(packageRoot) {
5921
6038
  for (const file of PLUGIN_MANIFESTS) {
5922
- const found = readJsonOr(join13(packageRoot, file), null);
6039
+ const found = readJsonOr(join14(packageRoot, file), null);
5923
6040
  if (found && typeof found === "object" && !Array.isArray(found))
5924
6041
  return found;
5925
6042
  }
5926
6043
  return {};
5927
6044
  }
5928
6045
  function packageManifest(packageRoot) {
5929
- const manifest = readJsonOr(join13(packageRoot, "package.json"), {});
6046
+ const manifest = readJsonOr(join14(packageRoot, "package.json"), {});
5930
6047
  const plugin = text6(manifest.name) ? {} : pluginJson(packageRoot);
5931
6048
  const own = manifest.harness && typeof manifest.harness === "object" && !Array.isArray(manifest.harness) ? manifest.harness : {};
5932
6049
  const pluginName = text6(plugin.name);
@@ -5951,8 +6068,8 @@ var SKIPPED = new Set(["node_modules", ".git"]);
5951
6068
  var MAX_FILES = 100;
5952
6069
  var MAX_FILE_BYTES = 256 * 1024;
5953
6070
  var MAX_TOTAL_BYTES = 1024 * 1024;
5954
- var dir = (root) => join14(root, "plugin-proposals");
5955
- var pathOf = (root, id) => join14(dir(root), `${safeId(id)}.json`);
6071
+ var dir = (root) => join15(root, "plugin-proposals");
6072
+ var pathOf = (root, id) => join15(dir(root), `${safeId(id)}.json`);
5956
6073
  function validate2(name, description, input) {
5957
6074
  if (typeof name !== "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name))
5958
6075
  throw invalid("Plugin name must be lowercase letters, digits, underscores or hyphens.");
@@ -5999,26 +6116,6 @@ function create2(root, name, description, files) {
5999
6116
  return proposal;
6000
6117
  }
6001
6118
 
6002
- // src/tools/defaultSkills.ts
6003
- import { existsSync as existsSync8 } from "fs";
6004
- import { dirname as dirname5, join as join15 } from "path";
6005
- import { fileURLToPath } from "url";
6006
- function defaultSkillPath() {
6007
- let dir = dirname5(fileURLToPath(import.meta.url));
6008
- for (;; ) {
6009
- const path = join15(dir, "builtin", "skills", "plugin-authoring", "SKILL.md");
6010
- if (existsSync8(path))
6011
- return path;
6012
- const parent = dirname5(dir);
6013
- if (parent === dir)
6014
- throw new Error("The bundled plugin-authoring skill is missing.");
6015
- dir = parent;
6016
- }
6017
- }
6018
- function defaultSkillPaths(variantDir) {
6019
- return variantDir && existsSync8(join15(variantDir, "skills", "plugin-authoring", "SKILL.md")) ? [] : [defaultSkillPath()];
6020
- }
6021
-
6022
6119
  // src/tools/dispatch.ts
6023
6120
  function str(params, key) {
6024
6121
  const value = params[key];
@@ -6077,25 +6174,24 @@ ${tail}` : ""}`;
6077
6174
  }
6078
6175
  }
6079
6176
  function loadSkill(variantDir, name) {
6080
- const skills = variantDir && existsSync9(join16(variantDir, "skills")) ? readdirSync6(join16(variantDir, "skills"), { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync9(join16(variantDir, "skills", entry.name, "SKILL.md"))).map((entry) => entry.name).sort() : [];
6081
- const defaults = defaultSkillPaths(variantDir);
6082
- if (defaults.length)
6177
+ const skills = variantDir && existsSync9(join16(variantDir, "skills")) ? readdirSync7(join16(variantDir, "skills"), { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync9(join16(variantDir, "skills", entry.name, "SKILL.md"))).map((entry) => entry.name).sort() : [];
6178
+ if (!skills.includes("plugin-authoring") && defaultSkillPaths(variantDir).length)
6083
6179
  skills.push("plugin-authoring");
6084
6180
  skills.sort();
6085
6181
  const wanted = name.trim().replace(/^skills\//, "").replace(/`/g, "");
6086
6182
  if (!wanted)
6087
6183
  return skills.length ? `Your skills: ${skills.join(", ")}. Load one with skill_load and its name.` : "You have no skills.";
6088
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(wanted))
6184
+ if (!SKILL_NAME.test(wanted))
6089
6185
  throw invalid("A skill name is lowercase letters, digits, _ or -.");
6090
6186
  if (!skills.includes(wanted))
6091
6187
  return `There is no skill named ${wanted}.${skills.length ? ` Your skills: ${skills.join(", ")}.` : ""}`;
6092
- if (wanted === "plugin-authoring" && defaults.length)
6093
- return readFileSync6(defaults[0], "utf8");
6094
- const dir = join16(variantDir, "skills", wanted);
6095
- const helpers = existsSync9(join16(dir, "scripts")) ? readdirSync6(join16(dir, "scripts")) : [];
6096
- return `${readFileSync6(join16(dir, "SKILL.md"), "utf8")}${helpers.length ? `
6188
+ const skill = readSkillFile(variantDir, wanted);
6189
+ if (!skill)
6190
+ return `There is no skill named ${wanted}.${skills.length ? ` Your skills: ${skills.join(", ")}.` : ""}`;
6191
+ const helperNote = skill.helpers.length ? `
6097
6192
 
6098
- (This skill's helpers are in ${join16(dir, "scripts")}: ${helpers.join(", ")}.)` : ""}`;
6193
+ (This skill's helpers are in ${join16(skill.baseDir, "scripts")}: ${skill.helpers.join(", ")}.)` : "";
6194
+ return `${readFileSync7(skill.path, "utf8")}${helperNote}`;
6099
6195
  }
6100
6196
 
6101
6197
  // src/tools/harnessTools.ts
@@ -6286,6 +6382,7 @@ var VARIANT_PLUGIN = {
6286
6382
  { id: "memory", name: "Memory", description: "c3: durable facts about this kind of work." },
6287
6383
  { id: "subagents", name: "Subagents", description: "c5: the list of this variant's subagents." },
6288
6384
  { id: "scripts", name: "Scripts", description: "c7: where this variant's scripts are." },
6385
+ { id: "required-skills", name: "Required skills", description: "Skills the person named as required for one request." },
6289
6386
  { id: "reporting", name: "Reporting", description: "Fixed foundation text, not part of the variant: how the final reply is shaped." }
6290
6387
  ],
6291
6388
  tools: tools(SKILL_TOOLS)
@@ -6692,7 +6789,80 @@ import { matchesKey as matchesKey4, truncateToWidth as truncateToWidth4 } from "
6692
6789
  import { fuzzyFilter, matchesKey as matchesKey3, parseColor as parseColor2, truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
6693
6790
 
6694
6791
  // src/terminal/tui/search.ts
6695
- import { Input, matchesKey as matchesKey2, truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
6792
+ import { matchesKey as matchesKey2, truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
6793
+
6794
+ // src/terminal/tui/mouseReport.ts
6795
+ import { Input } from "@earendil-works/pi-tui";
6796
+ var MOUSE_REPORT_TAIL_WINDOW_MS = 200;
6797
+ var REPORT = /^<\d{1,3};\d{1,4};\d{1,4}[Mm]$/;
6798
+ var PARTIAL = /^<\d{0,3}(;\d{0,4}){0,2}$/;
6799
+ var WHOLE = /^(?:\x1b\[)?<\d{1,3};\d{1,4};\d{1,4}[Mm]$/;
6800
+ var SGR_REPORT = /\x1b\[<\d+;\d+;\d+[Mm]|\\u001b\[<\d+;\d+;\d+[Mm]|\\x1b\[<\d+;\d+;\d+[Mm]/g;
6801
+ var X10_REPORT = /\x1b\[M[\u0020-\u00ff]{3}/g;
6802
+
6803
+ class MouseReportTail {
6804
+ text = "";
6805
+ startedAt = 0;
6806
+ push(sequence, nowMs = Date.now()) {
6807
+ if (WHOLE.test(sequence)) {
6808
+ this.reset();
6809
+ return { report: sequence.replace(/^\x1b\[/, ""), inserted: "" };
6810
+ }
6811
+ if (sequence.length !== 1) {
6812
+ this.reset();
6813
+ return;
6814
+ }
6815
+ if (sequence === "<") {
6816
+ this.text = "<";
6817
+ this.startedAt = nowMs;
6818
+ return;
6819
+ }
6820
+ if (!this.text)
6821
+ return;
6822
+ if (nowMs - this.startedAt > MOUSE_REPORT_TAIL_WINDOW_MS) {
6823
+ this.reset();
6824
+ return;
6825
+ }
6826
+ const next = this.text + sequence;
6827
+ if (REPORT.test(next)) {
6828
+ const inserted = this.text;
6829
+ this.reset();
6830
+ return { report: next, inserted };
6831
+ }
6832
+ if (!PARTIAL.test(next)) {
6833
+ this.reset();
6834
+ return;
6835
+ }
6836
+ this.text = next;
6837
+ return;
6838
+ }
6839
+ reset() {
6840
+ this.text = "";
6841
+ this.startedAt = 0;
6842
+ }
6843
+ }
6844
+
6845
+ class MouseSafeInput extends Input {
6846
+ mouseTail = new MouseReportTail;
6847
+ handleInput(data) {
6848
+ super.handleInput(data);
6849
+ const match = this.mouseTail.push(data);
6850
+ if (!match)
6851
+ return;
6852
+ const value = this.getValue();
6853
+ if (value.endsWith(match.report))
6854
+ this.setValue(value.slice(0, -match.report.length));
6855
+ }
6856
+ setValue(value) {
6857
+ this.mouseTail.reset();
6858
+ super.setValue(value);
6859
+ }
6860
+ }
6861
+ function stripMouseReports(text) {
6862
+ return text.replace(SGR_REPORT, "").replace(X10_REPORT, "");
6863
+ }
6864
+
6865
+ // src/terminal/tui/search.ts
6696
6866
  var SEARCH_GLYPH = "\u2315";
6697
6867
  var SEARCH_PLACEHOLDER = "Type to search";
6698
6868
  var SEARCH_FIELD_ROWS = 5;
@@ -6709,7 +6879,7 @@ class SearchField {
6709
6879
  input;
6710
6880
  constructor(theme, placeholder = SEARCH_PLACEHOLDER) {
6711
6881
  this.theme = theme;
6712
- this.input = new Input({ prompt: "", placeholder, placeholderStyle: (text) => this.theme().fg("dim", text) });
6882
+ this.input = new MouseSafeInput({ prompt: "", placeholder, placeholderStyle: (text) => this.theme().fg("dim", text) });
6713
6883
  }
6714
6884
  get value() {
6715
6885
  return this.input.getValue();
@@ -11278,7 +11448,7 @@ var payloadOf2 = (event) => event.payload && typeof event.payload === "object" ?
11278
11448
  var stringOf2 = (value) => typeof value === "string" ? value : undefined;
11279
11449
  var numberOf2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
11280
11450
  function safe2(value) {
11281
- return stripTerminalSequences4(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, " ");
11451
+ return stripTerminalSequences4(stripMouseReports(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, " ");
11282
11452
  }
11283
11453
  function timeOf2(value) {
11284
11454
  const parsed = Date.parse(value);
@@ -11514,8 +11684,8 @@ class TrajectoryPage {
11514
11684
  return this.events.map((event) => ({
11515
11685
  id: String(event.sequence),
11516
11686
  label: `${timeOf2(event.timestamp)} ${event.type}`,
11517
- detail: eventDescription(event),
11518
- searchText: `${event.timestamp} ${event.type} ${eventDescription(event)} ${safe2(JSON.stringify(payloadOf2(event)))}`,
11687
+ detail: safe2(eventDescription(event)),
11688
+ searchText: `${event.timestamp} ${event.type} ${safe2(eventDescription(event))} ${safe2(JSON.stringify(payloadOf2(event)))}`,
11519
11689
  data: event
11520
11690
  }));
11521
11691
  }
@@ -11654,7 +11824,7 @@ class TrajectoryPage {
11654
11824
  if (rows >= 4)
11655
11825
  lines.push(theme.fg("muted", usageLine(this.events)));
11656
11826
  if (rows >= 6)
11657
- lines.push(theme.fg("dim", this.run.goal ? `Prompt: ${this.run.goal.replace(/\s+/g, " ").slice(0, 140)}` : "Prompt text unavailable"));
11827
+ lines.push(theme.fg("dim", this.run.goal ? `Prompt: ${safe2(this.run.goal).replace(/\s+/g, " ").slice(0, 140)}` : "Prompt text unavailable"));
11658
11828
  if (this.error)
11659
11829
  lines.push(theme.fg("error", `Read error: ${this.error}`));
11660
11830
  return lines.slice(0, Math.min(rows, 5)).map((line) => truncateToWidth14(line, width, ""));
@@ -11961,7 +12131,7 @@ class WorktreesPage {
11961
12131
  }
11962
12132
 
11963
12133
  // src/terminal/tui/pages/annotatePage.ts
11964
- import { Input as Input2, matchesKey as matchesKey15, truncateToWidth as truncateToWidth16, visibleWidth as visibleWidth10 } from "@earendil-works/pi-tui";
12134
+ import { matchesKey as matchesKey15, truncateToWidth as truncateToWidth16, visibleWidth as visibleWidth10 } from "@earendil-works/pi-tui";
11965
12135
 
11966
12136
  // src/terminal/tui/annotateCommand.ts
11967
12137
  function normalizeAddress(text) {
@@ -12233,7 +12403,7 @@ class AnnotatePage {
12233
12403
  return "Load the bridge script in the app (shown at the top), then reload the app.";
12234
12404
  }
12235
12405
  openField(kind) {
12236
- const input = new Input2({ prompt: "", placeholder: FIELDS[kind].placeholder, placeholderStyle: (text) => this.options.theme().fg("dim", text) });
12406
+ const input = new MouseSafeInput({ prompt: "", placeholder: FIELDS[kind].placeholder, placeholderStyle: (text) => this.options.theme().fg("dim", text) });
12237
12407
  this.field = { kind, input };
12238
12408
  this.syncFocus();
12239
12409
  this.options.changed();
@@ -14378,15 +14548,6 @@ class ResumeSessionsPage {
14378
14548
  }
14379
14549
  }
14380
14550
 
14381
- // src/terminal/tui/skillInvocation.ts
14382
- function expandSkillInvocation(text, skills) {
14383
- const trimmed = text.trimStart();
14384
- const match = /^\$(\S+)(?=\s|$)/.exec(trimmed);
14385
- if (!match || !skills.has(match[1]))
14386
- return text;
14387
- return `/skill:${match[1]}${trimmed.slice(match[0].length)}`;
14388
- }
14389
-
14390
14551
  // src/terminal/commandHelp.ts
14391
14552
  function formatTerminalCommandLabel(name, aliases) {
14392
14553
  return aliases?.length ? `${name} (${aliases.join(", ")})` : name;
@@ -14478,7 +14639,6 @@ function createTerminalCommands(host) {
14478
14639
  return [];
14479
14640
  return session.resourceLoader.getSkills().skills.map((skill) => ({ name: skill.name, description: skill.description }));
14480
14641
  };
14481
- const expandSkill = (text) => expandSkillInvocation(text, new Set(skills().map((skill) => skill.name)));
14482
14642
  async function openPage(id, options = {}) {
14483
14643
  let close = () => {};
14484
14644
  const page = createCommandPage(host, id, { ...options, close: () => close() });
@@ -14930,7 +15090,7 @@ function createTerminalCommands(host) {
14930
15090
  }
14931
15091
  return true;
14932
15092
  }
14933
- return { execute, completions, skills, expandSkill, legacyAliases };
15093
+ return { execute, completions, skills, legacyAliases };
14934
15094
  }
14935
15095
 
14936
15096
  // src/sessions/planMode.ts
@@ -15072,7 +15232,7 @@ function parseProposedPlan(text) {
15072
15232
  };
15073
15233
  }
15074
15234
  // src/terminal/tui/dialogs.ts
15075
- import { Editor, Input as Input3, matchesKey as matchesKey18, stripTerminalSequences as stripTerminalSequences11, truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth14, wrapTextWithAnsi as wrapTextWithAnsi14 } from "@earendil-works/pi-tui";
15235
+ import { Editor, matchesKey as matchesKey18, stripTerminalSequences as stripTerminalSequences11, truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth14, wrapTextWithAnsi as wrapTextWithAnsi14 } from "@earendil-works/pi-tui";
15076
15236
  function selectTheme(theme) {
15077
15237
  return {
15078
15238
  selectedPrefix: (text) => selectionText(theme, text),
@@ -15083,7 +15243,7 @@ function selectTheme(theme) {
15083
15243
  };
15084
15244
  }
15085
15245
 
15086
- class SecretInput extends Input3 {
15246
+ class SecretInput extends MouseSafeInput {
15087
15247
  render(width) {
15088
15248
  const lines = super.render(width);
15089
15249
  if (!this.getValue())
@@ -15357,7 +15517,7 @@ class TerminalDialogs {
15357
15517
  }
15358
15518
  input(title, placeholder, opts, secret = false) {
15359
15519
  return this.show((done) => {
15360
- const InputType = secret ? SecretInput : Input3;
15520
+ const InputType = secret ? SecretInput : MouseSafeInput;
15361
15521
  const input = new InputType({ prompt: secret ? "" : "> ", placeholder, placeholderStyle: (text) => this.theme().fg("dim", text) });
15362
15522
  input.onSubmit = done;
15363
15523
  input.onEscape = () => done(undefined);
@@ -15449,7 +15609,7 @@ class TerminalDialogs {
15449
15609
  }
15450
15610
 
15451
15611
  // src/terminal/tui/keybindings.ts
15452
- import { readFileSync as readFileSync7 } from "fs";
15612
+ import { readFileSync as readFileSync8 } from "fs";
15453
15613
  import { join as join17 } from "path";
15454
15614
  import { KeybindingsManager, TUI_KEYBINDINGS } from "@earendil-works/pi-tui";
15455
15615
  var actions = {
@@ -15469,7 +15629,7 @@ var actions = {
15469
15629
  "app.thinking.toggle": "ctrl+t",
15470
15630
  "app.editor.external": "ctrl+g",
15471
15631
  "app.message.copy": "ctrl+x",
15472
- "app.message.followUp": ["super+enter", "ctrl+enter", "alt+enter"],
15632
+ "app.message.steer": ["super+enter", "ctrl+enter", "alt+enter"],
15473
15633
  "app.message.dequeue": "alt+up",
15474
15634
  "app.clipboard.pasteImage": process.platform === "darwin" ? ["ctrl+v", "alt+v"] : "ctrl+v",
15475
15635
  "app.session.toggleNamedFilter": "ctrl+n",
@@ -15507,7 +15667,7 @@ var HARNESS_KEYBINDINGS = {
15507
15667
  function readBindings(agentDir) {
15508
15668
  let bindings = {};
15509
15669
  try {
15510
- const source = JSON.parse(readFileSync7(join17(agentDir, "keybindings.json"), "utf8"));
15670
+ const source = JSON.parse(readFileSync8(join17(agentDir, "keybindings.json"), "utf8"));
15511
15671
  if (source && typeof source === "object" && !Array.isArray(source)) {
15512
15672
  bindings = Object.fromEntries(Object.entries(source).filter(([, value]) => typeof value === "string" || Array.isArray(value) && value.every((key) => typeof key === "string")));
15513
15673
  }
@@ -15576,6 +15736,7 @@ import { closeSync as closeSync2, existsSync as existsSync11, openSync as openSy
15576
15736
  import { isAbsolute as isAbsolute5, join as join21, relative as relative4, resolve as resolve4, sep as sep5 } from "path";
15577
15737
 
15578
15738
  // src/terminal/piExtension.ts
15739
+ import { readFileSync as readFileSync9 } from "fs";
15579
15740
  import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from "@earendil-works/pi-coding-agent";
15580
15741
  import { Type as Type2 } from "typebox";
15581
15742
 
@@ -15740,8 +15901,35 @@ function parseInitialBashCommand(input) {
15740
15901
  return outside ? { command: input.slice(outside[0].length).trim(), workspaceWriteScope: "outside" } : { command: input, workspaceWriteScope: "workspace" };
15741
15902
  }
15742
15903
 
15904
+ // src/terminal/tui/skillInvocation.ts
15905
+ var SKILL_TOKEN = /(?:^|[\s(])\$([^\s$]*)$/;
15906
+ var SKILL_INVOCATION = /(?:^|[\s(])\$([A-Za-z0-9][A-Za-z0-9_-]*)(?=[\s.,;:!?)]|$)/gu;
15907
+ function skillTokenAt(text, caret) {
15908
+ const match = SKILL_TOKEN.exec(text.slice(0, caret));
15909
+ if (!match)
15910
+ return null;
15911
+ const query = match[1] ?? "";
15912
+ return { query, from: caret - query.length - 1, to: caret };
15913
+ }
15914
+ function skillInvocationMatches(text) {
15915
+ const found = [];
15916
+ for (const match of text.matchAll(SKILL_INVOCATION)) {
15917
+ const from = match.index + match[0].indexOf("$");
15918
+ found.push({ name: match[1], from, to: from + 1 + match[1].length });
15919
+ }
15920
+ return found;
15921
+ }
15922
+ function skillInvocations(text, skills) {
15923
+ const found = [];
15924
+ for (const match of skillInvocationMatches(text)) {
15925
+ if (skills.has(match.name) && !found.includes(match.name))
15926
+ found.push(match.name);
15927
+ }
15928
+ return found;
15929
+ }
15930
+
15743
15931
  // src/core/workspace.ts
15744
- import { lstatSync as lstatSync2, realpathSync, statSync as statSync5 } from "fs";
15932
+ import { lstatSync as lstatSync2, realpathSync, statSync as statSync6 } from "fs";
15745
15933
  import { homedir as homedir2 } from "os";
15746
15934
  import { basename as basename6, dirname as dirname7, isAbsolute as isAbsolute4, join as join19, relative as relative3, resolve as resolve2, sep as sep4 } from "path";
15747
15935
  function resolveWritePath(candidate, cwd = process.cwd()) {
@@ -15751,7 +15939,7 @@ function resolveWritePath(candidate, cwd = process.cwd()) {
15751
15939
  }
15752
15940
  function isWithinWorkspace(workspaceRoot, canonicalCandidate) {
15753
15941
  const canonicalRoot = realpathSync(workspaceRoot);
15754
- if (!statSync5(canonicalRoot).isDirectory())
15942
+ if (!statSync6(canonicalRoot).isDirectory())
15755
15943
  throw invalid("The workspace root must be a directory.");
15756
15944
  const fromRoot = relative3(canonicalRoot, canonicalCandidate);
15757
15945
  return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep4}`) && !isAbsolute4(fromRoot);
@@ -15760,7 +15948,7 @@ function verifyWorkspaceRoot(workspaceRoot) {
15760
15948
  const canonicalRoot = realpathSync(workspaceRoot);
15761
15949
  if (dirname7(canonicalRoot) === canonicalRoot)
15762
15950
  throw invalid("The filesystem root cannot be used as an active project workspace.");
15763
- if (canonicalRoot !== resolve2(workspaceRoot) || !statSync5(canonicalRoot).isDirectory()) {
15951
+ if (canonicalRoot !== resolve2(workspaceRoot) || !statSync6(canonicalRoot).isDirectory()) {
15764
15952
  throw invalid("The active project workspace changed after this run began.");
15765
15953
  }
15766
15954
  return canonicalRoot;
@@ -15811,7 +15999,7 @@ function inspectWorkspaceWrite(workspaceRoot, cwd, tool, input) {
15811
15999
  import { Type } from "typebox";
15812
16000
 
15813
16001
  // src/core/shell.ts
15814
- import { accessSync as accessSync2, constants as constants2, statSync as statSync6 } from "fs";
16002
+ import { accessSync as accessSync2, constants as constants2, statSync as statSync7 } from "fs";
15815
16003
  import { win32 } from "path";
15816
16004
  var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"];
15817
16005
  var POWERSHELL_UTF8 = `try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}
@@ -15864,7 +16052,7 @@ function isWslLauncher(path, env) {
15864
16052
  function isFile(path) {
15865
16053
  try {
15866
16054
  accessSync2(path, constants2.F_OK);
15867
- return statSync6(path).isFile();
16055
+ return statSync7(path).isFile();
15868
16056
  } catch {
15869
16057
  return false;
15870
16058
  }
@@ -15874,7 +16062,7 @@ function isFile(path) {
15874
16062
  import { AsyncLocalStorage } from "async_hooks";
15875
16063
  import { spawn } from "child_process";
15876
16064
  import { randomUUID as randomUUID3 } from "crypto";
15877
- import { accessSync as accessSync3, constants as constants3, existsSync as existsSync10, lstatSync as lstatSync3, mkdtempSync, realpathSync as realpathSync2, readdirSync as readdirSync7, rmSync as rmSync6, statSync as statSync7 } from "fs";
16065
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync10, lstatSync as lstatSync3, mkdtempSync, realpathSync as realpathSync2, readdirSync as readdirSync8, rmSync as rmSync6, statSync as statSync8 } from "fs";
15878
16066
  import { tmpdir } from "os";
15879
16067
  import { join as join20 } from "path";
15880
16068
  import { spawnSync } from "child_process";
@@ -15896,7 +16084,7 @@ function systemExecutable(candidates) {
15896
16084
  for (const candidate of candidates) {
15897
16085
  try {
15898
16086
  accessSync3(candidate, constants3.X_OK);
15899
- if (statSync7(candidate).isFile())
16087
+ if (statSync8(candidate).isFile())
15900
16088
  return candidate;
15901
16089
  } catch {}
15902
16090
  }
@@ -16133,7 +16321,7 @@ function assertNoExternalHardlinks(workspaceRoot) {
16133
16321
  const visit = (directory) => {
16134
16322
  let entries;
16135
16323
  try {
16136
- entries = readdirSync7(directory, { withFileTypes: true });
16324
+ entries = readdirSync8(directory, { withFileTypes: true });
16137
16325
  } catch (error) {
16138
16326
  throw new Error(`Workspace hard-link inspection failed at ${directory}; Bash was not run: ${error instanceof Error ? error.message : String(error)}`);
16139
16327
  }
@@ -16268,7 +16456,7 @@ function withWorkspaceWriteScope(tool, note = "") {
16268
16456
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
16269
16457
  import { spawn as spawn2 } from "child_process";
16270
16458
  import { createHash as createHash3 } from "crypto";
16271
- import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync8 } from "fs";
16459
+ import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync9 } from "fs";
16272
16460
  import { access, readFile } from "fs/promises";
16273
16461
  import { dirname as dirname8, resolve as resolve3 } from "path";
16274
16462
  var writeAuthorization = new AsyncLocalStorage2;
@@ -16342,7 +16530,7 @@ function nearestExistingDirectory(path) {
16342
16530
  while (true) {
16343
16531
  try {
16344
16532
  const canonical = realpathSync3(current);
16345
- if (!statSync8(canonical).isDirectory())
16533
+ if (!statSync9(canonical).isDirectory())
16346
16534
  throw new Error(`Write scope is not a directory: ${canonical}`);
16347
16535
  return canonical;
16348
16536
  } catch (error) {
@@ -16734,6 +16922,32 @@ async function piExtension_default(pi, planRun, getSession) {
16734
16922
  if (notice)
16735
16923
  return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
16736
16924
  });
16925
+ pi.on("before_agent_start", async (event) => {
16926
+ if (event.messageType !== "user" || /^\s*!/u.test(event.prompt))
16927
+ return;
16928
+ if (pi.getSettings?.().enableSkillCommands === false)
16929
+ return;
16930
+ const skills = (pi.getCommands?.() ?? []).filter((command) => command.source === "skill").map((command) => ({
16931
+ name: command.name.replace(/^skill:/, ""),
16932
+ path: command.sourceInfo.path,
16933
+ ...command.sourceInfo.baseDir ? { baseDir: command.sourceInfo.baseDir } : {}
16934
+ }));
16935
+ if (!skills.length)
16936
+ return;
16937
+ const names = skillInvocations(event.prompt, new Set(skills.map((skill) => skill.name)));
16938
+ if (!names.length)
16939
+ return;
16940
+ const entries = names.map((name) => {
16941
+ const skill = skills.find((candidate) => candidate.name === name);
16942
+ try {
16943
+ return { name, path: skill.path, baseDir: skill.baseDir, body: bodyOf(readFileSync9(skill.path, "utf8")) };
16944
+ } catch {
16945
+ return { name, path: skill.path, body: "" };
16946
+ }
16947
+ });
16948
+ const content = requiredSkillsText(entries);
16949
+ return content ? { message: { customType: "xingular-required-skills", content, display: false, details: { skills: names } } } : undefined;
16950
+ });
16737
16951
  pi.on("before_agent_start", async (event, ctx) => {
16738
16952
  if (event.messageType !== "user")
16739
16953
  return;
@@ -17111,7 +17325,7 @@ class ModeChoices {
17111
17325
  this.quiet -= 1;
17112
17326
  }
17113
17327
  }
17114
- async apply(session, mode, modelFor) {
17328
+ async apply(session, mode, modelFor, fallback) {
17115
17329
  const choice = this.resolve(mode);
17116
17330
  await this.quietly(async () => {
17117
17331
  const model = choice.model ? modelFor(choice.model) : undefined;
@@ -17120,8 +17334,7 @@ class ModeChoices {
17120
17334
  await session.setModel(model, { persist: false });
17121
17335
  } catch {}
17122
17336
  }
17123
- if (choice.effort)
17124
- session.setThinkingLevel(choice.effort, { persist: false });
17337
+ session.setThinkingLevel(choice.effort ?? fallback, { persist: false });
17125
17338
  });
17126
17339
  }
17127
17340
  async record(mode, patch) {
@@ -17271,7 +17484,11 @@ async function createTerminalRuntime(args, host) {
17271
17484
  const modeChoices = new ModeChoices(choiceStore);
17272
17485
  const recordingMode = () => planRun.active ? "plan" : currentMode;
17273
17486
  const modelFor = (reference) => resolveCliModel({ cliModel: reference, modelRuntime }).model;
17274
- const applyMode = () => modeChoices.apply(native.session, currentMode, modelFor);
17487
+ const defaultEffort = (session) => {
17488
+ const model = session.model;
17489
+ return (model ? session.settingsManager.getModelThinkingLevel(model.provider, model.id) : undefined) ?? session.settingsManager.getDefaultThinkingLevel() ?? "medium";
17490
+ };
17491
+ const applyMode = () => modeChoices.apply(native.session, currentMode, modelFor, defaultEffort(native.session));
17275
17492
  const restoreChoice = async (session, previous) => {
17276
17493
  if (previous.model && (session.model?.provider !== previous.model.provider || session.model?.id !== previous.model.id)) {
17277
17494
  await session.setModel(previous.model, { persist: false }).catch(() => {
@@ -17467,7 +17684,7 @@ async function createTerminalRuntime(args, host) {
17467
17684
  planRun.active = true;
17468
17685
  try {
17469
17686
  session.setActiveToolsByName(allowedTools);
17470
- await modeChoices.apply(session, "plan", modelFor);
17687
+ await modeChoices.apply(session, "plan", modelFor, defaultEffort(session));
17471
17688
  await session.prompt(text, { expandPromptTemplates: false, ...images?.length ? { images } : {} });
17472
17689
  } finally {
17473
17690
  planRun.active = false;
@@ -19860,6 +20077,7 @@ class OpenTuiApplication {
19860
20077
  host = new PiDialogHost(() => this.change());
19861
20078
  transcript = new TranscriptModel;
19862
20079
  listeners = new Set;
20080
+ animationListeners = new Set;
19863
20081
  cleanup = [];
19864
20082
  statuses = new Map;
19865
20083
  widgets = new Map;
@@ -19884,8 +20102,10 @@ class OpenTuiApplication {
19884
20102
  editorFactory;
19885
20103
  customEditor;
19886
20104
  draft = "";
20105
+ mouseTail = new MouseReportTail;
19887
20106
  draftEpoch = 0;
19888
20107
  draftCursorOffset = null;
20108
+ paletteCaret = null;
19889
20109
  attachments = new ComposerAttachments;
19890
20110
  insertion = null;
19891
20111
  fileCompletion = null;
@@ -19930,8 +20150,9 @@ class OpenTuiApplication {
19930
20150
  done = new Promise((resolve) => {
19931
20151
  this.finish = resolve;
19932
20152
  });
19933
- pendingPolling = false;
19934
20153
  pendingBusy = false;
20154
+ decisionWatch;
20155
+ pendingRequests = [];
19935
20156
  pendingAbort;
19936
20157
  pendingId;
19937
20158
  dismissedDecisions = new Set;
@@ -19955,14 +20176,17 @@ class OpenTuiApplication {
19955
20176
  const target = raw ? linkTarget(raw, { base: this.runtime?.cwd }) : null;
19956
20177
  const next = target ? `${target.kind}:${"url" in target ? target.url : ("address" in target) ? target.address : target.path}` : raw;
19957
20178
  const entered = next !== this.hoveredLink;
20179
+ if (!entered && !next)
20180
+ return;
19958
20181
  if (!entered && x === this.hoveredAt.x && y === this.hoveredAt.y)
19959
20182
  return;
19960
20183
  this.hoveredLink = next;
19961
20184
  this.hoveredLinkId = next ? this.renderer?.getLinkIdAt(x, y) ?? 0 : 0;
19962
20185
  this.hoveredAt = { x, y };
19963
- if (entered)
20186
+ if (entered) {
19964
20187
  this.renderer?.setMousePointer(next ? "pointer" : "default");
19965
- this.change();
20188
+ this.change();
20189
+ }
19966
20190
  this.renderer?.requestRender();
19967
20191
  }
19968
20192
  activateLinkAt(x, y, modifiers = {}) {
@@ -20003,6 +20227,29 @@ class OpenTuiApplication {
20003
20227
  this.listeners.delete(listener);
20004
20228
  };
20005
20229
  }
20230
+ subscribeAnimation(listener) {
20231
+ this.animationListeners.add(listener);
20232
+ return () => {
20233
+ this.animationListeners.delete(listener);
20234
+ };
20235
+ }
20236
+ animationView() {
20237
+ return {
20238
+ working: this.working(),
20239
+ subagents: subagentRoster(this.subagents, Math.max(20, this.host.columns - 4), this.workingSpinnerFrame(this.startedAt ?? Date.now()))
20240
+ };
20241
+ }
20242
+ animate() {
20243
+ if (this.startedAt !== null || this.compacting) {
20244
+ for (const listener of this.animationListeners) {
20245
+ try {
20246
+ listener();
20247
+ } catch {}
20248
+ }
20249
+ }
20250
+ if (this.sideQuestion?.state === "thinking")
20251
+ this.change();
20252
+ }
20006
20253
  get hasFailure() {
20007
20254
  return !!this.failure;
20008
20255
  }
@@ -20255,16 +20502,18 @@ class OpenTuiApplication {
20255
20502
  this.initialized = true;
20256
20503
  this.refresh();
20257
20504
  this.refreshSubagents(true);
20505
+ const decisionWatch = new AbortController;
20506
+ this.decisionWatch = decisionWatch;
20507
+ this.cleanup.push(() => decisionWatch.abort());
20508
+ this.watchDecisions(decisionWatch.signal);
20258
20509
  let lastSubagentPoll = 0;
20259
20510
  const tick = setInterval(() => {
20260
- if (this.startedAt !== null || this.compacting || this.sideQuestion?.state === "thinking")
20261
- this.change();
20511
+ this.animate();
20262
20512
  const now = Date.now();
20263
20513
  if (this.startedAt !== null && now - lastSubagentPoll >= 1000) {
20264
20514
  lastSubagentPoll = now;
20265
20515
  this.refreshSubagents();
20266
20516
  }
20267
- this.decisions();
20268
20517
  }, 100);
20269
20518
  const git = setInterval(() => {
20270
20519
  this.refreshBranch();
@@ -20570,20 +20819,37 @@ class OpenTuiApplication {
20570
20819
  this.transcript.invalidate();
20571
20820
  this.change();
20572
20821
  }
20573
- async decisions() {
20574
- if (this.pendingPolling || !this.runtime || this.stopped)
20575
- return;
20576
- this.pendingPolling = true;
20577
- let requests;
20578
- try {
20579
- requests = await this.runtime.pending();
20580
- } catch (error) {
20581
- if (!this.stopped)
20822
+ async watchDecisions(signal) {
20823
+ let revision;
20824
+ while (!signal.aborted && !this.stopped) {
20825
+ try {
20826
+ const snapshot = await controlCall("terminal.pending.wait", { revision }, 25000, signal);
20827
+ revision = snapshot.revision;
20828
+ this.decisions(snapshot.requests);
20829
+ } catch (error) {
20830
+ if (signal.aborted || this.stopped)
20831
+ return;
20582
20832
  this.notify(error instanceof Error ? error.message : String(error), "error");
20583
- return;
20584
- } finally {
20585
- this.pendingPolling = false;
20833
+ revision = undefined;
20834
+ await new Promise((resolve) => {
20835
+ const timer = setTimeout(done, 1000);
20836
+ function done() {
20837
+ clearTimeout(timer);
20838
+ signal.removeEventListener("abort", done);
20839
+ resolve();
20840
+ }
20841
+ signal.addEventListener("abort", done, { once: true });
20842
+ if (signal.aborted)
20843
+ done();
20844
+ });
20845
+ }
20586
20846
  }
20847
+ }
20848
+ async decisions(requests) {
20849
+ if (!this.runtime || this.stopped)
20850
+ return;
20851
+ requests ??= await this.runtime.pending();
20852
+ this.pendingRequests = requests;
20587
20853
  if (this.pendingId && !requests.some((request) => request.requestId === this.pendingId))
20588
20854
  this.pendingAbort?.abort();
20589
20855
  if (this.pendingBusy)
@@ -20624,6 +20890,11 @@ class OpenTuiApplication {
20624
20890
  this.pendingBusy = false;
20625
20891
  this.pendingId = undefined;
20626
20892
  this.pendingAbort = undefined;
20893
+ if (!this.stopped && this.pendingRequests.some((item) => !this.dismissedDecisions.has(item.requestId))) {
20894
+ setTimeout(() => {
20895
+ this.decisions(this.pendingRequests);
20896
+ }, 100);
20897
+ }
20627
20898
  }
20628
20899
  }
20629
20900
  async showQuestionDock(questions, signal) {
@@ -20916,7 +21187,7 @@ class OpenTuiApplication {
20916
21187
  if (this.paletteSuppressed)
20917
21188
  return null;
20918
21189
  const slash = /^\/([^\s]*)$/u.exec(this.draft);
20919
- const skill = /^\$([^\s]*)$/u.exec(this.draft);
21190
+ const skill = this.bashMode ? null : skillTokenAt(this.draft, this.paletteCaret ?? this.draft.length);
20920
21191
  if (!this.paletteOpen && (this.bashMode || !slash && !skill))
20921
21192
  return null;
20922
21193
  let items = [];
@@ -20929,7 +21200,7 @@ class OpenTuiApplication {
20929
21200
  } catch {
20930
21201
  return null;
20931
21202
  }
20932
- const filter = this.paletteOpen ? this.paletteFilter : skill?.[1] ?? slash?.[1] ?? "";
21203
+ const filter = this.paletteOpen ? this.paletteFilter : skill?.query ?? slash?.[1] ?? "";
20933
21204
  const matched = matchPalette(items, filter);
20934
21205
  if (!matched.length)
20935
21206
  return null;
@@ -20994,6 +21265,14 @@ class OpenTuiApplication {
20994
21265
  this.paletteOpen = false;
20995
21266
  this.paletteSuppressed = true;
20996
21267
  this.paletteIndex = 0;
21268
+ const token = this.bashMode ? null : skillTokenAt(this.draft, this.paletteCaret ?? this.draft.length);
21269
+ if (token) {
21270
+ const tail = this.draft.slice(token.to);
21271
+ const suffix = tail === "" ? " " : /^[\s.,;:!?)]/u.test(tail) ? "" : " ";
21272
+ const inserted = `${item.prefix ?? "$"}${item.name}${suffix}`;
21273
+ this.setDraftExternal(`${this.draft.slice(0, token.from)}${inserted}${tail}`, token.from + inserted.length);
21274
+ return true;
21275
+ }
20997
21276
  const text = `${item.prefix ?? "/"}${item.name} `;
20998
21277
  this.setDraftExternal(text, text.length);
20999
21278
  return true;
@@ -21007,7 +21286,7 @@ class OpenTuiApplication {
21007
21286
  this.draftEpoch += 1;
21008
21287
  this.change();
21009
21288
  }
21010
- setDraft(value) {
21289
+ setDraft(value, caret) {
21011
21290
  if (this.paletteOpen) {
21012
21291
  this.paletteFilter = value.startsWith("/") && !/\s/u.test(value) ? value.slice(1) : value;
21013
21292
  this.paletteIndex = 0;
@@ -21015,15 +21294,20 @@ class OpenTuiApplication {
21015
21294
  this.change();
21016
21295
  return;
21017
21296
  }
21297
+ let nextCaret = caret;
21018
21298
  let modeChanged = false;
21019
21299
  if (!this.bashMode) {
21020
21300
  const bash = splitBashDraft(value);
21021
21301
  if (bash.bashMode) {
21022
21302
  modeChanged = true;
21023
21303
  this.applyComposerMode("bash");
21304
+ if (nextCaret !== undefined)
21305
+ nextCaret = Math.max(0, nextCaret - (value.length - bash.body.length));
21024
21306
  }
21025
21307
  value = bash.body;
21026
21308
  }
21309
+ if (nextCaret !== undefined)
21310
+ this.paletteCaret = nextCaret;
21027
21311
  if (value === this.draft) {
21028
21312
  if (modeChanged)
21029
21313
  this.change();
@@ -21042,12 +21326,14 @@ class OpenTuiApplication {
21042
21326
  this.change();
21043
21327
  }
21044
21328
  setDraftExternal(value, cursorOffset, preserveBashMode = false) {
21329
+ this.mouseTail.reset();
21045
21330
  this.resetHistoryBrowse();
21046
21331
  const bash = preserveBashMode ? { body: value, bashMode: this.bashMode } : splitBashDraft(value);
21047
21332
  this.invalidateFileCompletion();
21048
21333
  this.applyComposerMode(bash.bashMode ? "bash" : this.planMode ? "plan" : "normal");
21049
21334
  const body = bash.body;
21050
21335
  this.draft = body;
21336
+ this.paletteCaret = null;
21051
21337
  this.attachments.retain(body);
21052
21338
  this.draftCursorOffset = cursorOffset ?? null;
21053
21339
  this.draftEpoch += 1;
@@ -21073,6 +21359,7 @@ class OpenTuiApplication {
21073
21359
  this.invalidateFileCompletion();
21074
21360
  this.applyComposerMode("bash");
21075
21361
  this.draft = body;
21362
+ this.paletteCaret = null;
21076
21363
  this.draftCursorOffset = null;
21077
21364
  this.draftEpoch += 1;
21078
21365
  this.paletteSuppressed = false;
@@ -21088,11 +21375,13 @@ class OpenTuiApplication {
21088
21375
  this.change();
21089
21376
  }
21090
21377
  clearDraft() {
21378
+ this.mouseTail.reset();
21091
21379
  this.resetHistoryBrowse();
21092
21380
  this.invalidateFileCompletion();
21093
21381
  this.applyComposerMode(this.planMode ? "plan" : "normal");
21094
21382
  this.draft = "";
21095
21383
  this.attachments.clear();
21384
+ this.paletteCaret = null;
21096
21385
  this.draftCursorOffset = null;
21097
21386
  this.draftEpoch += 1;
21098
21387
  this.paletteSuppressed = false;
@@ -21136,7 +21425,8 @@ class OpenTuiApplication {
21136
21425
  this.input(() => this.onSubmitPrompt());
21137
21426
  }
21138
21427
  onSubmitPrompt() {
21139
- this.submit(this.bashMode ? `!${this.draft}` : this.draft, false);
21428
+ this.mouseTail.reset();
21429
+ this.submit(this.bashMode ? `!${this.draft}` : this.draft, true);
21140
21430
  }
21141
21431
  async runBash(text) {
21142
21432
  const command = /^\s*!\s*([\s\S]+)$/u.exec(text)?.[1]?.trim();
@@ -21203,16 +21493,15 @@ class OpenTuiApplication {
21203
21493
  this.enqueuePlan(text, images);
21204
21494
  return;
21205
21495
  }
21206
- const expanded = this.commands.expandSkill(text);
21207
21496
  if (this.planBarrierOpen) {
21208
- this.deferredInputs.push({ kind: "prompt", text: expanded, followUp, images });
21497
+ this.deferredInputs.push({ kind: "prompt", text, followUp, images });
21209
21498
  this.notify("Message queued to run after the current response.");
21210
21499
  this.change();
21211
21500
  return;
21212
21501
  }
21213
21502
  if (!this.session.isIdle)
21214
21503
  this.notify(followUp ? "Follow-up message queued." : "Steering message queued.");
21215
- await this.runtime.submit(expanded, followUp, images);
21504
+ await this.runtime.submit(text, followUp, images);
21216
21505
  } finally {
21217
21506
  if (command)
21218
21507
  this.commandBusy = false;
@@ -21353,6 +21642,7 @@ class OpenTuiApplication {
21353
21642
  this.applyComposerMode(mode);
21354
21643
  this.draft = parsed.body;
21355
21644
  this.attachments.clearForDraft(this.draft);
21645
+ this.paletteCaret = null;
21356
21646
  this.draftCursorOffset = this.draft.length;
21357
21647
  this.draftEpoch += 1;
21358
21648
  this.paletteSuppressed = true;
@@ -21607,9 +21897,9 @@ class OpenTuiApplication {
21607
21897
  const hidden = !this.session.settingsManager.getHideThinkingBlock();
21608
21898
  this.session.settingsManager.setHideThinkingBlock(hidden);
21609
21899
  this.transcript.setThinkingHidden(hidden);
21610
- } else if (is("app.message.followUp")) {
21900
+ } else if (is("app.message.steer")) {
21611
21901
  const editor = this.customEditor;
21612
- this.submit(editor?.getExpandedText?.() ?? (this.bashMode ? `!${this.draft}` : this.draft), true);
21902
+ this.submit(editor?.getExpandedText?.() ?? (this.bashMode ? `!${this.draft}` : this.draft), false);
21613
21903
  } else if (is("app.message.dequeue")) {
21614
21904
  const pending = this.session.clearQueue();
21615
21905
  const deferred = this.deferredInputs.map((input) => input.kind === "plan" ? `/plan ${input.text}` : input.text);
@@ -21634,11 +21924,29 @@ class OpenTuiApplication {
21634
21924
  this.attempt(() => this.commands.execute("/new"));
21635
21925
  else if (data === "?" && this.draft === "")
21636
21926
  this.notify(this.hotkeys());
21927
+ else if (this.dropMouseReportTail(key, data))
21928
+ return true;
21637
21929
  else if (!this.extensionShortcut(data))
21638
21930
  return false;
21639
21931
  this.change();
21640
21932
  return true;
21641
21933
  }
21934
+ dropMouseReportTail(key, data) {
21935
+ const match = this.mouseTail.push(data);
21936
+ if (!match)
21937
+ return false;
21938
+ if (!match.inserted) {
21939
+ this.change();
21940
+ return true;
21941
+ }
21942
+ const caret = key.cursorOffset ?? this.draft.length;
21943
+ const start = caret - match.inserted.length;
21944
+ const at = start >= 0 && this.draft.slice(start, caret) === match.inserted ? start : this.draft.endsWith(match.inserted) ? this.draft.length - match.inserted.length : -1;
21945
+ if (at < 0)
21946
+ return false;
21947
+ this.setDraftExternal(this.draft.slice(0, at) + this.draft.slice(at + match.inserted.length), at);
21948
+ return true;
21949
+ }
21642
21950
  hotkeys() {
21643
21951
  return [
21644
21952
  "Tab files/folders \xB7 Shift+Tab plan mode",
@@ -21647,7 +21955,7 @@ class OpenTuiApplication {
21647
21955
  "Ctrl+L model",
21648
21956
  "Ctrl+N new session",
21649
21957
  "\u2191/\u2193 at either draft endpoint browse older/newer history \xB7 Ctrl+\u2191/\u2193 anywhere",
21650
- "Enter send or steer \xB7 Cmd/Ctrl/Alt+Enter queue \xB7 Esc dismisses btw or stops reply",
21958
+ "Enter send or queue \xB7 Cmd/Ctrl/Alt+Enter steer \xB7 Esc dismisses btw or stops reply",
21651
21959
  "Ctrl+C clear or quit \xB7 Ctrl+D quit",
21652
21960
  "PgUp/PgDn transcript"
21653
21961
  ].join(`
@@ -21844,6 +22152,7 @@ class OpenTuiApplication {
21844
22152
  insert(text) {
21845
22153
  if (!text)
21846
22154
  return;
22155
+ this.mouseTail.reset();
21847
22156
  if (this.customEditor?.getExpandedText) {
21848
22157
  const editor = this.customEditor;
21849
22158
  if (editor.insertTextAtCursor) {
@@ -21910,6 +22219,8 @@ class OpenTuiApplication {
21910
22219
  }
21911
22220
  };
21912
22221
  this.stopped = true;
22222
+ this.mouseTail.reset();
22223
+ this.decisionWatch?.abort();
21913
22224
  protect(() => this.renderer?.setMousePointer("default"));
21914
22225
  this.hoveredLink = null;
21915
22226
  if (this.toastTimer)
@@ -23043,32 +23354,40 @@ function bufferOffset(text, index) {
23043
23354
  offset += visibleWidth16(line) + (position < lines.length - 1 ? 1 : 0);
23044
23355
  return offset;
23045
23356
  }
23046
- function leadingSkillInvocationRange(text, names) {
23047
- const match = /^(\s*)\$(\S+)/u.exec(text);
23048
- if (!match || !names.includes(match[2]))
23049
- return;
23050
- return { start: match[1].length, end: match[0].length };
23051
- }
23052
- function syncTextMark(target, range, typeId, styleId) {
23053
- const marks = target.extmarks.getAllForTypeId(typeId);
23054
- if (!range) {
23055
- for (const mark of marks)
23056
- target.extmarks.delete(mark.id);
23057
- return;
23357
+ function indexForBufferOffset(text, offset) {
23358
+ let cells = 0;
23359
+ let index = 0;
23360
+ for (const character of text) {
23361
+ if (cells >= offset)
23362
+ return index;
23363
+ cells += character === `
23364
+ ` ? 1 : visibleWidth16(character);
23365
+ index += character.length;
23058
23366
  }
23059
- const start = bufferOffset(target.plainText, range.start);
23060
- const end = start + visibleWidth16(target.plainText.slice(range.start, range.end));
23061
- if (marks.length === 1 && marks[0].start === start && marks[0].end === end)
23367
+ return text.length;
23368
+ }
23369
+ function skillInvocationRanges(text, names) {
23370
+ return skillInvocationMatches(text).filter((match) => names.includes(match.name)).map((match) => ({ start: match.from, end: match.to }));
23371
+ }
23372
+ function syncTextMarks(target, ranges, typeId, styleId) {
23373
+ const expected = ranges.map((range) => {
23374
+ const start = bufferOffset(target.plainText, range.start);
23375
+ return { start, end: start + visibleWidth16(target.plainText.slice(range.start, range.end)) };
23376
+ });
23377
+ const marks = target.extmarks.getAllForTypeId(typeId).sort((left, right) => left.start - right.start);
23378
+ if (marks.length === expected.length && expected.every((range, index) => marks[index].start === range.start && marks[index].end === range.end))
23062
23379
  return;
23063
23380
  for (const mark of marks)
23064
23381
  target.extmarks.delete(mark.id);
23065
- target.extmarks.create({ start, end, virtual: true, styleId, typeId });
23382
+ for (const range of expected)
23383
+ target.extmarks.create({ start: range.start, end: range.end, virtual: true, styleId, typeId });
23066
23384
  }
23067
23385
  function syncSlashCommandMark(target, names, typeId, styleId) {
23068
- syncTextMark(target, leadingSlashCommandRange(target.plainText, names), typeId, styleId);
23386
+ const range = leadingSlashCommandRange(target.plainText, names);
23387
+ syncTextMarks(target, range ? [range] : [], typeId, styleId);
23069
23388
  }
23070
23389
  function syncSkillInvocationMark(target, names, typeId, styleId) {
23071
- syncTextMark(target, leadingSkillInvocationRange(target.plainText, names), typeId, styleId);
23390
+ syncTextMarks(target, skillInvocationRanges(target.plainText, names), typeId, styleId);
23072
23391
  }
23073
23392
  function syncAttachmentMarks(target, labels, typeId, styleId) {
23074
23393
  const byId = new Map(labels.map((label) => [label.id, label]));
@@ -23695,8 +24014,9 @@ function TranscriptItemView(props) {
23695
24014
  }
23696
24015
  if (item.kind === "plan") {
23697
24016
  return (() => {
23698
- var _el$23 = _$createElement2("box"), _el$24 = _$createElement2("text"), _el$25 = _$createTextNode2(`Plan \xB7 `), _el$26 = _$createElement2("markdown");
24017
+ var _el$23 = _$createElement2("box"), _el$24 = _$createElement2("text"), _el$25 = _$createElement2("box"), _el$26 = _$createElement2("markdown");
23699
24018
  _$insertNode2(_el$23, _el$24);
24019
+ _$insertNode2(_el$23, _el$25);
23700
24020
  _$insertNode2(_el$23, _el$26);
23701
24021
  _$setProp2(_el$23, "border", ["left"]);
23702
24022
  _$setProp2(_el$23, "flexDirection", "column");
@@ -23705,7 +24025,6 @@ function TranscriptItemView(props) {
23705
24025
  _$setProp2(_el$23, "paddingTop", 1);
23706
24026
  _$setProp2(_el$23, "paddingBottom", 1);
23707
24027
  _$setProp2(_el$23, "flexShrink", 0);
23708
- _$insertNode2(_el$24, _el$25);
23709
24028
  _$setProp2(_el$24, "wrapMode", "word");
23710
24029
  _$insert2(_el$24, () => item.title, null);
23711
24030
  _$insert2(_el$24, _$createComponent2(Show2, {
@@ -23718,14 +24037,16 @@ function TranscriptItemView(props) {
23718
24037
  return t.dim;
23719
24038
  },
23720
24039
  attributes: 0,
23721
- children: " \xB7 streaming\u2026"
24040
+ children: " streaming\u2026"
23722
24041
  });
23723
24042
  }
23724
24043
  }), null);
24044
+ _$setProp2(_el$25, "height", 1);
24045
+ _$setProp2(_el$25, "flexShrink", 0);
23725
24046
  _$setProp2(_el$26, "bg", "transparent");
23726
24047
  _$setProp2(_el$26, "conceal", true);
23727
24048
  _$effect2((_p$) => {
23728
- var { planMode: _v$16, panel: _v$17, planMode: _v$18 } = t, _v$19 = TextAttributes2.BOLD, _v$20 = addHeadingOneSpacing(item.body), _v$21 = item.streaming, _v$22 = props.syntax, _v$23 = t.text, _v$24 = {
24049
+ var { planMode: _v$16, panel: _v$17, planMode: _v$18 } = t, _v$19 = TextAttributes2.BOLD | TextAttributes2.UNDERLINE, _v$20 = addHeadingOneSpacing(item.body), _v$21 = item.streaming, _v$22 = props.syntax, _v$23 = t.text, _v$24 = {
23729
24050
  borderColor: t.dim,
23730
24051
  cellPaddingX: 1
23731
24052
  };
@@ -24685,7 +25006,7 @@ function Prompt(props) {
24685
25006
  } finally {
24686
25007
  syncing = false;
24687
25008
  }
24688
- props.app.setDraft(area.plainText);
25009
+ props.app.setDraft(area.plainText, indexForBufferOffset(area.plainText, area.cursorOffset));
24689
25010
  };
24690
25011
  createEffect2(() => {
24691
25012
  view().attachments;
@@ -25347,6 +25668,15 @@ function OpenTuiRoot(props) {
25347
25668
  const [state, setState] = createStore(props.app.view());
25348
25669
  const off = props.app.subscribe(() => setState(reconcile(props.app.view())));
25349
25670
  onCleanup2(off);
25671
+ const offAnimation = props.app.subscribeAnimation(() => {
25672
+ const animation = props.app.animationView();
25673
+ setState("working", reconcile({
25674
+ ...animation.working,
25675
+ sessionName: state.working.sessionName
25676
+ }));
25677
+ setState("subagents", reconcile(animation.subagents));
25678
+ });
25679
+ onCleanup2(offAnimation);
25350
25680
  useSelectionHandler((selection) => {
25351
25681
  const selectedText = terminalSelectionText(selection);
25352
25682
  if (selectedText !== undefined)
@@ -25497,7 +25827,7 @@ function OpenTuiRoot(props) {
25497
25827
  if (key.eventType !== "press" || !key.sequence)
25498
25828
  return;
25499
25829
  if (prompt?.focused)
25500
- props.app.setDraft(prompt.plainText);
25830
+ props.app.setDraft(prompt.plainText, indexForBufferOffset(prompt.plainText, prompt.cursorOffset));
25501
25831
  const current = view();
25502
25832
  const page = Math.max(1, Math.floor((dimensions().height - 8) / 2));
25503
25833
  if (!current.overlay && !current.questionDock && (key.name === "pageup" || key.name === "pagedown" || key.ctrl && (key.name === "home" || key.name === "end"))) {