@alisio/alisio-code 0.1.0-alpha.21 → 0.1.0-alpha.22

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.
package/dist/main.js CHANGED
@@ -200,7 +200,7 @@ async function run(cmd, prompt, sessionId) {
200
200
  process.stderr.write(`[startup] ${JSON.stringify(d)}\n`);
201
201
  }
202
202
  if (!opts.quiet)
203
- console.log("Alisio · /exit /new /skill:name /command plugin.id:name args");
203
+ console.log("Alisio · /exit /new /btw question /skill:name /command plugin.id:name args");
204
204
  process.stdout.write("\nalisio › ");
205
205
  try {
206
206
  for await (const rawLine of rl) {
@@ -217,6 +217,22 @@ async function run(cmd, prompt, sessionId) {
217
217
  process.stdout.write("\nalisio › ");
218
218
  continue;
219
219
  }
220
+ if (line === "/btw" || line.startsWith("/btw ")) {
221
+ // Side question: printed here only, never added to the conversation.
222
+ try {
223
+ const { CommandCatalog } = await import("@alisio/core");
224
+ const result = await new CommandCatalog(app).execute("btw", line.slice(4), {
225
+ sessionId: session,
226
+ signal: controller.signal,
227
+ });
228
+ console.log(result.text ?? "");
229
+ }
230
+ catch (e) {
231
+ console.error(e instanceof Error ? e.message : String(e));
232
+ }
233
+ process.stdout.write("\nalisio › ");
234
+ continue;
235
+ }
220
236
  if (line.startsWith("/command ")) {
221
237
  const [name, ...args] = line.slice(9).split(" ");
222
238
  const handler = app.plugins.commands.get(name ?? "");
package/dist/tui/app.js CHANGED
@@ -3,8 +3,9 @@ import { CombinedAutocompleteProvider, Container, Editor, getImageDimensions, ge
3
3
  import { loadVersion } from "../version.js";
4
4
  import { activeAgentCatalog, agentPickerItems, agentRunOptions, mainAgentFromRecord, resolveActiveAgent, } from "./agents.js";
5
5
  import { MAX_ATTACHMENTS_PER_MESSAGE, MAX_IMAGE_BYTES, pasteImageFromClipboard, removeLastAttachment, toApiAttachment, } from "./attachments.js";
6
+ import { BTW_DESCRIPTION, BTW_USAGE, btwBrowser, btwPending } from "./btw.js";
6
7
  import { copyText, nodeSpawn } from "./clipboard.js";
7
- import { AttachmentsBar, BannerBlock, ContentInset, clock, contentInnerWidth, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
8
+ import { AttachmentsBar, BannerBlock, BtwPanel, ContentInset, clock, contentInnerWidth, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
8
9
  import { ConnectInputPrompt } from "./connect-input.js";
9
10
  import { bounded, EXIT_PENDING_CAP_MS, EXIT_SESSION_END_CAP_MS } from "./exit.js";
10
11
  import { BRANCH_REFRESH_MS, createBranchCache } from "./git-branch.js";
@@ -1440,6 +1441,57 @@ export async function runTui(options) {
1440
1441
  "**Keys**: Enter send · Shift+Enter / Alt+Enter / Ctrl+J newline · Tab complete · ↑↓ history · Esc interrupt · Ctrl+C clear input (twice to exit) · Ctrl+D exit on empty input · c / y copy last response (empty input) · x expand/collapse latest thought/tool batch/output (empty input) · click a collapsible header row to toggle it · PgUp/PgDn or mouse wheel scroll · Ctrl+X agent panel · Ctrl+B background running agents",
1441
1442
  `**Paste**: multi-line text pastes as one block automatically · Ctrl+V attach a clipboard image (PNG/JPEG/GIF/WebP, up to ${(MAX_IMAGE_BYTES / (1024 * 1024)).toFixed(0)} MB, up to ${MAX_ATTACHMENTS_PER_MESSAGE} per message) · Ctrl+R remove the last attached image`,
1442
1443
  ].join("\n");
1444
+ // `/btw` side questions: answered in the picker slot, never appended to the transcript. One
1445
+ // question is in flight at a time; Esc cancels it (independent of the run's controller, so a
1446
+ // side question also works, and is cancelled on its own, while a turn is running).
1447
+ let btwController;
1448
+ let btwPanel;
1449
+ const btwHeight = () => Math.max(4, Math.floor((process.stdout.rows ?? 30) / 2) - 6);
1450
+ const closeBtw = () => {
1451
+ btwController?.abort(new Error("Cancelled"));
1452
+ btwController = undefined;
1453
+ if (btwPanel && picker === btwPanel)
1454
+ closePicker();
1455
+ btwPanel = undefined;
1456
+ };
1457
+ const sideQuestion = async (question) => {
1458
+ const asked = session;
1459
+ const entries = app.sideQuestions.history(asked);
1460
+ if (!question) {
1461
+ const state = btwBrowser(entries);
1462
+ if (!state)
1463
+ return notice(`${BTW_USAGE}\n${BTW_DESCRIPTION}`);
1464
+ btwPanel = new BtwPanel(state, closeBtw, btwHeight);
1465
+ return showPicker(btwPanel);
1466
+ }
1467
+ btwController?.abort(new Error("Cancelled"));
1468
+ const controller = new AbortController();
1469
+ btwController = controller;
1470
+ const panel = new BtwPanel(btwPending(entries, question), closeBtw, btwHeight);
1471
+ btwPanel = panel;
1472
+ showPicker(panel);
1473
+ try {
1474
+ await app.sideQuestions.ask(asked, question, { signal: controller.signal });
1475
+ if (controller.signal.aborted)
1476
+ return;
1477
+ panel.dispatch({ type: "answered", entries: app.sideQuestions.history(asked) });
1478
+ // An approval may have taken the slot meanwhile: the answer is in the history anyway.
1479
+ if (picker !== panel)
1480
+ flashHint("Side answer ready: /btw to view it", 4000);
1481
+ }
1482
+ catch (e) {
1483
+ if (controller.signal.aborted)
1484
+ return;
1485
+ panel.dispatch({ type: "failed", message: e instanceof Error ? e.message : String(e) });
1486
+ if (picker !== panel)
1487
+ error(e);
1488
+ }
1489
+ finally {
1490
+ if (btwController === controller)
1491
+ btwController = undefined;
1492
+ tui.requestRender();
1493
+ }
1494
+ };
1443
1495
  const commandCatalog = new CommandCatalog(app);
1444
1496
  const mutating = new Set([
1445
1497
  "connect",
@@ -1521,6 +1573,8 @@ export async function runTui(options) {
1521
1573
  tui.flash(message);
1522
1574
  return result.ok ? undefined : notice(message);
1523
1575
  }
1576
+ case "btw":
1577
+ return await sideQuestion(parsed.args);
1524
1578
  case "ask": {
1525
1579
  if (!parsed.args)
1526
1580
  return notice("Usage: /ask <question>");
@@ -1834,6 +1888,7 @@ export async function runTui(options) {
1834
1888
  const ticker = setInterval(() => {
1835
1889
  clock.now = Date.now();
1836
1890
  if (busy ||
1891
+ btwPanel?.pending ||
1837
1892
  view.items.some((i) => i.kind === "tool" && i.status === "running") ||
1838
1893
  panelNodes().some((n) => n.status === "running")) {
1839
1894
  clock.frame++;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pure state of the TUI `/btw` panel: a pending side question, its answer, and browsing earlier
3
+ * answers of the session. The panel lives in the picker slot and never touches the transcript.
4
+ *
5
+ * Keys: ←/→ earlier/later answer, ↑/↓ (and PgUp/PgDn) scroll a long answer, Esc cancels a pending
6
+ * question or closes the panel.
7
+ */
8
+ import type { SideQuestionEntry } from "@alisio/sdk";
9
+ export declare const BTW_USAGE = "Usage: /btw <question>";
10
+ export declare const BTW_DESCRIPTION = "Ask a side question about the current session without adding to the conversation.";
11
+ export interface BtwState {
12
+ entries: SideQuestionEntry[];
13
+ /** Index of the entry on screen (ignored while `pending`). */
14
+ index: number;
15
+ /** A question waiting for its answer. */
16
+ pending?: {
17
+ question: string;
18
+ startedAt: number;
19
+ };
20
+ /** Why the last question failed (shown until the user navigates). */
21
+ error?: {
22
+ question: string;
23
+ message: string;
24
+ };
25
+ /** First answer line on screen. */
26
+ scroll: number;
27
+ }
28
+ export type BtwAction = {
29
+ type: "prev";
30
+ } | {
31
+ type: "next";
32
+ } | {
33
+ type: "scroll";
34
+ lines: number;
35
+ } | {
36
+ type: "answered";
37
+ entries: SideQuestionEntry[];
38
+ } | {
39
+ type: "failed";
40
+ message: string;
41
+ };
42
+ /** The browser for `/btw` without a question: the newest entry, or undefined when none. */
43
+ export declare function btwBrowser(entries: SideQuestionEntry[]): BtwState | undefined;
44
+ /** The panel while `question` is being answered (earlier entries stay browsable afterwards). */
45
+ export declare function btwPending(entries: SideQuestionEntry[], question: string, now?: number): BtwState;
46
+ export declare function reduceBtw(state: BtwState, action: BtwAction): BtwState;
47
+ /** The entry on screen, if the panel is not pending or showing an error. */
48
+ export declare function btwCurrent(state: BtwState): SideQuestionEntry | undefined;
49
+ /** "2/5" for the entry on screen; empty while pending or on an error. */
50
+ export declare function btwPosition(state: BtwState): string;
51
+ /** Footer hints for the current state. */
52
+ export declare function btwHints(state: BtwState, overflow: boolean): string;
53
+ /** "model · 12 in · 3 out tokens" for an entry. */
54
+ export declare function btwUsageLine(entry: SideQuestionEntry): string;
55
+ /** The visible slice of `lines` for `scroll` (clamped) and whether more lines exist. */
56
+ export declare function btwWindow(lines: string[], scroll: number, height: number): {
57
+ lines: string[];
58
+ scroll: number;
59
+ above: boolean;
60
+ below: boolean;
61
+ };
@@ -0,0 +1,84 @@
1
+ export const BTW_USAGE = "Usage: /btw <question>";
2
+ export const BTW_DESCRIPTION = "Ask a side question about the current session without adding to the conversation.";
3
+ /** The browser for `/btw` without a question: the newest entry, or undefined when none. */
4
+ export function btwBrowser(entries) {
5
+ return entries.length ? { entries, index: entries.length - 1, scroll: 0 } : undefined;
6
+ }
7
+ /** The panel while `question` is being answered (earlier entries stay browsable afterwards). */
8
+ export function btwPending(entries, question, now = Date.now()) {
9
+ return {
10
+ entries,
11
+ index: Math.max(0, entries.length - 1),
12
+ pending: { question, startedAt: now },
13
+ scroll: 0,
14
+ };
15
+ }
16
+ export function reduceBtw(state, action) {
17
+ switch (action.type) {
18
+ case "prev":
19
+ case "next": {
20
+ if (state.pending || !state.entries.length)
21
+ return state;
22
+ const step = action.type === "prev" ? -1 : 1;
23
+ // Leaving an error shows the stored entries again (the newest first).
24
+ const from = state.error ? state.entries.length : state.index;
25
+ const index = Math.min(state.entries.length - 1, Math.max(0, from + step));
26
+ if (index === state.index && !state.error)
27
+ return state;
28
+ return { entries: state.entries, index, scroll: 0 };
29
+ }
30
+ case "scroll":
31
+ return { ...state, scroll: Math.max(0, state.scroll + action.lines) };
32
+ case "answered":
33
+ return {
34
+ entries: action.entries,
35
+ index: Math.max(0, action.entries.length - 1),
36
+ scroll: 0,
37
+ };
38
+ case "failed": {
39
+ const { pending: _pending, ...rest } = state;
40
+ return {
41
+ ...rest,
42
+ error: { question: state.pending?.question ?? "", message: action.message },
43
+ scroll: 0,
44
+ };
45
+ }
46
+ }
47
+ }
48
+ /** The entry on screen, if the panel is not pending or showing an error. */
49
+ export function btwCurrent(state) {
50
+ return state.pending || state.error ? undefined : state.entries[state.index];
51
+ }
52
+ /** "2/5" for the entry on screen; empty while pending or on an error. */
53
+ export function btwPosition(state) {
54
+ return btwCurrent(state) ? `${state.index + 1}/${state.entries.length}` : "";
55
+ }
56
+ /** Footer hints for the current state. */
57
+ export function btwHints(state, overflow) {
58
+ if (state.pending)
59
+ return "Esc cancel";
60
+ const current = btwCurrent(state);
61
+ const hints = [
62
+ ...(state.entries.length > 1 || (state.error && state.entries.length)
63
+ ? ["←/→ earlier/later"]
64
+ : []),
65
+ ...(overflow ? ["↑/↓ scroll"] : []),
66
+ "Esc close",
67
+ ];
68
+ return current || state.error ? hints.join(" · ") : "Esc close";
69
+ }
70
+ /** "model · 12 in · 3 out tokens" for an entry. */
71
+ export function btwUsageLine(entry) {
72
+ return `${entry.model} · ${entry.usage.input} in · ${entry.usage.output} out tokens${entry.truncated ? " · truncated" : ""}`;
73
+ }
74
+ /** The visible slice of `lines` for `scroll` (clamped) and whether more lines exist. */
75
+ export function btwWindow(lines, scroll, height) {
76
+ const max = Math.max(0, lines.length - height);
77
+ const start = Math.min(Math.max(0, scroll), max);
78
+ return {
79
+ lines: lines.slice(start, start + height),
80
+ scroll: start,
81
+ above: start > 0,
82
+ below: start + height < lines.length,
83
+ };
84
+ }
@@ -1,6 +1,7 @@
1
1
  import type { PanelNode, TreeNode, UiBlock } from "@alisio/sdk";
2
2
  import { type Component, Container } from "@earendil-works/pi-tui";
3
3
  import { type PendingAttachment } from "./attachments.ts";
4
+ import { type BtwAction, type BtwState } from "./btw.ts";
4
5
  import { type QuestionPanelState, type QuestionSpec } from "./questions.ts";
5
6
  import { type ContextBudget, type FoldableKind, type FoldCandidate, type GroupedTool, type IdentityInput, type TranscriptEntry, type TranscriptItem, type ViewState } from "./state.ts";
6
7
  export declare const SPINNER: string[];
@@ -255,3 +256,21 @@ export declare class QuestionPanel implements Component {
255
256
  handleInput(data: string): void;
256
257
  render(width: number): string[];
257
258
  }
259
+ /**
260
+ * The `/btw` side-question panel (picker slot, never the transcript): a pending question with a
261
+ * spinner, then the answer rendered as Markdown like assistant text, scrollable, with ←/→ through
262
+ * the session's earlier side answers. `onClose` runs on Esc (the app cancels a pending question).
263
+ */
264
+ export declare class BtwPanel implements Component {
265
+ private state;
266
+ private readonly onClose;
267
+ private readonly height;
268
+ private markdown?;
269
+ constructor(state: BtwState, onClose: () => void, height: () => number);
270
+ get pending(): boolean;
271
+ dispatch(action: BtwAction): void;
272
+ invalidate(): void;
273
+ handleInput(data: string): void;
274
+ private answerLines;
275
+ render(width: number): string[];
276
+ }
@@ -1,6 +1,7 @@
1
1
  import { Container, getCapabilities, getImageDimensions, Image, Key, Markdown, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
2
2
  import { terminalCapabilities } from "../banner.js";
3
3
  import { attachmentCaption, MAX_ATTACHMENTS_PER_MESSAGE, } from "./attachments.js";
4
+ import { btwCurrent, btwHints, btwPosition, btwUsageLine, btwWindow, reduceBtw, } from "./btw.js";
4
5
  import { branchDisplay } from "./git-branch.js";
5
6
  import { initialQuestionState, reduceQuestions, } from "./questions.js";
6
7
  import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, } from "./state.js";
@@ -1113,3 +1114,89 @@ export class QuestionPanel {
1113
1114
  return fit(lines, width);
1114
1115
  }
1115
1116
  }
1117
+ /**
1118
+ * The `/btw` side-question panel (picker slot, never the transcript): a pending question with a
1119
+ * spinner, then the answer rendered as Markdown like assistant text, scrollable, with ←/→ through
1120
+ * the session's earlier side answers. `onClose` runs on Esc (the app cancels a pending question).
1121
+ */
1122
+ export class BtwPanel {
1123
+ state;
1124
+ onClose;
1125
+ height;
1126
+ markdown;
1127
+ constructor(state, onClose, height) {
1128
+ this.state = state;
1129
+ this.onClose = onClose;
1130
+ this.height = height;
1131
+ }
1132
+ get pending() {
1133
+ return !!this.state.pending;
1134
+ }
1135
+ dispatch(action) {
1136
+ this.state = reduceBtw(this.state, action);
1137
+ }
1138
+ invalidate() {
1139
+ this.markdown = undefined;
1140
+ }
1141
+ handleInput(data) {
1142
+ if (matchesKey(data, Key.escape))
1143
+ return this.onClose();
1144
+ if (matchesKey(data, Key.left))
1145
+ return this.dispatch({ type: "prev" });
1146
+ if (matchesKey(data, Key.right))
1147
+ return this.dispatch({ type: "next" });
1148
+ if (matchesKey(data, Key.up))
1149
+ return this.dispatch({ type: "scroll", lines: -1 });
1150
+ if (matchesKey(data, Key.down))
1151
+ return this.dispatch({ type: "scroll", lines: 1 });
1152
+ if (matchesKey(data, Key.pageUp))
1153
+ return this.dispatch({ type: "scroll", lines: -this.height() });
1154
+ if (matchesKey(data, Key.pageDown))
1155
+ return this.dispatch({ type: "scroll", lines: this.height() });
1156
+ }
1157
+ answerLines(id, text, width) {
1158
+ if (this.markdown?.id === id && this.markdown.width === width)
1159
+ return this.markdown.lines;
1160
+ const lines = new Markdown(text, 0, 0, markdownTheme, markdownDefaultTextStyle, {
1161
+ transform: markdownTransform,
1162
+ }).render(Math.max(1, width));
1163
+ this.markdown = { id, width, lines };
1164
+ return lines;
1165
+ }
1166
+ render(width) {
1167
+ const s = this.state;
1168
+ const inner = Math.max(1, width - 2);
1169
+ const bar = style.gray("│");
1170
+ const current = btwCurrent(s);
1171
+ const position = btwPosition(s);
1172
+ const lines = [
1173
+ "",
1174
+ `${style.bold(style.yellow("btw"))}${position ? ` ${style.dim(position)}` : ""}${style.dim(" · side question, not added to the conversation")}`,
1175
+ ];
1176
+ const question = s.pending?.question ?? s.error?.question ?? current?.question ?? "";
1177
+ lines.push(...wrap(question, inner).map((l) => `${bar} ${style.bold(l)}`));
1178
+ let overflow = false;
1179
+ if (s.pending) {
1180
+ const spinner = SPINNER[clock.frame % SPINNER.length] ?? "…";
1181
+ lines.push(`${bar} ${style.cyan(spinner)} Thinking… ${style.dim(formatDuration(clock.now - s.pending.startedAt))}`);
1182
+ }
1183
+ else if (s.error) {
1184
+ lines.push(...wrap(s.error.message, inner).map((l) => `${bar} ${style.red(l)}`));
1185
+ }
1186
+ else if (current) {
1187
+ const body = this.answerLines(current.id, current.answer, inner);
1188
+ const view = btwWindow(body, s.scroll, Math.max(3, this.height()));
1189
+ if (view.scroll !== s.scroll)
1190
+ this.state = { ...s, scroll: view.scroll };
1191
+ overflow = view.above || view.below;
1192
+ if (view.above)
1193
+ lines.push(`${bar} ${style.dim("↑ more")}`);
1194
+ lines.push(...view.lines.map((l) => `${bar} ${l}`));
1195
+ if (view.below)
1196
+ lines.push(`${bar} ${style.dim("↓ more")}`);
1197
+ lines.push(`${bar} ${style.dim(btwUsageLine(current))}`);
1198
+ }
1199
+ lines.push(style.dim(` ${btwHints(this.state, overflow)}`));
1200
+ return fit(lines, width);
1201
+ }
1202
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.22",
4
4
  "description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -45,12 +45,12 @@
45
45
  "alisio": "./dist/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@alisio/core": "0.1.0-alpha.18",
48
+ "@alisio/core": "0.1.0-alpha.19",
49
49
  "@alisio/plugin-memory": "0.1.0-alpha.9",
50
50
  "@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
51
51
  "@alisio/plugin-subagents": "0.1.0-alpha.10",
52
- "@alisio/sdk": "0.1.0-alpha.15",
53
- "@alisio/server": "0.1.0-alpha.1",
52
+ "@alisio/sdk": "0.1.0-alpha.16",
53
+ "@alisio/server": "0.1.0-alpha.2",
54
54
  "@earendil-works/pi-tui": "0.87.1",
55
55
  "commander": "15.0.0"
56
56
  },