@alisio/alisio-code 0.1.0-alpha.19 → 0.1.0-alpha.20

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
@@ -38,6 +38,7 @@ program
38
38
  .option("--allow-external", "Allow network tools: webfetch, websearch and provider-native search")
39
39
  .option("--allow-mcp", "Allow configured MCP servers and remote tool calls")
40
40
  .option("--allow-agents", "Allow messaging neighboring agents through Herdr")
41
+ .option("--add-dir <paths...>", "Additional directories tools may read/write outside the workspace (repeatable)")
41
42
  .option("--no-herdr", "Disable automatic Herdr lifecycle reports")
42
43
  .option("--read-only", "Disable writes, arbitrary processes, network tools, executable plugins and MCP")
43
44
  .option("--db <path>", "Session database")
@@ -54,6 +55,7 @@ const options = (cmd) => {
54
55
  baseURL: o.baseUrl,
55
56
  noHerdr: o.herdr === false,
56
57
  disablePlugins: o.disablePlugin,
58
+ addDirs: o.addDir,
57
59
  ...(o.agents
58
60
  ? { pluginOptions: { subagents: { agents: parseAgents(String(o.agents)) } } }
59
61
  : {}),
package/dist/tui/app.js CHANGED
@@ -4,7 +4,7 @@ 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
6
  import { copyText, nodeSpawn } from "./clipboard.js";
7
- import { AttachmentsBar, BannerBlock, clock, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
7
+ import { AttachmentsBar, BannerBlock, ContentInset, clock, contentInnerWidth, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
8
8
  import { ConnectInputPrompt } from "./connect-input.js";
9
9
  import { bounded, EXIT_PENDING_CAP_MS, EXIT_SESSION_END_CAP_MS } from "./exit.js";
10
10
  import { BRANCH_REFRESH_MS, createBranchCache } from "./git-branch.js";
@@ -141,6 +141,7 @@ export async function runTui(options) {
141
141
  const { BUILTIN_PROMPTS } = await import("../prompts/index.js");
142
142
  let dispatch = () => { };
143
143
  let approve = async () => "deny";
144
+ let approveExternalDirectory = async () => "deny";
144
145
  const app = await createApplication({
145
146
  builtins: BUILTIN_PLUGINS,
146
147
  builtinPrompts: BUILTIN_PROMPTS,
@@ -148,6 +149,7 @@ export async function runTui(options) {
148
149
  ...options,
149
150
  onEvent: (event) => dispatch(event),
150
151
  approve: (request) => approve(request),
152
+ approveExternalDirectory: (request) => approveExternalDirectory(request),
151
153
  });
152
154
  let activeProvider = app.providerInfo;
153
155
  let session = options.session ?? app.store.create(app.workspace, app.provider.id, app.provider.model).id;
@@ -390,14 +392,21 @@ export async function runTui(options) {
390
392
  // working (copyOnSelect must never be disturbed). The region sits between the ScrollView and
391
393
  // the content switch; content rows = visible rows + the scroll offset.
392
394
  let transcriptScroll;
393
- const transcriptRegion = new MouseRegion(new Switch(() => (viewedState() ? childView.container : transcript)), (event) => {
395
+ // Transcript content is inset from both borders in ONE place: the wrapper hands children a
396
+ // reduced width and prefixes every row, so agent text, copy hints, tool sub-lines, reasoning
397
+ // lines and group headers all share the same visual column. The config is read live, so a
398
+ // `/settings` change applies on the next render without rebuilding the view.
399
+ const contentInsetX = () => app.config.tui.contentPaddingX;
400
+ const transcriptRegion = new MouseRegion(new ContentInset(new Switch(() => (viewedState() ? childView.container : transcript)), contentInsetX), (event) => {
394
401
  if (picker || event.type !== "click" || event.button !== "left")
395
402
  return undefined;
396
403
  const scroll = transcriptScroll;
397
404
  if (!scroll)
398
405
  return undefined;
399
406
  const visible = visibleTranscript();
400
- const candidate = visible.sync.entryAt(event.y + scroll.scrollTop, event.width);
407
+ // entryAt re-renders each row to measure its height, so it must use the same reduced width
408
+ // the wrapper hands the rows during render.
409
+ const candidate = visible.sync.entryAt(event.y + scroll.scrollTop, contentInnerWidth(event.width, contentInsetX()));
401
410
  if (!candidate)
402
411
  return undefined;
403
412
  toggleFold(candidate);
@@ -549,6 +558,36 @@ export async function runTui(options) {
549
558
  ], (item) => finish(item.value), () => finish("deny")));
550
559
  }),
551
560
  });
561
+ /**
562
+ * External-directory approval: a tool path outside the workspace and every declared extra root
563
+ * asks here before the call runs, scoped to the containing directory (not the individual file),
564
+ * so one approval covers that directory's subtree for the session. Same queue and the same
565
+ * allow-once / allow-session / deny choices as the capability approval above.
566
+ */
567
+ approveExternalDirectory = (request) => interactiveQueue.submit({
568
+ sessionId: request.session,
569
+ label: request.label,
570
+ signal: request.signal,
571
+ onWithdrawn: () => "deny",
572
+ run: () => new Promise((resolve) => {
573
+ let settled = false;
574
+ const finish = (decision) => {
575
+ if (settled)
576
+ return;
577
+ settled = true;
578
+ request.signal.removeEventListener("abort", onAbort);
579
+ closePicker();
580
+ resolve(decision);
581
+ };
582
+ const onAbort = () => finish("deny");
583
+ request.signal.addEventListener("abort", onAbort, { once: true });
584
+ showPicker(new Picker(`${request.label ? `[${request.label}] ` : ""}Allow access outside the workspace to ${shortenPath(request.directory, homedir(), 72)}?`, [
585
+ { value: "once", label: "Allow once" },
586
+ { value: "session", label: "Always allow this directory in this session" },
587
+ { value: "deny", label: "Deny" },
588
+ ], (item) => finish(item.value), () => finish("deny")));
589
+ }),
590
+ });
552
591
  /**
553
592
  * `ask_user_question` / `/ask`: routed through the same `interactiveQueue` as `approve`/`select`,
554
593
  * so a root or subagent question never races another prompt for the screen. A question withdrawn
@@ -1269,6 +1308,9 @@ export async function runTui(options) {
1269
1308
  .then(() => {
1270
1309
  if (row.id === "tui.paddingX")
1271
1310
  editor.setPaddingX(Number(value));
1311
+ // The inset wrapper reads the live config; repaint so the new value shows immediately.
1312
+ if (row.id === "tui.contentPaddingX")
1313
+ tui.requestRender();
1272
1314
  // Rebuild the slash provider: the toggle gates the skill:<id> entries immediately.
1273
1315
  if (row.id === "tui.skillSlashCommands")
1274
1316
  editor.setAutocompleteProvider(buildSlashCompletionProvider());
@@ -1353,7 +1395,7 @@ export async function runTui(options) {
1353
1395
  return `| \`${t.name}\` | ${effect} | ${state} |`;
1354
1396
  })
1355
1397
  .join("\n");
1356
- return `**Tools**\n\n| Tool | Effect | State |\n| --- | --- | --- |\n${rows}\n\n\`ask\` prompts before running (allow once / session / deny). Use --allow-write / --allow-process to pre-allow; --read-only disables them. \`internal\` tools (built-in plugins) only write Alisio's own state.`;
1398
+ return `**Tools**\n\n| Tool | Effect | State |\n| --- | --- | --- |\n${rows}\n\n\`ask\` prompts before running (allow once / session / deny). Use --allow-write / --allow-process to pre-allow; --read-only disables them. Paths outside the workspace ask per directory; pre-allow with --add-dir or the \`additionalDirectories\` config key. \`internal\` tools (built-in plugins) only write Alisio's own state.`;
1357
1399
  };
1358
1400
  const statsReport = () => {
1359
1401
  const s = view.stats;
@@ -129,6 +129,33 @@ export declare class InfoBlock implements Component {
129
129
  invalidate(): void;
130
130
  render(width: number): string[];
131
131
  }
132
+ /**
133
+ * Minimum usable inner width (cells) the transcript keeps no matter how narrow the terminal is or
134
+ * how large the configured inset grows. The effective inset is clamped so this floor is honored,
135
+ * which also keeps the inner width positive.
136
+ */
137
+ export declare const MIN_CONTENT_WIDTH = 20;
138
+ /**
139
+ * Inset actually applied on each side: the configured value capped so the inner column stays at
140
+ * least `MIN_CONTENT_WIDTH` wide. On terminals narrower than that floor the inset degrades to 0
141
+ * rather than overflowing (the inner width then simply equals the terminal width).
142
+ */
143
+ export declare function effectiveContentInset(width: number, inset: number): number;
144
+ /** Inner width handed to transcript children after the effective inset is applied. Always >= 1. */
145
+ export declare function contentInnerWidth(width: number, inset: number): number;
146
+ /**
147
+ * Applies the transcript's horizontal inset in ONE place: it hands children a reduced width and
148
+ * prefixes every produced line with the inset, so all transcript chrome (text, copy hint, tool
149
+ * sub-lines, reasoning lines, group headers) shares the same visual column. The reduced width is a
150
+ * deterministic function of the terminal width, so per-row caches keyed by width stay effective.
151
+ */
152
+ export declare class ContentInset implements Component {
153
+ private readonly inner;
154
+ private readonly inset;
155
+ constructor(inner: Component, inset: () => number);
156
+ invalidate(): void;
157
+ render(width: number): string[];
158
+ }
132
159
  export declare function componentFor(entry: TranscriptEntry): Component;
133
160
  /**
134
161
  * Keeps a container of transcript components in step with view-model items. Rows are keyed
@@ -588,6 +588,51 @@ export class InfoBlock {
588
588
  return fit(["", ...inner.map((l) => `${style.gray("│")} ${l}`)], width);
589
589
  }
590
590
  }
591
+ /**
592
+ * Minimum usable inner width (cells) the transcript keeps no matter how narrow the terminal is or
593
+ * how large the configured inset grows. The effective inset is clamped so this floor is honored,
594
+ * which also keeps the inner width positive.
595
+ */
596
+ export const MIN_CONTENT_WIDTH = 20;
597
+ /**
598
+ * Inset actually applied on each side: the configured value capped so the inner column stays at
599
+ * least `MIN_CONTENT_WIDTH` wide. On terminals narrower than that floor the inset degrades to 0
600
+ * rather than overflowing (the inner width then simply equals the terminal width).
601
+ */
602
+ export function effectiveContentInset(width, inset) {
603
+ const requested = Math.max(0, Math.floor(inset));
604
+ const max = Math.max(0, Math.floor((Math.floor(width) - MIN_CONTENT_WIDTH) / 2));
605
+ return Math.min(requested, max);
606
+ }
607
+ /** Inner width handed to transcript children after the effective inset is applied. Always >= 1. */
608
+ export function contentInnerWidth(width, inset) {
609
+ return Math.max(1, Math.floor(width) - effectiveContentInset(width, inset) * 2);
610
+ }
611
+ /**
612
+ * Applies the transcript's horizontal inset in ONE place: it hands children a reduced width and
613
+ * prefixes every produced line with the inset, so all transcript chrome (text, copy hint, tool
614
+ * sub-lines, reasoning lines, group headers) shares the same visual column. The reduced width is a
615
+ * deterministic function of the terminal width, so per-row caches keyed by width stay effective.
616
+ */
617
+ export class ContentInset {
618
+ inner;
619
+ inset;
620
+ constructor(inner, inset) {
621
+ this.inner = inner;
622
+ this.inset = inset;
623
+ }
624
+ invalidate() {
625
+ this.inner.invalidate();
626
+ }
627
+ render(width) {
628
+ const inset = effectiveContentInset(width, this.inset());
629
+ const lines = this.inner.render(contentInnerWidth(width, inset));
630
+ if (!inset)
631
+ return lines;
632
+ const pad = " ".repeat(inset);
633
+ return lines.map((line) => `${pad}${line}`);
634
+ }
635
+ }
591
636
  export function componentFor(entry) {
592
637
  if (entry.kind === "group")
593
638
  return new ToolGroupBlock(entry.group);
@@ -56,6 +56,7 @@ export interface SettingsConfigView {
56
56
  };
57
57
  tui: {
58
58
  paddingX: number;
59
+ contentPaddingX: number;
59
60
  skillSlashCommands: boolean;
60
61
  };
61
62
  mcp: {
@@ -13,7 +13,7 @@ export const defaultConfig = {
13
13
  compaction: { auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 },
14
14
  context: { claudeMdFallback: false, maxBytes: 32 * 1024 },
15
15
  limits: { maxTurns: 100, maxOutputTokens: 16_384, maxContextChars: 800_000, timeoutMs: 300_000 },
16
- tui: { paddingX: 1, skillSlashCommands: true },
16
+ tui: { paddingX: 1, contentPaddingX: 2, skillSlashCommands: true },
17
17
  mcp: { allow: false },
18
18
  websearch: { provider: undefined },
19
19
  pluginHooks: { timeoutMs: 15_000 },
@@ -161,6 +161,15 @@ export const SETTINGS_DEFINITIONS = [
161
161
  read: (config) => config.tui.paddingX,
162
162
  description: "Horizontal padding (columns) around the editor input box. Applied immediately to the current editor.",
163
163
  },
164
+ {
165
+ id: "tui.contentPaddingX",
166
+ label: "Content inset",
167
+ category: "TUI",
168
+ valueType: "number",
169
+ values: [0, 1, 2, 3, 4, 6, 8, 12],
170
+ read: (config) => config.tui.contentPaddingX,
171
+ description: "Horizontal inset (columns) on each side of the transcript content, keeping agent-written rows clear of the window borders. Clamped on narrow terminals so the content column stays usable. Applied immediately.",
172
+ },
164
173
  {
165
174
  id: "tui.skillSlashCommands",
166
175
  label: "Skill slash commands",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.19",
3
+ "version": "0.1.0-alpha.20",
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,11 +45,11 @@
45
45
  "alisio": "./dist/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@alisio/core": "0.1.0-alpha.16",
48
+ "@alisio/core": "0.1.0-alpha.17",
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.11",
52
+ "@alisio/sdk": "0.1.0-alpha.12",
53
53
  "@earendil-works/pi-tui": "0.87.1",
54
54
  "commander": "15.0.0"
55
55
  },