agent-sh 0.13.4 → 0.13.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.
@@ -42,6 +42,7 @@ export declare class AgentLoop implements AgentBackend {
42
42
  private ctorListeners;
43
43
  private ctorPipeListeners;
44
44
  private lastProjectSkillNames;
45
+ private lastAgentInfo;
45
46
  private sessionStartTime;
46
47
  private toolCallCounts;
47
48
  private totalToolCalls;
@@ -99,6 +100,7 @@ export declare class AgentLoop implements AgentBackend {
99
100
  private cancel;
100
101
  private reasoningParams;
101
102
  private get currentMode();
103
+ private emitAgentInfoIfChanged;
102
104
  private get currentModel();
103
105
  /**
104
106
  * Run compaction via the `conversation:compact` handler. After any
@@ -60,6 +60,7 @@ export class AgentLoop {
60
60
  ctorListeners = [];
61
61
  ctorPipeListeners = [];
62
62
  lastProjectSkillNames = new Set();
63
+ lastAgentInfo = null;
63
64
  // ── Session telemetry — behavioral self-awareness ──────────────
64
65
  // Every ash deserves to know what it's been doing. This tracks the
65
66
  // agent's own behavioral patterns across the session: which tools
@@ -181,16 +182,7 @@ export class AgentLoop {
181
182
  message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
182
183
  });
183
184
  }
184
- const active = this.modes[this.currentModeIndex];
185
- if (active && active.contextWindow !== prev?.contextWindow) {
186
- this.bus.emit("agent:info", {
187
- name: "ash",
188
- version: PACKAGE_VERSION,
189
- model: active.model,
190
- provider: active.provider,
191
- contextWindow: active.contextWindow,
192
- });
193
- }
185
+ this.emitAgentInfoIfChanged();
194
186
  this.bus.emit("config:changed", {});
195
187
  });
196
188
  // Fires before wire() too — agent-backend emits this from
@@ -208,6 +200,7 @@ export class AgentLoop {
208
200
  else {
209
201
  this.llmClient.model = m.model;
210
202
  }
203
+ this.emitAgentInfoIfChanged();
211
204
  this.bus.emit("config:changed", {});
212
205
  });
213
206
  const getToolsPipe = () => ({ tools: this.getTools() });
@@ -256,7 +249,7 @@ export class AgentLoop {
256
249
  this.llmClient.model = m.model;
257
250
  }
258
251
  const label = m.provider ? `${m.provider}: ${m.model}` : m.model;
259
- this.bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: m.model, provider: m.provider, contextWindow: m.contextWindow });
252
+ this.emitAgentInfoIfChanged();
260
253
  // Persist as the new default — selection survives restart.
261
254
  // Safe even for dynamic providers: agent-backend defers mode
262
255
  // resolution to `core:extensions-loaded`, so the extension gets
@@ -375,6 +368,8 @@ export class AgentLoop {
375
368
  this.bus.emit("conversation:message-appended", { role: "system", content: note });
376
369
  }
377
370
  });
371
+ this.lastAgentInfo = null;
372
+ this.emitAgentInfoIfChanged();
378
373
  }
379
374
  /** Unsubscribe from bus events — deactivates this backend. */
380
375
  unwire() {
@@ -518,6 +513,23 @@ export class AgentLoop {
518
513
  get currentMode() {
519
514
  return this.modes[this.currentModeIndex];
520
515
  }
516
+ emitAgentInfoIfChanged() {
517
+ const m = this.modes[this.currentModeIndex];
518
+ if (!m)
519
+ return;
520
+ const prev = this.lastAgentInfo;
521
+ if (prev && prev.model === m.model && prev.provider === m.provider && prev.contextWindow === m.contextWindow) {
522
+ return;
523
+ }
524
+ this.lastAgentInfo = { model: m.model, provider: m.provider, contextWindow: m.contextWindow };
525
+ this.bus.emit("agent:info", {
526
+ name: "ash",
527
+ version: PACKAGE_VERSION,
528
+ model: m.model,
529
+ provider: m.provider,
530
+ contextWindow: m.contextWindow,
531
+ });
532
+ }
521
533
  get currentModel() {
522
534
  return this.modes[this.currentModeIndex].model;
523
535
  }
@@ -2,7 +2,6 @@ import { AgentLoop } from "./agent-loop.js";
2
2
  import { LlmClient } from "../utils/llm-client.js";
3
3
  import { createLlmFacade } from "../utils/llm-facade.js";
4
4
  import { resolveProvider, getProviderNames, getSettings } from "../core/settings.js";
5
- import { PACKAGE_VERSION } from "../utils/package-version.js";
6
5
  import { discoverSkills } from "./skills.js";
7
6
  import { resolveApiKey } from "../cli/auth/keys.js";
8
7
  import activateOpenrouter from "./providers/openrouter.js";
@@ -227,13 +226,6 @@ export default function agentBackend(ctx) {
227
226
  });
228
227
  },
229
228
  });
230
- bus.emit("agent:info", {
231
- name: "ash",
232
- version: PACKAGE_VERSION,
233
- model: llmClient.model,
234
- provider: modes[initialModeIndex]?.provider,
235
- contextWindow: modes[initialModeIndex]?.contextWindow,
236
- });
237
229
  },
238
230
  });
239
231
  });
@@ -330,9 +322,7 @@ export default function agentBackend(ctx) {
330
322
  };
331
323
  });
332
324
  bus.emit("config:set-modes", { modes: newModes });
333
- bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: switchModel, provider: name, contextWindow: p.contextWindow });
334
325
  bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
335
- bus.emit("config:changed", {});
336
326
  });
337
327
  bus.onPipe("banner:collect", (e) => {
338
328
  if (e.activeBackend && e.activeBackend !== "ash")
package/dist/cli/index.js CHANGED
@@ -200,7 +200,8 @@ async function main() {
200
200
  catch {
201
201
  // Ignore errors, we already have process.env as fallback
202
202
  }
203
- if (!config.apiKey && !config.provider && !anyProviderConfigured()) {
203
+ const selectedBackend = config.backend ?? getSettings().defaultBackend ?? "ash";
204
+ if (selectedBackend === "ash" && !config.apiKey && !config.provider && !anyProviderConfigured()) {
204
205
  console.error("\nagent-sh: no LLM provider configured.\n\n" +
205
206
  " Run `agent-sh auth login` to store an API key, or\n" +
206
207
  " export OPENAI_API_KEY / OPENROUTER_API_KEY / DEEPSEEK_API_KEY, or\n" +
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanyilun/ashi",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Ash in an interactive TUI — agent-sh's built-in agent without the shell underneath",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -175,6 +175,7 @@ export function mountAshi(
175
175
 
176
176
  const chat = new Container();
177
177
  const footerSlot = new Container();
178
+ const queueSlot = new Container();
178
179
  const editor = new Editor(tui, editorTheme(), { paddingX: 1 });
179
180
  editor.setAutocompleteProvider(new BusAutocompleteProvider(bus));
180
181
  editor.onSubmit = (text) => {
@@ -188,6 +189,12 @@ export function mountAshi(
188
189
  bus.emit("command:execute", { name, args });
189
190
  return;
190
191
  }
192
+ if (processing) {
193
+ queuedQueries.push(query);
194
+ renderQueueSlot();
195
+ tui.requestRender();
196
+ return;
197
+ }
191
198
  bus.emit("agent:submit", { query });
192
199
  };
193
200
 
@@ -211,6 +218,7 @@ export function mountAshi(
211
218
 
212
219
  tui.addChild(chat);
213
220
  tui.addChild(footerSlot);
221
+ tui.addChild(queueSlot);
214
222
  tui.addChild(editor);
215
223
  tui.addChild(statusFooter);
216
224
  tui.setFocus(editor);
@@ -227,6 +235,16 @@ export function mountAshi(
227
235
  let lastToolResult: ToolResultView | null = null;
228
236
  let loader: Loader | null = null;
229
237
  let processing = false;
238
+ const queuedQueries: string[] = [];
239
+
240
+ const renderQueueSlot = (): void => {
241
+ queueSlot.clear();
242
+ for (const q of queuedQueries) {
243
+ const oneLine = q.replace(/\s+/g, " ");
244
+ const preview = oneLine.length > 80 ? oneLine.slice(0, 77) + "…" : oneLine;
245
+ queueSlot.addChild(new InfoLine(`↳ queued: ${preview}`));
246
+ }
247
+ };
230
248
  let hideThinking = true;
231
249
 
232
250
  const renderState = (): { state: Record<string, unknown>; invalidate: () => void } => ({
@@ -552,6 +570,11 @@ export function mountAshi(
552
570
  chat.addChild(new Spacer(1));
553
571
  refreshFooterStats();
554
572
  refreshBranch();
573
+ const next = queuedQueries.shift();
574
+ if (next !== undefined) {
575
+ renderQueueSlot();
576
+ bus.emit("agent:submit", { query: next });
577
+ }
555
578
  tui.requestRender();
556
579
  });
557
580
 
@@ -612,6 +635,9 @@ export function mountAshi(
612
635
 
613
636
  // ── Pickers ────────────────────────────────────────────────────
614
637
  let pickerOpen = false;
638
+ let activeSessionPicker: SelectList | null = null;
639
+ let activeSessionRepopulate: ((keepIndex?: number) => boolean) | null = null;
640
+ let activeSessionClose: (() => void) | null = null;
615
641
 
616
642
  const openTreePicker = async (): Promise<void> => {
617
643
  if (pickerOpen) return;
@@ -657,38 +683,60 @@ export function mountAshi(
657
683
 
658
684
  const openSessionPicker = async (): Promise<void> => {
659
685
  if (pickerOpen) return;
660
- const currentId = getStore().current().id;
661
- const list = getStore().listSessions().filter((s) => s.id !== currentId);
662
- if (list.length === 0) {
663
- bus.emit("ui:info", { message: "no past sessions in this cwd" });
664
- return;
665
- }
666
- const items: SelectItem[] = list.map((s) => ({
667
- value: s.id,
668
- label: formatSessionRow(s, false),
669
- }));
670
- const picker = new SelectList(items, 15, selectListTheme());
686
+
687
+ const hint = new InfoLine("↑↓ move · enter: resume · d: delete · esc: cancel");
671
688
 
672
689
  const close = (): void => {
690
+ if (activeSessionPicker) footerSlot.removeChild(activeSessionPicker);
691
+ footerSlot.removeChild(hint);
692
+ activeSessionPicker = null;
693
+ activeSessionRepopulate = null;
694
+ activeSessionClose = null;
673
695
  pickerOpen = false;
674
- footerSlot.removeChild(picker);
675
696
  tui.setFocus(editor);
676
697
  tui.requestRender();
677
698
  };
678
699
 
679
- picker.onSelect = async (item) => {
680
- const id = item.value;
681
- close();
682
- resumeSession(ctx, getStore, capture, id);
683
- bus.emit("ui:info", { message: `resumed session ${id}` });
684
- await rebuildChat();
685
- refreshFooterStats();
700
+ const populate = (keepIndex?: number): boolean => {
701
+ if (activeSessionPicker) footerSlot.removeChild(activeSessionPicker);
702
+ const currentId = getStore().current().id;
703
+ const list = getStore().listSessions().filter((s) => s.id !== currentId);
704
+ if (list.length === 0) {
705
+ activeSessionPicker = null;
706
+ return false;
707
+ }
708
+ const items: SelectItem[] = list.map((s) => ({
709
+ value: s.id,
710
+ label: formatSessionRow(s, false),
711
+ }));
712
+ const picker = new SelectList(items, 15, selectListTheme());
713
+ if (keepIndex !== undefined) {
714
+ picker.setSelectedIndex(Math.min(keepIndex, items.length - 1));
715
+ }
716
+ picker.onSelect = async (item) => {
717
+ const id = item.value;
718
+ close();
719
+ resumeSession(ctx, getStore, capture, id);
720
+ bus.emit("ui:info", { message: `resumed session ${id}` });
721
+ await rebuildChat();
722
+ refreshFooterStats();
723
+ };
724
+ picker.onCancel = close;
725
+ activeSessionPicker = picker;
726
+ footerSlot.addChild(picker);
727
+ tui.setFocus(picker);
728
+ return true;
686
729
  };
687
- picker.onCancel = close;
688
730
 
731
+ footerSlot.addChild(hint);
732
+ if (!populate()) {
733
+ footerSlot.removeChild(hint);
734
+ bus.emit("ui:info", { message: "no past sessions in this cwd" });
735
+ return;
736
+ }
689
737
  pickerOpen = true;
690
- footerSlot.addChild(picker);
691
- tui.setFocus(picker);
738
+ activeSessionRepopulate = populate;
739
+ activeSessionClose = close;
692
740
  tui.requestRender();
693
741
  };
694
742
 
@@ -711,6 +759,31 @@ export function mountAshi(
711
759
  bus.emit("agent:cancel-request", {});
712
760
  return { consume: true };
713
761
  }
762
+ if (activeSessionPicker && matchesKey(data, "d")) {
763
+ const selected = activeSessionPicker.getSelectedItem();
764
+ if (selected) {
765
+ const currentId = getStore().current().id;
766
+ const idx = getStore().listSessions()
767
+ .filter((s) => s.id !== currentId)
768
+ .findIndex((s) => s.id === selected.value);
769
+ try {
770
+ getStore().deleteSession(selected.value);
771
+ } catch (e) {
772
+ bus.emit("ui:error", { message: `delete failed: ${(e as Error).message}` });
773
+ return { consume: true };
774
+ }
775
+ if (!activeSessionRepopulate?.(idx)) activeSessionClose?.();
776
+ tui.requestRender();
777
+ }
778
+ return { consume: true };
779
+ }
780
+ if (matchesKey(data, "up") && queuedQueries.length > 0 && editor.getText().length === 0) {
781
+ const last = queuedQueries.pop()!;
782
+ renderQueueSlot();
783
+ editor.setText(last);
784
+ tui.requestRender();
785
+ return { consume: true };
786
+ }
714
787
  if (matchesKey(data, "ctrl+c")) {
715
788
  editor.setText("");
716
789
  return { consume: true };
@@ -81,6 +81,14 @@ export class MultiSessionStore {
81
81
  return this.currentStore;
82
82
  }
83
83
 
84
+ deleteSession(id: string): void {
85
+ if (id === this.currentStore.id) throw new Error("cannot delete the active session");
86
+ const filePath = this.sessionFile(id);
87
+ for (const p of [filePath, filePath + ".leaf", filePath + ".meta"]) {
88
+ try { fs.unlinkSync(p); } catch { /* missing siblings are fine */ }
89
+ }
90
+ }
91
+
84
92
  listSessions(): SessionInfo[] {
85
93
  let names: string[];
86
94
  try { names = fs.readdirSync(this.dir); } catch { return []; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",