agent-sh 0.12.26 → 0.12.27

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.
@@ -1105,7 +1105,7 @@ export class AgentLoop {
1105
1105
  // Compact deeply — shallow targets buy only 1–2 turns of runway on
1106
1106
  // tool-heavy workloads.
1107
1107
  const target = Math.floor(threshold * 0.25);
1108
- const result = this.compactWithHooks(target, 6);
1108
+ const result = this.compactWithHooks(target, 1);
1109
1109
  if (!result) {
1110
1110
  // Auto-compact fired but nothing was evictable. This can happen
1111
1111
  // in short conversations with heavy tool output where the pin
@@ -1488,7 +1488,7 @@ export class AgentLoop {
1488
1488
  if (this.isContextOverflow(e)) {
1489
1489
  const contextWindow = this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
1490
1490
  const target = Math.floor((contextWindow - RESPONSE_RESERVE) * 0.6);
1491
- const stats = this.compactWithHooks(target, 6);
1491
+ const stats = this.compactWithHooks(target, 1);
1492
1492
  // If compaction freed nothing, retrying will hit the same error.
1493
1493
  // Surface the real failure instead of looping until exhaustion.
1494
1494
  if (!stats || stats.after >= stats.before) {
@@ -29,6 +29,19 @@ function firstMatchExcerpt(text, regex) {
29
29
  function recencyWeight(idx, total) {
30
30
  return Math.max(0.1, 1 - idx / total);
31
31
  }
32
+ // Head+tail because the start (command, opening lines) and end (final
33
+ // result, exit code) are the informative parts of shell/file output.
34
+ function slimToolContent(content, maxLen) {
35
+ const exitMatch = content.match(/exit code:?\s*(\d+)/i);
36
+ const exitSuffix = exitMatch ? ` (exit ${exitMatch[1]})` : "";
37
+ const lines = content.split("\n");
38
+ if (lines.length > 6) {
39
+ const head = lines.slice(0, 3).join("\n");
40
+ const tail = lines.slice(-2).join("\n");
41
+ return `${head}\n... [${lines.length - 5} lines trimmed by compact]\n${tail}${exitSuffix}`;
42
+ }
43
+ return `${content.slice(0, maxLen)}\n... [${content.length - maxLen} chars trimmed by compact]${exitSuffix}`;
44
+ }
32
45
  /**
33
46
  * Conversation state with eager nucleation — shell-history shaped.
34
47
  *
@@ -611,6 +624,7 @@ export class ConversationState {
611
624
  // ── Internal: Two-tier pin for recent turns ────────────────────
612
625
  slimTurn(messages) {
613
626
  const MAX_RESULT_LEN = 1500;
627
+ const MAX_ASSISTANT_LEN = 1500;
614
628
  const result = [];
615
629
  const droppedToolIds = new Set();
616
630
  for (const msg of messages) {
@@ -642,13 +656,20 @@ export class ConversationState {
642
656
  continue;
643
657
  const content = typeof msg.content === "string" ? msg.content : "";
644
658
  if (content.length > MAX_RESULT_LEN) {
645
- result.push({ ...msg, content: content.slice(0, MAX_RESULT_LEN) + "\n... [truncated by compact]" });
659
+ result.push({ ...msg, content: slimToolContent(content, MAX_RESULT_LEN) });
646
660
  }
647
661
  else {
648
662
  result.push(msg);
649
663
  }
650
664
  continue;
651
665
  }
666
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.length > MAX_ASSISTANT_LEN) {
667
+ const head = msg.content.slice(0, Math.floor(MAX_ASSISTANT_LEN * 0.6));
668
+ const tail = msg.content.slice(-Math.floor(MAX_ASSISTANT_LEN * 0.2));
669
+ const trimmed = msg.content.length - head.length - tail.length;
670
+ result.push({ ...msg, content: `${head}\n... [${trimmed} chars trimmed by compact]\n${tail}` });
671
+ continue;
672
+ }
652
673
  result.push(msg);
653
674
  }
654
675
  return result;
package/dist/install.js CHANGED
@@ -69,11 +69,13 @@ function pickResolver(spec) {
69
69
  return r;
70
70
  return bundledResolver;
71
71
  }
72
- function maybeNpmInstall(target) {
72
+ function readPackageJson(target) {
73
73
  const pkgJson = path.join(target, "package.json");
74
74
  if (!fs.existsSync(pkgJson))
75
- return;
76
- const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
75
+ return null;
76
+ return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
77
+ }
78
+ function maybeNpmInstall(target, pkg) {
77
79
  const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
78
80
  if (Object.keys(deps).length === 0)
79
81
  return;
@@ -88,6 +90,56 @@ function maybeNpmInstall(target) {
88
90
  throw new Error(`npm install failed in ${target}; run it manually.`);
89
91
  }
90
92
  }
93
+ function normalizeBin(pkg) {
94
+ if (!pkg.bin)
95
+ return {};
96
+ if (typeof pkg.bin === "string") {
97
+ const name = pkg.name?.startsWith("@") ? pkg.name.split("/")[1] : pkg.name;
98
+ return name ? { [name]: pkg.bin } : {};
99
+ }
100
+ return pkg.bin;
101
+ }
102
+ function maybeNpmBuild(target, pkg) {
103
+ if (!pkg.scripts?.build)
104
+ return;
105
+ const binPaths = Object.values(normalizeBin(pkg)).map((p) => path.join(target, p));
106
+ if (binPaths.length === 0)
107
+ return;
108
+ if (binPaths.every((p) => fs.existsSync(p)))
109
+ return;
110
+ console.log(`Running npm run build in ${target}...`);
111
+ const result = spawnSync("npm", ["run", "build"], { cwd: target, stdio: "inherit" });
112
+ if (result.status !== 0) {
113
+ throw new Error(`npm run build failed in ${target}; run it manually.`);
114
+ }
115
+ }
116
+ function linkBins(target, pkg) {
117
+ const bins = normalizeBin(pkg);
118
+ if (Object.keys(bins).length === 0)
119
+ return [];
120
+ const binDir = path.join(CONFIG_DIR, "bin");
121
+ fs.mkdirSync(binDir, { recursive: true });
122
+ const linked = [];
123
+ for (const [name, relPath] of Object.entries(bins)) {
124
+ const src = path.resolve(target, relPath);
125
+ if (!fs.existsSync(src)) {
126
+ console.error(`agent-sh: skipping bin "${name}" — ${src} not found`);
127
+ continue;
128
+ }
129
+ try {
130
+ fs.chmodSync(src, 0o755);
131
+ }
132
+ catch { /* ignore */ }
133
+ const linkPath = path.join(binDir, name);
134
+ try {
135
+ fs.unlinkSync(linkPath);
136
+ }
137
+ catch { /* ignore */ }
138
+ fs.symlinkSync(src, linkPath);
139
+ linked.push(name);
140
+ }
141
+ return linked;
142
+ }
91
143
  export async function runInstall(spec, opts = {}) {
92
144
  if (!spec) {
93
145
  console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
@@ -114,10 +166,16 @@ export async function runInstall(spec, opts = {}) {
114
166
  }
115
167
  fs.rmSync(target, { recursive: true, force: true });
116
168
  }
169
+ let linkedBins = [];
117
170
  if (resolved.isDirectory) {
118
171
  fs.cpSync(resolved.sourcePath, target, { recursive: true });
119
172
  try {
120
- maybeNpmInstall(target);
173
+ const pkg = readPackageJson(target);
174
+ if (pkg) {
175
+ maybeNpmInstall(target, pkg);
176
+ maybeNpmBuild(target, pkg);
177
+ linkedBins = linkBins(target, pkg);
178
+ }
121
179
  }
122
180
  catch (err) {
123
181
  console.error(`agent-sh: ${err instanceof Error ? err.message : String(err)}`);
@@ -128,6 +186,11 @@ export async function runInstall(spec, opts = {}) {
128
186
  fs.copyFileSync(resolved.sourcePath, target);
129
187
  }
130
188
  console.log(`Installed: ${resolved.name} -> ${target}`);
189
+ if (linkedBins.length > 0) {
190
+ const binDir = path.join(CONFIG_DIR, "bin");
191
+ console.log(`Linked bins: ${linkedBins.join(", ")} -> ${binDir}`);
192
+ console.log(`Add to PATH: export PATH="${binDir}:$PATH"`);
193
+ }
131
194
  }
132
195
  export async function runUninstall(name) {
133
196
  if (!name) {
@@ -146,6 +209,23 @@ export async function runUninstall(name) {
146
209
  console.error(`agent-sh: not installed: ${name}`);
147
210
  process.exit(1);
148
211
  }
212
+ const pkg = readPackageJson(target);
213
+ if (pkg) {
214
+ const binDir = path.join(CONFIG_DIR, "bin");
215
+ const targetPrefix = path.resolve(target) + path.sep;
216
+ for (const binName of Object.keys(normalizeBin(pkg))) {
217
+ const linkPath = path.join(binDir, binName);
218
+ try {
219
+ const stat = fs.lstatSync(linkPath, { throwIfNoEntry: false });
220
+ if (!stat?.isSymbolicLink())
221
+ continue;
222
+ const dest = path.resolve(binDir, fs.readlinkSync(linkPath));
223
+ if (dest.startsWith(targetPrefix))
224
+ fs.unlinkSync(linkPath);
225
+ }
226
+ catch { /* ignore */ }
227
+ }
228
+ }
149
229
  fs.rmSync(target, { recursive: true, force: true });
150
230
  console.log(`Uninstalled: ${name}`);
151
231
  }
@@ -147,7 +147,8 @@ export declare class FloatingPanel {
147
147
  * - `{prefix}:render-border-bottom(ctx: FrameContext) -> string`
148
148
  * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
149
149
  * - `{prefix}:submit(query: string) -> void`
150
- * - `{prefix}:dismiss() -> void`
150
+ * - `{prefix}:hide() -> void` (screen down; conversation state preserved)
151
+ * - `{prefix}:reset() -> void` (conversation state cleared)
151
152
  * - `{prefix}:show() -> void`
152
153
  * - `{prefix}:input(data: string) -> boolean`
153
154
  * - `{prefix}:build-row(content: string, width: number) -> string`
@@ -177,6 +178,9 @@ export declare class FloatingPanel {
177
178
  private wrapCacheWidth;
178
179
  private passthroughTimer;
179
180
  private prevSerialized;
181
+ private autocompleteItems;
182
+ private autocompleteIndex;
183
+ private autocompleteActive;
180
184
  constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
181
185
  private registerDefaultHandlers;
182
186
  private wireEvents;
@@ -196,8 +200,10 @@ export declare class FloatingPanel {
196
200
  hide(): void;
197
201
  /** Show the panel again after hide(), preserving conversation. */
198
202
  show(): void;
199
- /** Fully destroy the panel, resetting all state. */
200
- dismiss(): void;
203
+ /** End the conversation: screen down and all buffered state cleared. */
204
+ reset(): void;
205
+ /** Screen-only teardown; conversation state is left untouched. */
206
+ private teardownToHidden;
201
207
  /** Common screen enter logic shared by open() and show(). */
202
208
  private enterScreen;
203
209
  appendText(text: string): void;
@@ -213,8 +219,14 @@ export declare class FloatingPanel {
213
219
  scrollDown(lines?: number): void;
214
220
  getInput(): string;
215
221
  requestRender(): void;
222
+ private updateAutocomplete;
223
+ private applyAutocomplete;
224
+ private clearAutocomplete;
216
225
  private handleIntercept;
217
- /** Handle scroll input. Returns true if consumed. */
226
+ /**
227
+ * Handle scroll input. Returns true if consumed.
228
+ * Pass `includeArrows=false` in input phase so arrows reach the editor.
229
+ */
218
230
  private handleScroll;
219
231
  private handleInputKey;
220
232
  /** Compute box geometry from config + current viewport. */