agent-sh 0.14.4 → 0.14.6

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.
@@ -192,11 +192,15 @@ export class AgentLoop {
192
192
  const modes = this.pullModes();
193
193
  const prev = this.activeMode;
194
194
  const fresh = modes.find((m) => m.model === prev.model && m.provider === prev.provider);
195
+ let identityChanged = false;
195
196
  if (fresh) {
196
197
  this.activeMode = fresh;
197
198
  if (fresh.providerConfig && fresh.providerConfig !== prev.providerConfig) {
198
199
  this.llmClient.reconfigure({ ...fresh.providerConfig, model: fresh.model });
199
200
  }
201
+ identityChanged = fresh.model !== prev.model
202
+ || fresh.provider !== prev.provider
203
+ || fresh.contextWindow !== prev.contextWindow;
200
204
  }
201
205
  else if (prev.provider) {
202
206
  // Ghost: keep prev active so mid-turn stream() doesn't switch models.
@@ -204,7 +208,8 @@ export class AgentLoop {
204
208
  message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
205
209
  });
206
210
  }
207
- this.emitIdentity();
211
+ if (identityChanged)
212
+ this.emitIdentity();
208
213
  this.bus.emit("config:changed", {});
209
214
  });
210
215
  onPipe("config:get-models", () => {
@@ -320,6 +320,8 @@ export default function agentBackend(ctx) {
320
320
  bus.emit("agent:modes-changed", {});
321
321
  if (!ashActive)
322
322
  return;
323
+ if (buildModes().some((m) => m.model === llmClient.model))
324
+ return;
323
325
  const pendingProvider = getSettings().defaultProvider;
324
326
  if (!pendingProvider)
325
327
  return;
@@ -5,7 +5,8 @@ import { InputHandler } from "./input-handler.js";
5
5
  import { OutputParser } from "./output-parser.js";
6
6
  import { getSettings } from "../core/settings.js";
7
7
  import { clearOpost } from "../utils/tty.js";
8
- import { processTerminal } from "./terminal.js";
8
+ import { processTerminal, surfaceFromTerminal } from "./terminal.js";
9
+ import { TuiInputView } from "./tui-input-view.js";
9
10
  import { pickStrategy, FALLBACK_STRATEGY, SUPPORTED_SHELL_NAMES, } from "./strategies/index.js";
10
11
  export class Shell {
11
12
  ptyProcess;
@@ -96,6 +97,7 @@ export class Shell {
96
97
  bus: opts.bus,
97
98
  handlers: opts.handlers,
98
99
  onShowAgentInfo: opts.onShowAgentInfo ?? (() => ({ info: "" })),
100
+ view: new TuiInputView(surfaceFromTerminal(this.terminal)),
99
101
  });
100
102
  this.setupOutput();
101
103
  this.setupInput();
@@ -25,6 +25,32 @@ export interface Terminal {
25
25
  }
26
26
  /** Default Terminal: wraps process.stdin/stdout. */
27
27
  export declare function processTerminal(): Terminal;
28
+ /**
29
+ * No-op terminal for non-rendering hosts (tests, agent-only embeds).
30
+ * Writes are discarded; input/resize never fire.
31
+ */
32
+ export declare function headlessTerminal(cols?: number, rows?: number): Terminal;
33
+ /**
34
+ * Pipe-based terminal for embedders that own their own renderer (web hubs
35
+ * via xterm.js, electron windows, recording harnesses). Bytes from the
36
+ * Shell flow through `onWrite`; the host drives `pushInput`/`pushResize`
37
+ * to forward keystrokes and viewport changes back.
38
+ */
39
+ export declare class BridgedTerminal implements Terminal {
40
+ private readonly onWrite;
41
+ private inputCbs;
42
+ private resizeCbs;
43
+ private _cols;
44
+ private _rows;
45
+ constructor(onWrite: (data: string) => void, cols?: number, rows?: number);
46
+ write(data: string): void;
47
+ onInput(cb: (d: string) => void): () => void;
48
+ onResize(cb: (c: number, r: number) => void): () => void;
49
+ cols(): number;
50
+ rows(): number;
51
+ pushInput(data: string): void;
52
+ pushResize(cols: number, rows: number): void;
53
+ }
28
54
  /**
29
55
  * Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
30
56
  * the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
@@ -45,6 +45,50 @@ export function processTerminal() {
45
45
  },
46
46
  };
47
47
  }
48
+ /**
49
+ * No-op terminal for non-rendering hosts (tests, agent-only embeds).
50
+ * Writes are discarded; input/resize never fire.
51
+ */
52
+ export function headlessTerminal(cols = 100, rows = 30) {
53
+ return {
54
+ write() { },
55
+ onInput: () => () => { },
56
+ onResize: () => () => { },
57
+ cols: () => cols,
58
+ rows: () => rows,
59
+ };
60
+ }
61
+ /**
62
+ * Pipe-based terminal for embedders that own their own renderer (web hubs
63
+ * via xterm.js, electron windows, recording harnesses). Bytes from the
64
+ * Shell flow through `onWrite`; the host drives `pushInput`/`pushResize`
65
+ * to forward keystrokes and viewport changes back.
66
+ */
67
+ export class BridgedTerminal {
68
+ onWrite;
69
+ inputCbs = new Set();
70
+ resizeCbs = new Set();
71
+ _cols;
72
+ _rows;
73
+ constructor(onWrite, cols = 100, rows = 30) {
74
+ this.onWrite = onWrite;
75
+ this._cols = cols;
76
+ this._rows = rows;
77
+ }
78
+ write(data) { this.onWrite(data); }
79
+ onInput(cb) { this.inputCbs.add(cb); return () => { this.inputCbs.delete(cb); }; }
80
+ onResize(cb) { this.resizeCbs.add(cb); return () => { this.resizeCbs.delete(cb); }; }
81
+ cols() { return this._cols; }
82
+ rows() { return this._rows; }
83
+ pushInput(data) { for (const cb of this.inputCbs)
84
+ cb(data); }
85
+ pushResize(cols, rows) {
86
+ this._cols = cols;
87
+ this._rows = rows;
88
+ for (const cb of this.resizeCbs)
89
+ cb(cols, rows);
90
+ }
91
+ }
48
92
  /**
49
93
  * Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
50
94
  * the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
@@ -43,16 +43,13 @@ export class StatusFooter extends Container {
43
43
  const contentWidth = width > 0 ? Math.max(1, width - 2) : 0;
44
44
  const right = this.buildRight();
45
45
  const rightWidth = visibleWidth(right);
46
- const join = (left: string): string => {
47
- if (!right) return left;
48
- const leftWidth = visibleWidth(left);
49
- const gap = Math.max(1, contentWidth - leftWidth - rightWidth);
50
- return `${left}${" ".repeat(gap)}${right}`;
51
- };
52
- const full = this.buildLine("full");
53
- const fullFits = contentWidth === 0
54
- || visibleWidth(full) + (right ? rightWidth + 1 : 0) <= contentWidth;
55
- this.text.setText(fullFits ? join(full) : join(this.buildLine("basename")));
46
+ const left = this.buildLine();
47
+ if (!right) {
48
+ this.text.setText(left);
49
+ return;
50
+ }
51
+ const gap = Math.max(1, contentWidth - visibleWidth(left) - rightWidth);
52
+ this.text.setText(`${left}${" ".repeat(gap)}${right}`);
56
53
  }
57
54
 
58
55
  private buildRight(): string {
@@ -62,7 +59,7 @@ export class StatusFooter extends Container {
62
59
  return "";
63
60
  }
64
61
 
65
- private buildLine(cwdMode: "full" | "basename"): string {
62
+ private buildLine(): string {
66
63
  const { model, provider, contextWindow, cwd, branch, leaf, tokens, compactions, thinking } = this.fields;
67
64
  const sep = theme.fg("dim", " | ");
68
65
  const parts: string[] = [];
@@ -73,7 +70,7 @@ export class StatusFooter extends Container {
73
70
  } else if (provider) {
74
71
  parts.push(theme.fg("muted", `@${provider}`));
75
72
  }
76
- if (cwd) parts.push(theme.fg("muted", formatCwd(cwd, cwdMode)));
73
+ if (cwd) parts.push(theme.fg("muted", basename(cwd) || cwd));
77
74
  if (branch) parts.push(theme.fg("muted", `⎇ ${branch}`));
78
75
  if (leaf != null && leaf > 0) parts.push(theme.fg("muted", `#${leaf}`));
79
76
  if (tokens != null) {
@@ -86,14 +83,6 @@ export class StatusFooter extends Container {
86
83
  }
87
84
  }
88
85
 
89
- function formatCwd(cwd: string, mode: "full" | "basename"): string {
90
- if (mode === "basename") return basename(cwd) || cwd;
91
- const home = process.env.HOME;
92
- if (home && cwd.startsWith(`${home}/`)) return `~/${cwd.slice(home.length + 1)}`;
93
- if (home && cwd === home) return "~";
94
- return cwd;
95
- }
96
-
97
86
  function fmtTokens(n: number): string {
98
87
  if (n < 1000) return String(n);
99
88
  if (n < 100_000) return `${(n / 1000).toFixed(1)}k`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,6 +41,10 @@
41
41
  "types": "./dist/shell/shell.d.ts",
42
42
  "default": "./dist/shell/shell.js"
43
43
  },
44
+ "./shell/host": {
45
+ "types": "./dist/shell/index.d.ts",
46
+ "default": "./dist/shell/index.js"
47
+ },
44
48
  "./shell/strategies": {
45
49
  "types": "./dist/shell/strategies/index.d.ts",
46
50
  "default": "./dist/shell/strategies/index.js"