agent-sh 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +25 -30
  2. package/dist/agent/agent-loop.d.ts +43 -6
  3. package/dist/agent/agent-loop.js +817 -157
  4. package/dist/agent/conversation-state.d.ts +72 -21
  5. package/dist/agent/conversation-state.js +364 -151
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +84 -3
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +34 -1
  15. package/dist/agent/system-prompt.js +96 -47
  16. package/dist/agent/token-budget.d.ts +10 -13
  17. package/dist/agent/token-budget.js +6 -46
  18. package/dist/agent/tool-protocol.d.ts +23 -1
  19. package/dist/agent/tool-protocol.js +169 -4
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +1 -2
  26. package/dist/context-manager.d.ts +16 -19
  27. package/dist/context-manager.js +48 -152
  28. package/dist/core.js +27 -6
  29. package/dist/event-bus.d.ts +59 -3
  30. package/dist/executor.d.ts +4 -3
  31. package/dist/executor.js +18 -15
  32. package/dist/extension-loader.js +75 -17
  33. package/dist/extensions/agent-backend.d.ts +8 -7
  34. package/dist/extensions/agent-backend.js +72 -50
  35. package/dist/extensions/index.js +0 -2
  36. package/dist/extensions/slash-commands.js +14 -9
  37. package/dist/extensions/tui-renderer.js +67 -80
  38. package/dist/index.js +25 -6
  39. package/dist/settings.d.ts +39 -16
  40. package/dist/settings.js +51 -11
  41. package/dist/shell/input-handler.d.ts +2 -1
  42. package/dist/shell/input-handler.js +84 -76
  43. package/dist/shell/shell.js +19 -2
  44. package/dist/types.d.ts +15 -0
  45. package/dist/utils/ansi.d.ts +7 -0
  46. package/dist/utils/ansi.js +69 -8
  47. package/dist/utils/box-frame.js +8 -2
  48. package/dist/utils/compositor.d.ts +5 -0
  49. package/dist/utils/compositor.js +31 -3
  50. package/dist/utils/diff-renderer.d.ts +9 -0
  51. package/dist/utils/diff-renderer.js +221 -143
  52. package/dist/utils/diff.d.ts +21 -2
  53. package/dist/utils/diff.js +165 -89
  54. package/dist/utils/handler-registry.d.ts +5 -0
  55. package/dist/utils/handler-registry.js +6 -0
  56. package/dist/utils/line-editor.d.ts +11 -1
  57. package/dist/utils/line-editor.js +44 -5
  58. package/dist/utils/markdown.js +23 -8
  59. package/dist/utils/package-version.d.ts +1 -0
  60. package/dist/utils/package-version.js +10 -0
  61. package/dist/utils/shell-output-spill.d.ts +2 -0
  62. package/dist/utils/shell-output-spill.js +81 -0
  63. package/dist/utils/tool-display.d.ts +1 -1
  64. package/dist/utils/tool-display.js +4 -4
  65. package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
  66. package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
  67. package/examples/extensions/claude-code-bridge/README.md +14 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +204 -145
  69. package/examples/extensions/claude-code-bridge/package.json +1 -0
  70. package/examples/extensions/interactive-prompts.ts +39 -25
  71. package/examples/extensions/overlay-agent.ts +3 -3
  72. package/examples/extensions/peer-mesh.ts +115 -0
  73. package/examples/extensions/pi-bridge/README.md +16 -0
  74. package/examples/extensions/pi-bridge/index.ts +9 -155
  75. package/examples/extensions/questionnaire.ts +16 -5
  76. package/examples/extensions/subagents.ts +19 -4
  77. package/examples/extensions/terminal-buffer.ts +163 -0
  78. package/examples/extensions/user-shell.ts +136 -0
  79. package/examples/extensions/web-access.ts +8 -0
  80. package/package.json +36 -2
  81. package/dist/agent/tools/display.d.ts +0 -13
  82. package/dist/agent/tools/display.js +0 -70
  83. package/dist/agent/tools/user-shell.d.ts +0 -13
  84. package/dist/agent/tools/user-shell.js +0 -87
  85. package/dist/extensions/shell-recall.d.ts +0 -9
  86. package/dist/extensions/shell-recall.js +0 -8
  87. package/dist/extensions/terminal-buffer.d.ts +0 -14
  88. package/dist/extensions/terminal-buffer.js +0 -134
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { highlight } from "cli-highlight";
14
14
  import { MarkdownRenderer, wrapLine, MAX_CONTENT_WIDTH } from "../utils/markdown.js";
15
+ import { DEFAULT_CONTEXT_WINDOW } from "../agent/token-budget.js";
15
16
  import { createFencedBlockTransform } from "../utils/stream-transform.js";
16
17
  import { palette as p } from "../utils/palette.js";
17
18
  import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
@@ -63,16 +64,21 @@ function createRenderState() {
63
64
  isThinking: false,
64
65
  showThinkingText: false,
65
66
  thinkingPending: false,
66
- lastTruncatedDiff: null,
67
67
  };
68
68
  }
69
69
  export default function activate(ctx) {
70
70
  const { bus, define, compositor } = ctx;
71
71
  const s = createRenderState();
72
+ /** Track the shell's cwd so path shortening is relative to where the user actually is. */
73
+ let shellCwd = process.cwd();
74
+ bus.on("shell:cwd-change", (e) => { shellCwd = e.cwd; });
72
75
  /** Shorthand — get the current agent surface. */
73
76
  function out() { return compositor.surface("agent"); }
74
- /** Capped width for borders, tool lines, and content — keeps everything aligned. */
75
- function cappedW() { return Math.min(MAX_CONTENT_WIDTH + 2, out().columns); }
77
+ /** Capped width for borders, tool lines, and content — keeps everything aligned.
78
+ * MarkdownRenderer.writeLine prepends a 2-char indent (" ") to every line,
79
+ * so available width for actual content is columns - 2. Subtract an additional
80
+ * 1 to prevent terminal auto-wrap when a line lands exactly at the right edge. */
81
+ function cappedW() { return Math.min(MAX_CONTENT_WIDTH + 2, out().columns) - 2 - 1; }
76
82
  // Gate: other extensions (e.g. overlay) can advise this to suppress
77
83
  // TUI rendering of agent output while they own the display.
78
84
  define("tui:should-render-agent", () => true);
@@ -226,7 +232,7 @@ export default function activate(ctx) {
226
232
  s.isThinking = false;
227
233
  if (pendingUsage && s.renderer) {
228
234
  const { prompt_tokens, completion_tokens } = pendingUsage;
229
- const maxTokens = backendInfo?.contextWindow ?? 128_000;
235
+ const maxTokens = backendInfo?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
230
236
  s.renderer.writeLine("");
231
237
  s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
232
238
  drain();
@@ -379,17 +385,37 @@ export default function activate(ctx) {
379
385
  s.renderer.flush();
380
386
  drain();
381
387
  }
388
+ // Diff rendering is handled in the async pipe below so it can yield
389
+ // to the event loop between hunks (keeping the spinner responsive).
390
+ });
391
+ // Async pipe: render diffs via the tui:render-diff handler (extensions can
392
+ // advise to customize). Runs after the sync `on` handler above (which
393
+ // flushes state) and before shell.ts's pipe (which pauses stdout).
394
+ bus.onPipeAsync("permission:request", async (e) => {
395
+ if (!shouldRender())
396
+ return e;
382
397
  if (e.kind === "file-write" && e.metadata?.diff) {
383
398
  showCollapsedThinking();
384
- showFileDiff(e.title, e.metadata.diff);
399
+ const lines = ctx.call("tui:render-diff", e.title, e.metadata.diff, cappedW());
400
+ if (lines.length > 0) {
401
+ if (!s.renderer)
402
+ startAgentResponse();
403
+ contentGap("diff");
404
+ for (const line of lines)
405
+ s.renderer.writeLine(line);
406
+ drain();
407
+ }
408
+ // The diff box IS the visual representation of the upcoming tool call.
409
+ // Mark lastContentKind as "tool" so the tool call line that follows
410
+ // doesn't inject an extra gap between the diff box and the checkmark.
411
+ s.lastContentKind = "tool";
385
412
  }
386
413
  // Don't endAgentResponse() here — permission requests that aren't
387
414
  // file-write diffs are handled inline (auto-approved or by extensions).
388
415
  // Closing the response prematurely causes double separator borders.
416
+ return e;
389
417
  });
390
418
  bus.on("input:keypress", (e) => {
391
- if (e.key === "\x0f")
392
- expandLastDiff(); // Ctrl+O
393
419
  if (e.key === "\x14")
394
420
  toggleThinkingDisplay(); // Ctrl+T
395
421
  });
@@ -518,9 +544,23 @@ export default function activate(ctx) {
518
544
  }
519
545
  let highlighted;
520
546
  try {
521
- highlighted = language
522
- ? highlight(code, { language })
523
- : highlight(code); // auto-detect
547
+ // highlight.js warns to console.error for unsupported languages (elisp, org, etc).
548
+ // Suppress so it doesn't leak into the terminal.
549
+ const origError = console.error;
550
+ console.error = (...args) => {
551
+ const msg = args.join(" ");
552
+ if (msg.includes("Could not find the language"))
553
+ return;
554
+ origError.apply(console, args);
555
+ };
556
+ try {
557
+ highlighted = language
558
+ ? highlight(code, { language })
559
+ : highlight(code); // auto-detect
560
+ }
561
+ finally {
562
+ console.error = origError;
563
+ }
524
564
  }
525
565
  catch {
526
566
  highlighted = code;
@@ -574,6 +614,17 @@ export default function activate(ctx) {
574
614
  }
575
615
  return [];
576
616
  });
617
+ /**
618
+ * Default renderer for standalone diffs (e.g. permission prompts).
619
+ * Extensions can advise this to customize diff rendering:
620
+ *
621
+ * ctx.advise("tui:render-diff", (next, filePath, diff, width) => {
622
+ * return myCustomDiffBox(filePath, diff, width);
623
+ * });
624
+ */
625
+ define("tui:render-diff", (filePath, diff, width) => {
626
+ return renderDiffBody(diff, filePath, width);
627
+ });
577
628
  /** Render a diff as framed box lines (pure — no TUI state side effects). */
578
629
  function renderDiffBody(diff, filePath, width) {
579
630
  if (diff.isIdentical)
@@ -586,18 +637,8 @@ export default function activate(ctx) {
586
637
  maxLines: getSettings().diffMaxLines,
587
638
  trueColor: true,
588
639
  });
589
- const lastLine = diffLines[diffLines.length - 1] ?? "";
590
- const isTruncated = lastLine.includes("… ");
591
- if (isTruncated) {
592
- s.lastTruncatedDiff = { filePath, diff, expanded: false };
593
- }
594
- else {
595
- s.lastTruncatedDiff = null;
596
- }
597
640
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
598
- const footer = isTruncated
599
- ? [` ${p.dim}ctrl+o to expand${p.reset}`]
600
- : undefined;
641
+ const footer = undefined;
601
642
  return renderBoxFrame(body, {
602
643
  width: boxW,
603
644
  style: "rounded",
@@ -625,11 +666,10 @@ export default function activate(ctx) {
625
666
  function extractDetail(extra) {
626
667
  if (extra.locations && extra.locations.length > 0) {
627
668
  const loc = extra.locations[0];
628
- const cwd = process.cwd();
629
669
  const home = process.env.HOME;
630
670
  let fp = loc.path;
631
- if (fp.startsWith(cwd + "/"))
632
- fp = fp.slice(cwd.length + 1);
671
+ if (fp.startsWith(shellCwd + "/"))
672
+ fp = fp.slice(shellCwd.length + 1);
633
673
  else if (home && fp.startsWith(home + "/"))
634
674
  fp = "~/" + fp.slice(home.length + 1);
635
675
  return loc.line ? `${fp}:${loc.line}` : fp;
@@ -642,11 +682,10 @@ export default function activate(ctx) {
642
682
  if (typeof raw.pattern === "string")
643
683
  return raw.pattern;
644
684
  if (typeof raw.path === "string") {
645
- const cwd = process.cwd();
646
685
  const home = process.env.HOME;
647
686
  let fp = raw.path;
648
- if (fp.startsWith(cwd + "/"))
649
- fp = fp.slice(cwd.length + 1);
687
+ if (fp.startsWith(shellCwd + "/"))
688
+ fp = fp.slice(shellCwd.length + 1);
650
689
  else if (home && fp.startsWith(home + "/"))
651
690
  fp = "~/" + fp.slice(home.length + 1);
652
691
  return fp;
@@ -674,7 +713,7 @@ export default function activate(ctx) {
674
713
  locations: extra?.locations,
675
714
  rawInput: extra?.rawInput,
676
715
  displayDetail: extra?.displayDetail,
677
- }, cappedW());
716
+ }, cappedW(), shellCwd);
678
717
  if (extra?.groupContinuation && lines.length > 0) {
679
718
  // Swap the colored kind icon for a muted tree connector,
680
719
  // and strip the tool name prefix — show detail only.
@@ -879,58 +918,6 @@ export default function activate(ctx) {
879
918
  : `${p.success}+${diff.added}${p.reset} ${p.error}-${diff.removed}${p.reset}`;
880
919
  return `${p.dim}${filePath}${p.reset} ${stats}`;
881
920
  }
882
- function showFileDiff(filePath, diff) {
883
- if (diff.isIdentical)
884
- return;
885
- contentGap("diff");
886
- const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, cappedW()) ?? [];
887
- if (!s.renderer)
888
- startAgentResponse();
889
- for (const line of lines) {
890
- s.renderer.writeLine(line);
891
- }
892
- drain();
893
- }
894
- function expandLastDiff() {
895
- if (!s.lastTruncatedDiff)
896
- return;
897
- const entry = s.lastTruncatedDiff;
898
- entry.expanded = !entry.expanded;
899
- if (!entry.expanded) {
900
- showFileDiffCached(entry);
901
- return;
902
- }
903
- if (!entry.expandedLines) {
904
- const { filePath, diff } = entry;
905
- const boxW = Math.min(cappedW() - 2, out().columns - 2); // -2 for writeLine indent
906
- const contentW = boxW - 4;
907
- const diffLines = renderDiff(diff, {
908
- width: contentW,
909
- filePath,
910
- maxLines: 500,
911
- trueColor: true,
912
- });
913
- const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
914
- entry.expandedLines = renderBoxFrame(body, {
915
- width: boxW,
916
- style: "rounded",
917
- borderColor: p.dim,
918
- title: diffTitle(filePath, diff),
919
- footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
920
- });
921
- }
922
- out().write("\n");
923
- for (const line of entry.expandedLines) {
924
- out().write(line + "\n");
925
- }
926
- }
927
- function showFileDiffCached(entry) {
928
- const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, cappedW()) ?? [];
929
- out().write("\n");
930
- for (const line of lines) {
931
- out().write(line + "\n");
932
- }
933
- }
934
921
  function toggleThinkingDisplay() {
935
922
  s.showThinkingText = !s.showThinkingText;
936
923
  if (s.spinner) {
package/dist/index.js CHANGED
@@ -15,6 +15,13 @@ import { discoverSkills } from "./agent/skills.js";
15
15
  */
16
16
  async function captureShellEnvAsync(shell) {
17
17
  return new Promise((resolve) => {
18
+ let settled = false;
19
+ const done = (result) => {
20
+ if (settled)
21
+ return;
22
+ settled = true;
23
+ resolve(result);
24
+ };
18
25
  try {
19
26
  const shellName = path.basename(shell);
20
27
  const isZsh = shellName.includes("zsh");
@@ -30,8 +37,9 @@ async function captureShellEnvAsync(shell) {
30
37
  output += data.toString("utf-8");
31
38
  });
32
39
  child.on("close", (code) => {
40
+ clearTimeout(timer);
33
41
  if (code !== 0 || !output) {
34
- resolve({});
42
+ done({});
35
43
  return;
36
44
  }
37
45
  const env = {};
@@ -40,18 +48,19 @@ async function captureShellEnvAsync(shell) {
40
48
  if (eq > 0)
41
49
  env[entry.slice(0, eq)] = entry.slice(eq + 1);
42
50
  }
43
- resolve(env);
51
+ done(env);
44
52
  });
45
53
  child.on("error", () => {
46
- resolve({});
54
+ clearTimeout(timer);
55
+ done({});
47
56
  });
48
- setTimeout(() => {
57
+ const timer = setTimeout(() => {
49
58
  child.kill("SIGTERM");
50
- resolve({});
59
+ done({});
51
60
  }, 5000);
52
61
  }
53
62
  catch {
54
- resolve({});
63
+ done({});
55
64
  }
56
65
  });
57
66
  }
@@ -245,6 +254,9 @@ async function main() {
245
254
  if (process.env.DEBUG) {
246
255
  console.error('[agent-sh] Extensions loaded');
247
256
  }
257
+ // Tell deferred-init listeners (agent-backend) that the provider
258
+ // registry is now complete.
259
+ core.bus.emit("core:extensions-loaded", {});
248
260
  // ── Discover skills ───────────────────────────────────────────
249
261
  const skills = discoverSkills(process.cwd());
250
262
  // ── Activate agent backend ────────────────────────────────────
@@ -281,6 +293,13 @@ async function main() {
281
293
  sections += `\n ${p.dim}${s.name}${p.reset}`;
282
294
  }
283
295
  }
296
+ const extSections = bus.emitPipe("banner:collect", { sections: [] }).sections;
297
+ for (const sec of extSections) {
298
+ sections += `\n\n ${p.muted}${sec.label}:${p.reset}`;
299
+ for (const item of sec.items) {
300
+ sections += `\n ${p.dim}${item}${p.reset}`;
301
+ }
302
+ }
284
303
  const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
285
304
  const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
286
305
  process.stdout.write("\n" + borderLine + "\n" +
@@ -32,44 +32,56 @@ export interface Settings {
32
32
  defaultProvider?: string;
33
33
  /** Preferred agent backend (extension name, e.g. "pi", "claude-code"). */
34
34
  defaultBackend?: string;
35
- /** Recent exchanges included in agent context window. */
36
- contextWindowSize?: number;
37
- /** Context budget in bytes (~4 chars per token). */
38
- contextBudget?: number;
39
- /** Shell output lines before truncation kicks in. */
35
+ /** Shell output lines before spill-to-tempfile kicks in. */
40
36
  shellTruncateThreshold?: number;
41
- /** Lines kept from start of truncated shell output. */
37
+ /** Lines kept from start of spilled shell output. */
42
38
  shellHeadLines?: number;
43
- /** Lines kept from end of truncated shell output. */
39
+ /** Lines kept from end of spilled shell output. */
44
40
  shellTailLines?: number;
45
- /** Max lines for recall expand before requiring line ranges. */
46
- recallExpandMaxLines?: number;
47
- /** Fraction of content budget allocated to shell context (0-1, default 0.35). */
48
- shellContextRatio?: number;
49
41
  /** Max history file size in bytes (default: 102400 = 100KB). */
50
42
  historyMaxBytes?: number;
51
43
  /** Number of prior history entries to load on startup (default: 50). */
52
44
  historyStartupEntries?: number;
53
- /** Max nuclear entries kept in-context before flushing to history file (default: 200). */
54
- nuclearMaxEntries?: number;
55
45
  /** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
56
46
  autoCompactThreshold?: number;
57
47
  /** Max command output lines shown inline in TUI. */
58
48
  maxCommandOutputLines?: number;
59
49
  /** Max read tool output lines shown inline in TUI (0 = hide). */
60
50
  readOutputMaxLines?: number;
61
- /** Max diff lines shown before "ctrl+o to expand". */
51
+ /** Max diff lines rendered in the TUI (Infinity = no limit). */
62
52
  diffMaxLines?: number;
63
- /** Tool protocol: "api" (all tools), "deferred" (extensions via meta-tool), "inline" (text). */
64
- toolMode?: "api" | "deferred" | "inline";
53
+ /** Tool protocol:
54
+ * "api" all tools sent with full schema.
55
+ * "deferred" — extensions dispatched through `use_extension(name, args)` meta-tool.
56
+ * "deferred-lookup" — extensions loaded on demand via `load_tool(names[])`; once loaded, callable as first-class tools.
57
+ * "inline" — tools described as text.
58
+ */
59
+ toolMode?: "api" | "deferred" | "deferred-lookup" | "inline";
65
60
  /** Additional directories to scan for skills (supports ~ expansion). */
66
61
  skillPaths?: string[];
62
+ /**
63
+ * Enable the "diagnose" tool — lets the agent evaluate JavaScript
64
+ * expressions against its own runtime state. Powerful for introspection
65
+ * (e.g. this.conversation.turns.length) but grants arbitrary code
66
+ * execution within the agent process. Off by default because the
67
+ * agent already has unrestricted bash access — this is a convenience,
68
+ * not a new capability.
69
+ */
70
+ diagnose?: boolean;
67
71
  /** Show a startup banner when agent-sh launches. */
68
72
  startupBanner?: boolean;
69
73
  /** Show a subtle agent-sh indicator in the shell prompt. */
70
74
  promptIndicator?: boolean;
71
75
  /** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
72
76
  disabledBuiltins?: string[];
77
+ /**
78
+ * Names of user extensions in ~/.agent-sh/extensions/ to skip when
79
+ * auto-discovering. Match by basename without extension for files
80
+ * (e.g. "peer-mesh" matches peer-mesh.ts), or by directory name for
81
+ * directory-style extensions (e.g. "superash" matches superash/index.ts).
82
+ * Beats having to rename files to .disabled every time.
83
+ */
84
+ disabledExtensions?: string[];
73
85
  }
74
86
  declare const DEFAULTS: Required<Settings>;
75
87
  /** Load settings from disk (cached after first call). */
@@ -87,6 +99,17 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
87
99
  export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
88
100
  /** Reset cached settings (for testing or after external edit). */
89
101
  export declare function reloadSettings(): void;
102
+ /**
103
+ * Deep-merge a patch into ~/.agent-sh/settings.json on disk.
104
+ *
105
+ * Reads the raw file (preserving unknown keys), merges the patch, writes back
106
+ * with 2-space indentation, and clears the cache so subsequent getSettings()
107
+ * calls see the new values.
108
+ *
109
+ * Used by runtime controls (`/model`, `/backend`) that want their selection
110
+ * to persist as the default across restarts.
111
+ */
112
+ export declare function updateSettings(patch: Record<string, unknown>): void;
90
113
  /**
91
114
  * Expand $ENV_VAR references in a string.
92
115
  * Supports $VAR and ${VAR} syntax.
package/dist/settings.js CHANGED
@@ -16,24 +16,21 @@ const DEFAULTS = {
16
16
  defaultProvider: undefined,
17
17
  defaultBackend: "ash",
18
18
  toolMode: "api",
19
- contextWindowSize: 20,
20
- contextBudget: 16384,
21
- shellTruncateThreshold: 10,
22
- shellHeadLines: 5,
23
- shellTailLines: 5,
24
- recallExpandMaxLines: 100,
25
- shellContextRatio: 0.35,
26
- historyMaxBytes: 102400,
27
- historyStartupEntries: 50,
28
- nuclearMaxEntries: 200,
19
+ shellTruncateThreshold: 20,
20
+ shellHeadLines: 10,
21
+ shellTailLines: 10,
22
+ historyMaxBytes: 104857600, // 100MB — history is only accessed via search/expand, never loaded wholesale
23
+ historyStartupEntries: 100,
29
24
  autoCompactThreshold: 0.5,
30
25
  maxCommandOutputLines: 3,
31
26
  readOutputMaxLines: 10,
32
- diffMaxLines: 20,
27
+ diffMaxLines: Infinity,
33
28
  skillPaths: [],
29
+ diagnose: false,
34
30
  startupBanner: true,
35
31
  promptIndicator: true,
36
32
  disabledBuiltins: [],
33
+ disabledExtensions: [],
37
34
  };
38
35
  let cached = null;
39
36
  /** Load settings from disk (cached after first call). */
@@ -74,6 +71,49 @@ export function getExtensionSettings(namespace, defaults) {
74
71
  export function reloadSettings() {
75
72
  cached = null;
76
73
  }
74
+ /**
75
+ * Deep-merge a patch into ~/.agent-sh/settings.json on disk.
76
+ *
77
+ * Reads the raw file (preserving unknown keys), merges the patch, writes back
78
+ * with 2-space indentation, and clears the cache so subsequent getSettings()
79
+ * calls see the new values.
80
+ *
81
+ * Used by runtime controls (`/model`, `/backend`) that want their selection
82
+ * to persist as the default across restarts.
83
+ */
84
+ export function updateSettings(patch) {
85
+ let existing = {};
86
+ try {
87
+ const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
88
+ existing = JSON.parse(raw);
89
+ }
90
+ catch {
91
+ // file missing or unreadable — start fresh
92
+ }
93
+ const merged = deepMerge(existing, patch);
94
+ try {
95
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
96
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
97
+ cached = null;
98
+ }
99
+ catch (err) {
100
+ console.error(`[agent-sh] Warning: failed to update ${SETTINGS_PATH}: ${err.message}`);
101
+ }
102
+ }
103
+ function deepMerge(target, source) {
104
+ const out = { ...target };
105
+ for (const [key, val] of Object.entries(source)) {
106
+ const existing = out[key];
107
+ if (val !== null && typeof val === "object" && !Array.isArray(val) &&
108
+ existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
109
+ out[key] = deepMerge(existing, val);
110
+ }
111
+ else {
112
+ out[key] = val;
113
+ }
114
+ }
115
+ return out;
116
+ }
77
117
  /**
78
118
  * Expand $ENV_VAR references in a string.
79
119
  * Supports $VAR and ${VAR} syntax.
@@ -28,7 +28,8 @@ export declare class InputHandler {
28
28
  private history;
29
29
  private historyIndex;
30
30
  private savedBuffer;
31
- private promptWrappedLines;
31
+ private cursorRowsBelow;
32
+ private cursorTermCol;
32
33
  private escapeTimer;
33
34
  private bus;
34
35
  private onShowAgentInfo;