@alisio/alisio-code 0.1.0-alpha.18 → 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/builtin.d.ts +3 -1
- package/dist/builtin.js +0 -24
- package/dist/main.js +2 -0
- package/dist/tui/app.d.ts +37 -0
- package/dist/tui/app.js +113 -15
- package/dist/tui/components.d.ts +27 -0
- package/dist/tui/components.js +45 -0
- package/dist/tui/settings.d.ts +1 -0
- package/dist/tui/settings.js +10 -1
- package/dist/tui/state.d.ts +37 -2
- package/dist/tui/state.js +58 -5
- package/package.json +3 -6
package/dist/builtin.d.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Built-in plugin registry wired by the CLI. Built-ins load through the trusted path of the
|
|
3
3
|
* plugin host (unprefixed names, `internal` effect) and can be disabled by configuration
|
|
4
4
|
* (`builtinPlugins.<id>.enabled: false`) or `--disable-plugin <id>`. Add a built-in by
|
|
5
|
-
* appending an entry here.
|
|
5
|
+
* appending an entry here. Dedicated model providers (DeepSeek, OpenCode Console, OpenCode Go)
|
|
6
|
+
* are no longer built-ins: users install them independently via
|
|
7
|
+
* `alisio install npm:@alisio/plugin-{deepseek,opencode,opencode-go}`.
|
|
6
8
|
*/
|
|
7
9
|
import type { BuiltinPlugin } from "@alisio/core";
|
|
8
10
|
export declare const BUILTIN_PLUGINS: BuiltinPlugin[];
|
package/dist/builtin.js
CHANGED
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
import { createDeepSeekPlugin } from "@alisio/plugin-deepseek";
|
|
2
1
|
import { createMemoryPlugin } from "@alisio/plugin-memory";
|
|
3
2
|
import { createOpenAICompatiblePlugin } from "@alisio/plugin-openai-compatible";
|
|
4
|
-
import { createOpenCodePlugin } from "@alisio/plugin-opencode";
|
|
5
|
-
import { createOpenCodeGoPlugin } from "@alisio/plugin-opencode-go";
|
|
6
3
|
import { createSubagentsPlugin } from "@alisio/plugin-subagents";
|
|
7
4
|
export const BUILTIN_PLUGINS = [
|
|
8
|
-
{
|
|
9
|
-
id: "deepseek",
|
|
10
|
-
name: "DeepSeek",
|
|
11
|
-
description: "Dedicated DeepSeek Chat Completions and Responses provider",
|
|
12
|
-
categories: ["model-provider"],
|
|
13
|
-
create: createDeepSeekPlugin,
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
id: "opencode",
|
|
17
|
-
name: "OpenCode Console (Zen)",
|
|
18
|
-
description: "Dedicated OpenCode Console (Zen) multi-protocol provider",
|
|
19
|
-
categories: ["model-provider"],
|
|
20
|
-
create: createOpenCodePlugin,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: "opencode-go",
|
|
24
|
-
name: "OpenCode Go",
|
|
25
|
-
description: "Dedicated OpenCode Go multi-protocol provider",
|
|
26
|
-
categories: ["model-provider"],
|
|
27
|
-
create: createOpenCodeGoPlugin,
|
|
28
|
-
},
|
|
29
5
|
{
|
|
30
6
|
id: "openai-compatible",
|
|
31
7
|
name: "OpenAI compatible",
|
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.d.ts
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import type { AppOptions } from "@alisio/core";
|
|
2
|
+
import { type Component, type SelectItem } from "@earendil-works/pi-tui";
|
|
3
|
+
/**
|
|
4
|
+
* Inline selection list with type-to-filter, rendered above the editor.
|
|
5
|
+
*
|
|
6
|
+
* Grouped item builders (pluginCatalogItems) interleave their output with non-selectable
|
|
7
|
+
* `__group:` header items. Headers stay in the SelectList so they render, scroll and truncate
|
|
8
|
+
* like rows, but selection is owned here: navigation jumps over headers via skipGroupHeaders
|
|
9
|
+
* (wrapping both directions), Enter never fires onSelect for a header, and the whole display
|
|
10
|
+
* sequence is re-derived on every filter keystroke so headers of fully-filtered groups vanish.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Picker implements Component {
|
|
13
|
+
private title;
|
|
14
|
+
private readonly onSelectItem;
|
|
15
|
+
private readonly onCancelPick;
|
|
16
|
+
private filterable;
|
|
17
|
+
private detail?;
|
|
18
|
+
private list;
|
|
19
|
+
private filter;
|
|
20
|
+
/** Full item sequence (group headers + rows) before any filtering. */
|
|
21
|
+
private readonly allItems;
|
|
22
|
+
/** Display sequence: filtered rows plus the headers of groups that still match. */
|
|
23
|
+
private items;
|
|
24
|
+
/** Selection index within `items`; always a real row, never a group header. */
|
|
25
|
+
private index;
|
|
26
|
+
/** Picker height target: counted on real rows only, so headers never stretch the list. */
|
|
27
|
+
private readonly maxVisible;
|
|
28
|
+
constructor(title: string, items: SelectItem[], onSelectItem: (item: SelectItem) => void, onCancelPick: () => void, filterable?: boolean, detail?: string | undefined);
|
|
29
|
+
/** Copies items, dimming group-header labels (style helpers stay plain under NO_COLOR). */
|
|
30
|
+
private static displayItems;
|
|
31
|
+
/** (Re)builds the SelectList over the filtered display sequence; selection starts on a row. */
|
|
32
|
+
private resetList;
|
|
33
|
+
invalidate(): void;
|
|
34
|
+
/** The currently selected item — always a real row, never a group header. */
|
|
35
|
+
getSelectedItem(): SelectItem | null;
|
|
36
|
+
handleInput(data: string): void;
|
|
37
|
+
render(width: number): string[];
|
|
38
|
+
}
|
|
2
39
|
export interface TuiOptions extends AppOptions {
|
|
3
40
|
session?: string;
|
|
4
41
|
quiet?: boolean;
|
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";
|
|
@@ -14,38 +14,94 @@ import { InteractiveQueue } from "./queue.js";
|
|
|
14
14
|
import { settingsMenuRows } from "./settings.js";
|
|
15
15
|
import { SettingsMenu } from "./settings-menu.js";
|
|
16
16
|
import { SkillsManager } from "./skills-manager.js";
|
|
17
|
-
import { addItem, COMMANDS, configuredProviderModelItems, editorCopyKey, effectiveEffort, effortPickerItems, foldCandidates, foldToggleKey, formatContext, formatDuration, formatTokens, hostOf, initialViewState, itemsFromHistory, lastAssistantText, mcpServerItems, mcpToolItems, parseCommand, pluginCatalogItems, pluginToggleNeedsConfirmation, providerModelItems, reduceEvent, reservedCommandNames, resolveCommand, richPartsOf, shortenPath, shortId, slashCompletionCommands, summarizeToolArgs, validateEffortLevel, } from "./state.js";
|
|
17
|
+
import { addItem, COMMANDS, configuredProviderModelItems, editorCopyKey, effectiveEffort, effortPickerItems, foldCandidates, foldToggleKey, formatContext, formatDuration, formatTokens, hostOf, initialViewState, isGroupHeader, itemsFromHistory, lastAssistantText, mcpServerItems, mcpToolItems, parseCommand, pluginCatalogItems, pluginToggleNeedsConfirmation, providerModelItems, reduceEvent, reservedCommandNames, resolveCommand, richPartsOf, shortenPath, shortId, skipGroupHeaders, slashCompletionCommands, summarizeToolArgs, validateEffortLevel, visibleGroupedItems, } from "./state.js";
|
|
18
18
|
import { editorTheme, selectListTheme, style } from "./theme.js";
|
|
19
19
|
const VERSION = loadVersion(import.meta.url);
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Inline selection list with type-to-filter, rendered above the editor.
|
|
22
|
+
*
|
|
23
|
+
* Grouped item builders (pluginCatalogItems) interleave their output with non-selectable
|
|
24
|
+
* `__group:` header items. Headers stay in the SelectList so they render, scroll and truncate
|
|
25
|
+
* like rows, but selection is owned here: navigation jumps over headers via skipGroupHeaders
|
|
26
|
+
* (wrapping both directions), Enter never fires onSelect for a header, and the whole display
|
|
27
|
+
* sequence is re-derived on every filter keystroke so headers of fully-filtered groups vanish.
|
|
28
|
+
*/
|
|
29
|
+
export class Picker {
|
|
22
30
|
title;
|
|
31
|
+
onSelectItem;
|
|
32
|
+
onCancelPick;
|
|
23
33
|
filterable;
|
|
24
34
|
detail;
|
|
25
|
-
list;
|
|
35
|
+
list = new SelectList([], 1, selectListTheme);
|
|
26
36
|
filter = "";
|
|
27
|
-
|
|
37
|
+
/** Full item sequence (group headers + rows) before any filtering. */
|
|
38
|
+
allItems;
|
|
39
|
+
/** Display sequence: filtered rows plus the headers of groups that still match. */
|
|
40
|
+
items = [];
|
|
41
|
+
/** Selection index within `items`; always a real row, never a group header. */
|
|
42
|
+
index = 0;
|
|
43
|
+
/** Picker height target: counted on real rows only, so headers never stretch the list. */
|
|
44
|
+
maxVisible;
|
|
45
|
+
constructor(title, items, onSelectItem, onCancelPick, filterable = false, detail) {
|
|
28
46
|
this.title = title;
|
|
47
|
+
this.onSelectItem = onSelectItem;
|
|
48
|
+
this.onCancelPick = onCancelPick;
|
|
29
49
|
this.filterable = filterable;
|
|
30
50
|
this.detail = detail;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
51
|
+
this.allItems = items;
|
|
52
|
+
this.maxVisible = Math.min(10, Math.max(1, items.filter((item) => !isGroupHeader(item)).length));
|
|
53
|
+
this.resetList();
|
|
54
|
+
}
|
|
55
|
+
/** Copies items, dimming group-header labels (style helpers stay plain under NO_COLOR). */
|
|
56
|
+
static displayItems(items) {
|
|
57
|
+
return items.map((item) => isGroupHeader(item) ? { ...item, label: style.gray(item.label) } : item);
|
|
58
|
+
}
|
|
59
|
+
/** (Re)builds the SelectList over the filtered display sequence; selection starts on a row. */
|
|
60
|
+
resetList() {
|
|
61
|
+
this.items = Picker.displayItems(this.filter ? visibleGroupedItems(this.allItems, this.filter) : this.allItems);
|
|
62
|
+
this.list = new SelectList(this.items, this.maxVisible, selectListTheme);
|
|
63
|
+
this.list.onSelect = (item) => {
|
|
64
|
+
if (!isGroupHeader(item))
|
|
65
|
+
this.onSelectItem(item);
|
|
66
|
+
};
|
|
67
|
+
this.list.onCancel = this.onCancelPick;
|
|
68
|
+
this.index = this.items.length ? skipGroupHeaders(this.items, -1, 1) : 0;
|
|
69
|
+
if (this.index < 0)
|
|
70
|
+
this.index = 0;
|
|
71
|
+
this.list.setSelectedIndex(this.index);
|
|
34
72
|
}
|
|
35
73
|
invalidate() {
|
|
36
74
|
this.list.invalidate();
|
|
37
75
|
}
|
|
76
|
+
/** The currently selected item — always a real row, never a group header. */
|
|
77
|
+
getSelectedItem() {
|
|
78
|
+
return this.items[this.index] ?? null;
|
|
79
|
+
}
|
|
38
80
|
handleInput(data) {
|
|
39
81
|
if (this.filterable && matchesKey(data, Key.backspace)) {
|
|
40
82
|
this.filter = this.filter.slice(0, -1);
|
|
41
|
-
this.
|
|
83
|
+
this.resetList();
|
|
42
84
|
}
|
|
43
85
|
else if (this.filterable && data.length === 1 && data >= " " && data <= "~") {
|
|
44
86
|
this.filter += data;
|
|
45
|
-
this.
|
|
87
|
+
this.resetList();
|
|
46
88
|
}
|
|
47
|
-
else
|
|
89
|
+
else if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
|
|
90
|
+
const dir = matchesKey(data, Key.down) ? 1 : -1;
|
|
91
|
+
this.index = this.items.length ? skipGroupHeaders(this.items, this.index, dir) : 0;
|
|
92
|
+
this.list.setSelectedIndex(this.index);
|
|
93
|
+
}
|
|
94
|
+
else if (matchesKey(data, Key.enter)) {
|
|
95
|
+
const item = this.items[this.index];
|
|
96
|
+
if (item && !isGroupHeader(item))
|
|
97
|
+
this.onSelectItem(item);
|
|
98
|
+
}
|
|
99
|
+
else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
100
|
+
this.onCancelPick();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
48
103
|
this.list.handleInput(data);
|
|
104
|
+
}
|
|
49
105
|
}
|
|
50
106
|
render(width) {
|
|
51
107
|
const hint = this.filterable
|
|
@@ -85,6 +141,7 @@ export async function runTui(options) {
|
|
|
85
141
|
const { BUILTIN_PROMPTS } = await import("../prompts/index.js");
|
|
86
142
|
let dispatch = () => { };
|
|
87
143
|
let approve = async () => "deny";
|
|
144
|
+
let approveExternalDirectory = async () => "deny";
|
|
88
145
|
const app = await createApplication({
|
|
89
146
|
builtins: BUILTIN_PLUGINS,
|
|
90
147
|
builtinPrompts: BUILTIN_PROMPTS,
|
|
@@ -92,6 +149,7 @@ export async function runTui(options) {
|
|
|
92
149
|
...options,
|
|
93
150
|
onEvent: (event) => dispatch(event),
|
|
94
151
|
approve: (request) => approve(request),
|
|
152
|
+
approveExternalDirectory: (request) => approveExternalDirectory(request),
|
|
95
153
|
});
|
|
96
154
|
let activeProvider = app.providerInfo;
|
|
97
155
|
let session = options.session ?? app.store.create(app.workspace, app.provider.id, app.provider.model).id;
|
|
@@ -334,14 +392,21 @@ export async function runTui(options) {
|
|
|
334
392
|
// working (copyOnSelect must never be disturbed). The region sits between the ScrollView and
|
|
335
393
|
// the content switch; content rows = visible rows + the scroll offset.
|
|
336
394
|
let transcriptScroll;
|
|
337
|
-
|
|
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) => {
|
|
338
401
|
if (picker || event.type !== "click" || event.button !== "left")
|
|
339
402
|
return undefined;
|
|
340
403
|
const scroll = transcriptScroll;
|
|
341
404
|
if (!scroll)
|
|
342
405
|
return undefined;
|
|
343
406
|
const visible = visibleTranscript();
|
|
344
|
-
|
|
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()));
|
|
345
410
|
if (!candidate)
|
|
346
411
|
return undefined;
|
|
347
412
|
toggleFold(candidate);
|
|
@@ -493,6 +558,36 @@ export async function runTui(options) {
|
|
|
493
558
|
], (item) => finish(item.value), () => finish("deny")));
|
|
494
559
|
}),
|
|
495
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
|
+
});
|
|
496
591
|
/**
|
|
497
592
|
* `ask_user_question` / `/ask`: routed through the same `interactiveQueue` as `approve`/`select`,
|
|
498
593
|
* so a root or subagent question never races another prompt for the screen. A question withdrawn
|
|
@@ -1213,6 +1308,9 @@ export async function runTui(options) {
|
|
|
1213
1308
|
.then(() => {
|
|
1214
1309
|
if (row.id === "tui.paddingX")
|
|
1215
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();
|
|
1216
1314
|
// Rebuild the slash provider: the toggle gates the skill:<id> entries immediately.
|
|
1217
1315
|
if (row.id === "tui.skillSlashCommands")
|
|
1218
1316
|
editor.setAutocompleteProvider(buildSlashCompletionProvider());
|
|
@@ -1297,7 +1395,7 @@ export async function runTui(options) {
|
|
|
1297
1395
|
return `| \`${t.name}\` | ${effect} | ${state} |`;
|
|
1298
1396
|
})
|
|
1299
1397
|
.join("\n");
|
|
1300
|
-
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.`;
|
|
1301
1399
|
};
|
|
1302
1400
|
const statsReport = () => {
|
|
1303
1401
|
const s = view.stats;
|
package/dist/tui/components.d.ts
CHANGED
|
@@ -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
|
package/dist/tui/components.js
CHANGED
|
@@ -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);
|
package/dist/tui/settings.d.ts
CHANGED
package/dist/tui/settings.js
CHANGED
|
@@ -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/dist/tui/state.d.ts
CHANGED
|
@@ -39,11 +39,46 @@ export interface PluginCatalogView {
|
|
|
39
39
|
}
|
|
40
40
|
/** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
|
|
41
41
|
/** Group headings are derived from the primary category (or "General") of each plugin. */
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Reserved value prefix marking a non-selectable group-header row in picker item lists. It can
|
|
44
|
+
* never collide with real entry ids (plugin ids, server names), which never start with "_".
|
|
45
|
+
*/
|
|
46
|
+
export declare const GROUP_HEADER_PREFIX = "__group:";
|
|
47
|
+
/** True when the item is a group header emitted under GROUP_HEADER_PREFIX (never selectable). */
|
|
48
|
+
export declare const isGroupHeader: (item: {
|
|
49
|
+
value: string;
|
|
50
|
+
}) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Nearest non-header index from `index` stepping `dir` (1 = down, -1 = up), wrapping at the
|
|
53
|
+
* edges exactly like a plain list would, so navigation skips header rows in both directions and
|
|
54
|
+
* never stops on one. Returns -1 for an empty list and `index` unchanged when every item is a
|
|
55
|
+
* header (callers then treat it as "no move").
|
|
56
|
+
*/
|
|
57
|
+
export declare function skipGroupHeaders(items: readonly {
|
|
58
|
+
value: string;
|
|
59
|
+
}[], index: number, dir: 1 | -1): number;
|
|
60
|
+
/**
|
|
61
|
+
* The display sequence a picker should render for a filter: rows matching SelectList's own
|
|
62
|
+
* prefix semantics, plus the group header of every group that still has at least one visible
|
|
63
|
+
* row, in original order. Groups with no match emit no header.
|
|
64
|
+
*/
|
|
65
|
+
export declare function visibleGroupedItems(items: ReadonlyArray<{
|
|
66
|
+
value: string;
|
|
67
|
+
label: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
}>, filter: string): Array<{
|
|
70
|
+
value: string;
|
|
71
|
+
label: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
}>;
|
|
74
|
+
export declare function pluginCatalogItems(entries: PluginCatalogView[]): ({
|
|
75
|
+
value: string;
|
|
76
|
+
label: string;
|
|
77
|
+
} | {
|
|
43
78
|
value: string;
|
|
44
79
|
label: string;
|
|
45
80
|
description: string;
|
|
46
|
-
}[];
|
|
81
|
+
})[];
|
|
47
82
|
export declare const pluginToggleNeedsConfirmation: (entry: PluginCatalogView) => boolean;
|
|
48
83
|
export interface McpServerView {
|
|
49
84
|
name: string;
|
package/dist/tui/state.js
CHANGED
|
@@ -24,6 +24,55 @@ export function providerModelItems(provider, models, current) {
|
|
|
24
24
|
}
|
|
25
25
|
/** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
|
|
26
26
|
/** Group headings are derived from the primary category (or "General") of each plugin. */
|
|
27
|
+
/**
|
|
28
|
+
* Reserved value prefix marking a non-selectable group-header row in picker item lists. It can
|
|
29
|
+
* never collide with real entry ids (plugin ids, server names), which never start with "_".
|
|
30
|
+
*/
|
|
31
|
+
export const GROUP_HEADER_PREFIX = "__group:";
|
|
32
|
+
/** True when the item is a group header emitted under GROUP_HEADER_PREFIX (never selectable). */
|
|
33
|
+
export const isGroupHeader = (item) => item.value.startsWith(GROUP_HEADER_PREFIX);
|
|
34
|
+
/**
|
|
35
|
+
* Nearest non-header index from `index` stepping `dir` (1 = down, -1 = up), wrapping at the
|
|
36
|
+
* edges exactly like a plain list would, so navigation skips header rows in both directions and
|
|
37
|
+
* never stops on one. Returns -1 for an empty list and `index` unchanged when every item is a
|
|
38
|
+
* header (callers then treat it as "no move").
|
|
39
|
+
*/
|
|
40
|
+
export function skipGroupHeaders(items, index, dir) {
|
|
41
|
+
const count = items.length;
|
|
42
|
+
if (!count)
|
|
43
|
+
return -1;
|
|
44
|
+
let next = (index + dir + count) % count;
|
|
45
|
+
for (let steps = 0; steps < count && next !== index; steps++) {
|
|
46
|
+
if (!isGroupHeader(items[next]))
|
|
47
|
+
return next;
|
|
48
|
+
next = (next + dir + count) % count;
|
|
49
|
+
}
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The display sequence a picker should render for a filter: rows matching SelectList's own
|
|
54
|
+
* prefix semantics, plus the group header of every group that still has at least one visible
|
|
55
|
+
* row, in original order. Groups with no match emit no header.
|
|
56
|
+
*/
|
|
57
|
+
export function visibleGroupedItems(items, filter) {
|
|
58
|
+
const query = filter.toLowerCase();
|
|
59
|
+
const visible = [];
|
|
60
|
+
let header;
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
if (isGroupHeader(item)) {
|
|
63
|
+
header = item;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (item.value.toLowerCase().startsWith(query)) {
|
|
67
|
+
if (header) {
|
|
68
|
+
visible.push(header);
|
|
69
|
+
header = undefined;
|
|
70
|
+
}
|
|
71
|
+
visible.push(item);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return visible;
|
|
75
|
+
}
|
|
27
76
|
export function pluginCatalogItems(entries) {
|
|
28
77
|
const marker = (entry) => entry.status === "active"
|
|
29
78
|
? "[x]"
|
|
@@ -37,11 +86,15 @@ export function pluginCatalogItems(entries) {
|
|
|
37
86
|
const primary = entry.categories[0] ?? "General";
|
|
38
87
|
grouped.set(primary, [...(grouped.get(primary) ?? []), entry]);
|
|
39
88
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
// Every group opens with its own non-selectable header row; plugin rows are indented under it.
|
|
90
|
+
return [...grouped].flatMap(([title, group]) => [
|
|
91
|
+
{ value: `${GROUP_HEADER_PREFIX}${title}`, label: `${title} ›` },
|
|
92
|
+
...group.map((entry) => ({
|
|
93
|
+
value: entry.id,
|
|
94
|
+
label: ` ${marker(entry)} ${entry.name} · ${entry.builtin ? "built-in" : entry.source}`,
|
|
95
|
+
description: `${entry.status}${entry.categories.length ? ` · ${entry.categories.join(", ")}` : ""} · ${entry.description}`,
|
|
96
|
+
})),
|
|
97
|
+
]);
|
|
45
98
|
}
|
|
46
99
|
export const pluginToggleNeedsConfirmation = (entry) => !entry.builtin;
|
|
47
100
|
const mcpSourceTitle = (kind) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/alisio-code",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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,14 +45,11 @@
|
|
|
45
45
|
"alisio": "./dist/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/core": "0.1.0-alpha.
|
|
49
|
-
"@alisio/plugin-deepseek": "0.1.0-alpha.10",
|
|
48
|
+
"@alisio/core": "0.1.0-alpha.17",
|
|
50
49
|
"@alisio/plugin-memory": "0.1.0-alpha.9",
|
|
51
50
|
"@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
|
|
52
|
-
"@alisio/plugin-opencode": "0.1.0-alpha.8",
|
|
53
|
-
"@alisio/plugin-opencode-go": "0.1.0-alpha.8",
|
|
54
51
|
"@alisio/plugin-subagents": "0.1.0-alpha.10",
|
|
55
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
52
|
+
"@alisio/sdk": "0.1.0-alpha.12",
|
|
56
53
|
"@earendil-works/pi-tui": "0.87.1",
|
|
57
54
|
"commander": "15.0.0"
|
|
58
55
|
},
|