@alisio/alisio-code 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +21 -2
- package/dist/tui/agents.d.ts +66 -0
- package/dist/tui/agents.js +113 -0
- package/dist/tui/app.js +260 -15
- package/dist/tui/components.d.ts +77 -12
- package/dist/tui/components.js +490 -63
- package/dist/tui/state.d.ts +204 -1
- package/dist/tui/state.js +501 -12
- package/dist/tui/theme.d.ts +42 -1
- package/dist/tui/theme.js +516 -1
- package/package.json +6 -6
package/dist/main.js
CHANGED
|
@@ -129,6 +129,19 @@ async function run(cmd, prompt, sessionId) {
|
|
|
129
129
|
});
|
|
130
130
|
for (const failure of app.mcpStartupFailures())
|
|
131
131
|
process.stderr.write(`[startup] ${failure}\n`);
|
|
132
|
+
// The ACTIVE agent (built-in `build`/`plan` or a main-capable definition) drives the main
|
|
133
|
+
// session here too: its system prompt is appended and a read-only agent narrows the run. The
|
|
134
|
+
// TUI adds the picker and the model switch; effort is a TUI feature (validated against the
|
|
135
|
+
// active model's catalog).
|
|
136
|
+
const { agentCatalogFromState, agentRunOptions, resolveActiveAgent } = await import("./tui/agents.js");
|
|
137
|
+
let activeRunOptions = {};
|
|
138
|
+
try {
|
|
139
|
+
const reported = app.plugins.pluginState("subagents", "mainAgents");
|
|
140
|
+
activeRunOptions = agentRunOptions(resolveActiveAgent(agentCatalogFromState(reported), app.config.agents.active));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* agent resolution is best-effort */
|
|
144
|
+
}
|
|
132
145
|
const controller = new AbortController();
|
|
133
146
|
const interrupt = () => controller.abort(new Error("Interrupted"));
|
|
134
147
|
process.on("SIGINT", interrupt);
|
|
@@ -151,7 +164,10 @@ async function run(cmd, prompt, sessionId) {
|
|
|
151
164
|
const match = /^\/skill:([a-z0-9-]+)\s*([\s\S]*)$/.exec(prompt);
|
|
152
165
|
if (match?.[1])
|
|
153
166
|
prompt = `${await app.skills.load(match[1])}\n\nUser request: ${match[2] ?? ""}`;
|
|
154
|
-
await app.runner.run(session, template?.text ?? prompt, controller.signal,
|
|
167
|
+
await app.runner.run(session, template?.text ?? prompt, controller.signal, {
|
|
168
|
+
...activeRunOptions,
|
|
169
|
+
...(template ? { display: template.display } : {}),
|
|
170
|
+
});
|
|
155
171
|
if (!opts.json)
|
|
156
172
|
process.stdout.write(`\nSession: ${session}\n`);
|
|
157
173
|
return;
|
|
@@ -211,7 +227,10 @@ async function run(cmd, prompt, sessionId) {
|
|
|
211
227
|
const input = match?.[1] ? `${await app.skills.load(match[1])}\n\n${match[2] ?? ""}` : line;
|
|
212
228
|
try {
|
|
213
229
|
const template = app.expandPrompt(input);
|
|
214
|
-
await app.runner.run(session, template?.text ?? input, controller.signal,
|
|
230
|
+
await app.runner.run(session, template?.text ?? input, controller.signal, {
|
|
231
|
+
...activeRunOptions,
|
|
232
|
+
...(template ? { display: template.display } : {}),
|
|
233
|
+
});
|
|
215
234
|
}
|
|
216
235
|
catch (e) {
|
|
217
236
|
console.error(String(e));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for the ACTIVE (main-session) agent: the built-in catalog, user/plugin
|
|
3
|
+
* contributions, resolution and the per-run options an agent implies. No terminal or pi-tui
|
|
4
|
+
* imports here — the Picker and status-line rendering live in the TUI shell.
|
|
5
|
+
*
|
|
6
|
+
* The active agent drives the MAIN session (its `instructions` are appended per run and an
|
|
7
|
+
* agent-specific `model` is offered on selection). This is deliberately separate from the
|
|
8
|
+
* delegated child-session agents of the subagents plugin: a subagent definition becomes a main
|
|
9
|
+
* agent only when it is marked main-capable (`mode: primary|all`), which the subagents plugin
|
|
10
|
+
* publishes for the host to read.
|
|
11
|
+
*/
|
|
12
|
+
export interface ActiveAgent {
|
|
13
|
+
/** Stable id, also the persisted `agents.active` value. */
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** System prompt appended to the main session's instructions on every run. */
|
|
18
|
+
instructions?: string;
|
|
19
|
+
/** Optional default model selector (`provider/model` or an unambiguous model id). */
|
|
20
|
+
model?: string;
|
|
21
|
+
/** Read-only agents run with write/process/external tools disabled and approvals off. */
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
/** Product built-ins are shown as built-in and never shadowed by contributed definitions. */
|
|
24
|
+
source: "builtin" | "user" | "plugin";
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_AGENT_ID = "build";
|
|
27
|
+
/** Product built-ins. `build` keeps the CURRENT behavior exactly: no added persona, full power. */
|
|
28
|
+
export declare const BUILTIN_AGENTS: ActiveAgent[];
|
|
29
|
+
export interface MainCapableAgentRecord {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
prompt: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
source: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Maps a main-capable definition published by the subagents plugin to an active agent. */
|
|
39
|
+
export declare function mainAgentFromRecord(record: MainCapableAgentRecord): ActiveAgent;
|
|
40
|
+
/** Builds the selectable catalog from the subagents plugin's published state (best-effort). */
|
|
41
|
+
export declare function agentCatalogFromState(untrusted: unknown): ActiveAgent[];
|
|
42
|
+
/**
|
|
43
|
+
* The full selectable catalog: built-ins first, then contributed main-capable definitions.
|
|
44
|
+
* A contributed definition may never shadow a built-in id; later duplicates of a contributed id
|
|
45
|
+
* are dropped (the subagents plugin already resolved precedence by its own discovery order).
|
|
46
|
+
*/
|
|
47
|
+
export declare function activeAgentCatalog(contributions?: ActiveAgent[]): ActiveAgent[];
|
|
48
|
+
/** Resolves the persisted agent id; unknown ids fall back to the built-in default (`build`). */
|
|
49
|
+
export declare function resolveActiveAgent(agents: ActiveAgent[], id: string | undefined): ActiveAgent;
|
|
50
|
+
/** The run options an active agent implies. An undefined agent yields the current behavior. */
|
|
51
|
+
export declare function agentRunOptions(agent: ActiveAgent | undefined): {
|
|
52
|
+
instructions?: string;
|
|
53
|
+
policy?: {
|
|
54
|
+
write: false;
|
|
55
|
+
process: false;
|
|
56
|
+
external: false;
|
|
57
|
+
};
|
|
58
|
+
approvals?: false;
|
|
59
|
+
};
|
|
60
|
+
export interface AgentPickerItem {
|
|
61
|
+
value: string;
|
|
62
|
+
label: string;
|
|
63
|
+
description: string;
|
|
64
|
+
}
|
|
65
|
+
/** Picker rows for `/agents`: name with (current), built-in/default and read-only markers. */
|
|
66
|
+
export declare function agentPickerItems(agents: ActiveAgent[], activeId: string): AgentPickerItem[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const DEFAULT_AGENT_ID = "build";
|
|
2
|
+
const PLAN_INSTRUCTIONS = `You are the "plan" agent: a read-only planning assistant for this main session.
|
|
3
|
+
|
|
4
|
+
Your job is analysis and planning only. Investigate the repository with read-only tools
|
|
5
|
+
(list_files, search_text, read_file, git_status, git_diff) and return a concrete, ordered plan:
|
|
6
|
+
files to change, functions to touch, risks and verification steps. You may run context and
|
|
7
|
+
inspection tools that do not modify anything.
|
|
8
|
+
|
|
9
|
+
You never edit, write or delete files, and you never start processes that change the workspace
|
|
10
|
+
or the network. If a task requires changes, hand back a precise plan (and the exact commands)
|
|
11
|
+
instead of making them.`;
|
|
12
|
+
/** Product built-ins. `build` keeps the CURRENT behavior exactly: no added persona, full power. */
|
|
13
|
+
export const BUILTIN_AGENTS = [
|
|
14
|
+
{
|
|
15
|
+
id: DEFAULT_AGENT_ID,
|
|
16
|
+
name: "build",
|
|
17
|
+
description: "Default full-power agent: edits code and files, runs processes and verifies its work through the normal permission flow.",
|
|
18
|
+
source: "builtin",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "plan",
|
|
22
|
+
name: "plan",
|
|
23
|
+
description: "Read-only planning agent: analyzes the codebase and returns an implementation plan without modifying anything.",
|
|
24
|
+
instructions: PLAN_INSTRUCTIONS,
|
|
25
|
+
readOnly: true,
|
|
26
|
+
source: "builtin",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
/** Maps a main-capable definition published by the subagents plugin to an active agent. */
|
|
30
|
+
export function mainAgentFromRecord(record) {
|
|
31
|
+
const source = String(record.source ?? "");
|
|
32
|
+
return {
|
|
33
|
+
id: record.name,
|
|
34
|
+
name: record.name,
|
|
35
|
+
description: record.description,
|
|
36
|
+
...(record.prompt ? { instructions: record.prompt } : {}),
|
|
37
|
+
...(record.model ? { model: record.model } : {}),
|
|
38
|
+
...(record.readOnly ? { readOnly: true } : {}),
|
|
39
|
+
...(source.startsWith("plugin:")
|
|
40
|
+
? { source: "plugin" }
|
|
41
|
+
: { source: "user" }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Builds the selectable catalog from the subagents plugin's published state (best-effort). */
|
|
45
|
+
export function agentCatalogFromState(untrusted) {
|
|
46
|
+
if (!Array.isArray(untrusted))
|
|
47
|
+
return activeAgentCatalog([]);
|
|
48
|
+
const records = untrusted.filter((entry) => !!entry &&
|
|
49
|
+
typeof entry === "object" &&
|
|
50
|
+
typeof entry.name === "string" &&
|
|
51
|
+
typeof entry.description === "string" &&
|
|
52
|
+
entry.description.length > 0 &&
|
|
53
|
+
typeof entry.prompt === "string");
|
|
54
|
+
return activeAgentCatalog(records.map(mainAgentFromRecord));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The full selectable catalog: built-ins first, then contributed main-capable definitions.
|
|
58
|
+
* A contributed definition may never shadow a built-in id; later duplicates of a contributed id
|
|
59
|
+
* are dropped (the subagents plugin already resolved precedence by its own discovery order).
|
|
60
|
+
*/
|
|
61
|
+
export function activeAgentCatalog(contributions = []) {
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const agent of [...BUILTIN_AGENTS, ...contributions]) {
|
|
65
|
+
if (seen.has(agent.id))
|
|
66
|
+
continue;
|
|
67
|
+
seen.add(agent.id);
|
|
68
|
+
out.push(agent);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Resolves the persisted agent id; unknown ids fall back to the built-in default (`build`). */
|
|
73
|
+
export function resolveActiveAgent(agents, id) {
|
|
74
|
+
const found = id ? agents.find((a) => a.id === id) : undefined;
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
return agents.find((a) => a.id === DEFAULT_AGENT_ID) ?? agents[0] ?? BUILTIN_AGENTS[0];
|
|
78
|
+
}
|
|
79
|
+
/** The run options an active agent implies. An undefined agent yields the current behavior. */
|
|
80
|
+
export function agentRunOptions(agent) {
|
|
81
|
+
if (!agent)
|
|
82
|
+
return {};
|
|
83
|
+
return {
|
|
84
|
+
...(agent.instructions ? { instructions: agent.instructions } : {}),
|
|
85
|
+
...(agent.readOnly
|
|
86
|
+
? {
|
|
87
|
+
policy: { write: false, process: false, external: false },
|
|
88
|
+
approvals: false,
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Picker rows for `/agents`: name with (current), built-in/default and read-only markers. */
|
|
94
|
+
export function agentPickerItems(agents, activeId) {
|
|
95
|
+
return agents.map((agent) => ({
|
|
96
|
+
value: agent.id,
|
|
97
|
+
label: [
|
|
98
|
+
agent.name,
|
|
99
|
+
agent.id === activeId ? "(current)" : undefined,
|
|
100
|
+
agent.id === DEFAULT_AGENT_ID ? "(default)" : undefined,
|
|
101
|
+
agent.readOnly ? "read-only" : undefined,
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" · "),
|
|
105
|
+
description: [
|
|
106
|
+
agent.description,
|
|
107
|
+
agent.model ? `default model: ${agent.model}` : undefined,
|
|
108
|
+
agent.instructions ? "injects its system prompt from the next prompt" : undefined,
|
|
109
|
+
]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(" · "),
|
|
112
|
+
}));
|
|
113
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { CombinedAutocompleteProvider, Container, Editor, getImageDimensions, getNativeClipboard, Key, matchesKey, ProcessTerminal, ScrollView, SelectList, TuiAltScreen, truncateToWidth, VStack, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { CombinedAutocompleteProvider, Container, Editor, getImageDimensions, getNativeClipboard, Key, MouseRegion, matchesKey, ProcessTerminal, ScrollView, SelectList, TuiAltScreen, truncateToWidth, VStack, } from "@earendil-works/pi-tui";
|
|
3
3
|
import { loadVersion } from "../version.js";
|
|
4
|
+
import { activeAgentCatalog, agentPickerItems, agentRunOptions, mainAgentFromRecord, resolveActiveAgent, } from "./agents.js";
|
|
4
5
|
import { MAX_ATTACHMENTS_PER_MESSAGE, MAX_IMAGE_BYTES, pasteImageFromClipboard, removeLastAttachment, toApiAttachment, } from "./attachments.js";
|
|
5
6
|
import { copyText, nodeSpawn } from "./clipboard.js";
|
|
6
7
|
import { AttachmentsBar, BannerBlock, clock, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
|
|
@@ -13,7 +14,7 @@ import { InteractiveQueue } from "./queue.js";
|
|
|
13
14
|
import { settingsMenuRows } from "./settings.js";
|
|
14
15
|
import { SettingsMenu } from "./settings-menu.js";
|
|
15
16
|
import { SkillsManager } from "./skills-manager.js";
|
|
16
|
-
import { addItem, COMMANDS, configuredProviderModelItems, formatContext, formatDuration, formatTokens, hostOf, initialViewState, itemsFromHistory, lastAssistantText, mcpServerItems, mcpToolItems, parseCommand, pluginCatalogItems, pluginToggleNeedsConfirmation, providerModelItems, reduceEvent, reservedCommandNames, resolveCommand, shortenPath, shortId, slashCompletionCommands, summarizeToolArgs, } from "./state.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
18
|
import { editorTheme, selectListTheme, style } from "./theme.js";
|
|
18
19
|
const VERSION = loadVersion(import.meta.url);
|
|
19
20
|
/** Inline selection list with type-to-filter, rendered above the editor. */
|
|
@@ -106,17 +107,44 @@ export async function runTui(options) {
|
|
|
106
107
|
modelList = undefined;
|
|
107
108
|
throw e;
|
|
108
109
|
}));
|
|
110
|
+
/** Active-provider catalog by model id; feeds effort support and display names. */
|
|
111
|
+
let modelCatalog = new Map();
|
|
109
112
|
/**
|
|
110
113
|
* Reloads the active-provider catalog and repaints when it lands, so the context bar learns
|
|
111
114
|
* the ACTIVE model's real window (previously only the first /model picker or autocomplete
|
|
112
115
|
* call loaded it, leaving a fabricated 40k total on screen). Called at startup and after
|
|
113
116
|
* every provider/model switch; the bar honestly shows `?` until the catalog arrives.
|
|
117
|
+
* The same refresh feeds the effort status (supported levels + default) and the model
|
|
118
|
+
* display name used by the header and the status line below the editor.
|
|
114
119
|
*/
|
|
115
120
|
const primeModels = () => {
|
|
116
121
|
void models()
|
|
117
|
-
.then(() =>
|
|
122
|
+
.then((list) => {
|
|
123
|
+
modelCatalog = new Map(list.map((m) => [m.id, m]));
|
|
124
|
+
refreshEffort();
|
|
125
|
+
tui.requestRender();
|
|
126
|
+
})
|
|
118
127
|
.catch(() => { });
|
|
119
128
|
};
|
|
129
|
+
const effortSupportFor = (model) => modelCatalog.get(model)?.effort;
|
|
130
|
+
const modelDisplayName = (model) => modelCatalog.get(model)?.name ?? model;
|
|
131
|
+
/** Persisted `agents.effort` is silently replaced by the model default when unsupported; one notice. */
|
|
132
|
+
let notifiedEffortFallback = "";
|
|
133
|
+
function refreshEffort() {
|
|
134
|
+
const persisted = app.config.agents.effort;
|
|
135
|
+
const capability = effortSupportFor(view.model);
|
|
136
|
+
if (!persisted || !capability?.supportedLevels?.length)
|
|
137
|
+
return;
|
|
138
|
+
if (capability.supportedLevels.includes(persisted))
|
|
139
|
+
return;
|
|
140
|
+
const key = `${view.model}:${persisted}`;
|
|
141
|
+
if (notifiedEffortFallback !== key) {
|
|
142
|
+
notifiedEffortFallback = key;
|
|
143
|
+
flashHint(`Reasoning effort "${persisted}" is not supported by ${modelDisplayName(view.model)}; using ${capability.defaultLevel ? `"${capability.defaultLevel}"` : "the provider default"}.`, 7000);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Effective effort sent on runs: the persisted level when supported, else the model default. */
|
|
147
|
+
const currentEffort = () => effectiveEffort(app.config.agents.effort, effortSupportFor(view.model));
|
|
120
148
|
const terminal = new ProcessTerminal();
|
|
121
149
|
const copy = async (text) => copyText(text, {
|
|
122
150
|
platform: process.platform,
|
|
@@ -142,6 +170,23 @@ export async function runTui(options) {
|
|
|
142
170
|
// Read-only views of child sessions (subagents), fed by their events.
|
|
143
171
|
const childViews = new Map();
|
|
144
172
|
const childView = new TranscriptSync();
|
|
173
|
+
// Fold state of every collapsible transcript row (reasoning sections, tool batches, long
|
|
174
|
+
// outputs). Keyed by stable fold keys from state.ts; absent entries use the pure defaults.
|
|
175
|
+
const folded = new Map();
|
|
176
|
+
const foldOf = (candidate) => folded.get(candidate.key) ?? candidate.defaultExpanded;
|
|
177
|
+
main.setFoldResolver(foldOf);
|
|
178
|
+
childView.setFoldResolver(foldOf);
|
|
179
|
+
/** Which transcript is on screen right now (child view while viewing a subagent, else main). */
|
|
180
|
+
const visibleTranscript = () => viewedState()
|
|
181
|
+
? { items: viewedState()?.items ?? [], sync: childView }
|
|
182
|
+
: { items: view.items, sync: main };
|
|
183
|
+
/** Applies one fold toggle and re-syncs only the affected transcript (cheap: events, not frames). */
|
|
184
|
+
const toggleFold = (candidate) => {
|
|
185
|
+
folded.set(candidate.key, !(folded.get(candidate.key) ?? candidate.defaultExpanded));
|
|
186
|
+
const visible = visibleTranscript();
|
|
187
|
+
visible.sync.sync(visible.items);
|
|
188
|
+
tui.requestRender();
|
|
189
|
+
};
|
|
145
190
|
let panelState = initialPanelState();
|
|
146
191
|
const startupDiagnostics = [];
|
|
147
192
|
let hint;
|
|
@@ -170,6 +215,24 @@ export async function runTui(options) {
|
|
|
170
215
|
};
|
|
171
216
|
refreshBranch();
|
|
172
217
|
const branchTimer = setInterval(refreshBranch, BRANCH_REFRESH_MS);
|
|
218
|
+
/**
|
|
219
|
+
* Selectable ACTIVE (main-session) agents: the built-in `build`/`plan` pair plus the
|
|
220
|
+
* main-capable definitions the subagents plugin publishes (`mode: primary|all`) through its
|
|
221
|
+
* plugin state. Never imports the plugin package; a missing/unavailable contribution is just
|
|
222
|
+
* an empty extra list. The persisted `agents.active` id resolves against this catalog.
|
|
223
|
+
*/
|
|
224
|
+
const mainAgents = () => {
|
|
225
|
+
let contributions = [];
|
|
226
|
+
try {
|
|
227
|
+
const records = app.plugins.pluginState("subagents", "mainAgents");
|
|
228
|
+
contributions = (records ?? []).map(mainAgentFromRecord);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* plugin state is best-effort */
|
|
232
|
+
}
|
|
233
|
+
return activeAgentCatalog(contributions);
|
|
234
|
+
};
|
|
235
|
+
const currentAgent = () => resolveActiveAgent(mainAgents(), app.config.agents.active);
|
|
173
236
|
const headerInfo = () => {
|
|
174
237
|
const policy = app.runner.policy, ask = app.runner.approvals;
|
|
175
238
|
return {
|
|
@@ -177,6 +240,8 @@ export async function runTui(options) {
|
|
|
177
240
|
host: hostOf(String(activeProvider?.profile.baseURL ?? app.config.provider.baseURL)),
|
|
178
241
|
apiMode: String(activeProvider?.profile.apiMode ?? app.config.provider.apiMode),
|
|
179
242
|
provider: app.providers.get(activeProvider?.id ?? "")?.name,
|
|
243
|
+
modelName: modelDisplayName(view.model),
|
|
244
|
+
effort: currentEffort(),
|
|
180
245
|
cwd: shortenPath(app.workspace, homedir()),
|
|
181
246
|
session: shortId(session),
|
|
182
247
|
branch: branchName,
|
|
@@ -228,7 +293,15 @@ export async function runTui(options) {
|
|
|
228
293
|
: panelState.focus === "view"
|
|
229
294
|
? viewHint()
|
|
230
295
|
: undefined;
|
|
231
|
-
const footer = new Footer(() => viewedState() ?? view, () => app.contextBudget((viewedState() ?? view).model), () => panelHint() ?? hint, () => [...app.plugins.status.values()].map((s) => s.text)
|
|
296
|
+
const footer = new Footer(() => viewedState() ?? view, () => app.contextBudget((viewedState() ?? view).model), () => panelHint() ?? hint, () => [...app.plugins.status.values()].map((s) => s.text),
|
|
297
|
+
// Status row below the editor: `agent: <name> · <model> · <provider> · <effort>` (the
|
|
298
|
+
// effort segment appears only when the active model advertises supported levels).
|
|
299
|
+
() => ({
|
|
300
|
+
agent: currentAgent().name,
|
|
301
|
+
model: modelDisplayName(view.model),
|
|
302
|
+
provider: app.providers.get(activeProvider?.id ?? "")?.name,
|
|
303
|
+
effort: currentEffort(),
|
|
304
|
+
}));
|
|
232
305
|
const treePanel = new TreePanel(() => {
|
|
233
306
|
const entry = panelEntry();
|
|
234
307
|
if (!entry)
|
|
@@ -256,18 +329,32 @@ export async function runTui(options) {
|
|
|
256
329
|
bottom.addChild(editor);
|
|
257
330
|
bottom.addChild(treePanel);
|
|
258
331
|
bottom.addChild(footer);
|
|
332
|
+
// Click-to-toggle on collapsible transcript rows: pi-tui synthesizes `click` events after a
|
|
333
|
+
// press+release without movement, so plain clicks fold/unfold while drag-selection keeps
|
|
334
|
+
// working (copyOnSelect must never be disturbed). The region sits between the ScrollView and
|
|
335
|
+
// the content switch; content rows = visible rows + the scroll offset.
|
|
336
|
+
let transcriptScroll;
|
|
337
|
+
const transcriptRegion = new MouseRegion(new Switch(() => (viewedState() ? childView.container : transcript)), (event) => {
|
|
338
|
+
if (picker || event.type !== "click" || event.button !== "left")
|
|
339
|
+
return undefined;
|
|
340
|
+
const scroll = transcriptScroll;
|
|
341
|
+
if (!scroll)
|
|
342
|
+
return undefined;
|
|
343
|
+
const visible = visibleTranscript();
|
|
344
|
+
const candidate = visible.sync.entryAt(event.y + scroll.scrollTop, event.width);
|
|
345
|
+
if (!candidate)
|
|
346
|
+
return undefined;
|
|
347
|
+
toggleFold(candidate);
|
|
348
|
+
return { handled: true };
|
|
349
|
+
});
|
|
350
|
+
transcriptScroll = new ScrollView(transcriptRegion, {
|
|
351
|
+
follow: "end",
|
|
352
|
+
primary: true,
|
|
353
|
+
overscroll: "chain",
|
|
354
|
+
});
|
|
259
355
|
tui.setLayoutRoot(new VStack([
|
|
260
356
|
{ component: header, basis: "auto" },
|
|
261
|
-
{
|
|
262
|
-
component: new ScrollView(new Switch(() => (viewedState() ? childView.container : transcript)), {
|
|
263
|
-
follow: "end",
|
|
264
|
-
primary: true,
|
|
265
|
-
overscroll: "chain",
|
|
266
|
-
}),
|
|
267
|
-
basis: 0,
|
|
268
|
-
grow: 1,
|
|
269
|
-
minSize: 1,
|
|
270
|
-
},
|
|
357
|
+
{ component: transcriptScroll, basis: 0, grow: 1, minSize: 1 },
|
|
271
358
|
{ component: bottom, basis: "auto", shrink: 1, minSize: 3 },
|
|
272
359
|
]));
|
|
273
360
|
// Prime the active provider's catalog so the context bar shows the model's real window
|
|
@@ -317,6 +404,14 @@ export async function runTui(options) {
|
|
|
317
404
|
const info = (text) => push({ kind: "info", text });
|
|
318
405
|
let terminalEvent = false;
|
|
319
406
|
dispatch = (event) => {
|
|
407
|
+
// The event stream stays text-only (headless/JSONL consumers see the projection); the TUI
|
|
408
|
+
// pulls the persisted rich parts (ui/image) itself so native rendering survives resume.
|
|
409
|
+
if (event.type === "tool_completed") {
|
|
410
|
+
const data = (event.data ?? {});
|
|
411
|
+
const rich = richPartsOf(app.store.callResult(event.sessionId, String(data.id ?? "")));
|
|
412
|
+
if (rich.ui !== undefined || rich.image !== undefined)
|
|
413
|
+
event = { ...event, data: { ...data, ...rich } };
|
|
414
|
+
}
|
|
320
415
|
if (event.sessionId !== session) {
|
|
321
416
|
// Child session (e.g. a subagent): keep a view model for its read-only view.
|
|
322
417
|
const child = childViews.get(event.sessionId) ?? initialViewState(view.model);
|
|
@@ -489,7 +584,14 @@ export async function runTui(options) {
|
|
|
489
584
|
pendingAttachments = [];
|
|
490
585
|
attachmentsBar.invalidate();
|
|
491
586
|
push({ kind: "user", text: display });
|
|
587
|
+
// The ACTIVE agent drives the main session: its system prompt is appended per run and a
|
|
588
|
+
// read-only agent narrows the run to reads (no approvals). The persisted reasoning effort is
|
|
589
|
+
// resolved against the ACTIVE model and sent when the model advertises supported levels.
|
|
590
|
+
const agent = currentAgent();
|
|
591
|
+
const effort = currentEffort();
|
|
492
592
|
return task((signal) => app.runner.run(session, prompt, signal, {
|
|
593
|
+
...agentRunOptions(agent),
|
|
594
|
+
...(effort ? { reasoningEffort: effort } : {}),
|
|
493
595
|
...(persistDisplay ? { display: persistDisplay } : {}),
|
|
494
596
|
...(attachments.length ? { attachments } : {}),
|
|
495
597
|
}));
|
|
@@ -953,6 +1055,93 @@ export async function runTui(options) {
|
|
|
953
1055
|
};
|
|
954
1056
|
openCatalog();
|
|
955
1057
|
};
|
|
1058
|
+
/**
|
|
1059
|
+
* `/agents` (active agent): a navigable picker listing every selectable main-session agent with
|
|
1060
|
+
* its description. Selecting one persists `agents.active` globally (applies from the next
|
|
1061
|
+
* prompt), optionally switches the session model to the agent's declared model, and starts a
|
|
1062
|
+
* fresh session just like `/model` when it does. Esc returns to the editor.
|
|
1063
|
+
*/
|
|
1064
|
+
const openAgentsPicker = () => {
|
|
1065
|
+
const agents = mainAgents();
|
|
1066
|
+
const current = currentAgent();
|
|
1067
|
+
showPicker(new Picker("Active agent", agentPickerItems(agents, current.id), (item) => {
|
|
1068
|
+
const agent = agents.find((candidate) => candidate.id === item.value);
|
|
1069
|
+
if (agent)
|
|
1070
|
+
void applyActiveAgent(agent);
|
|
1071
|
+
}, closePicker, true, "Changing the active agent takes effect from the next prompt; the current session is kept unless the agent declares a different model."));
|
|
1072
|
+
};
|
|
1073
|
+
const applyActiveAgent = (agent) => {
|
|
1074
|
+
void (async () => {
|
|
1075
|
+
closePicker();
|
|
1076
|
+
const previous = currentAgent().name;
|
|
1077
|
+
try {
|
|
1078
|
+
await app.updateSetting("agents.active", agent.id);
|
|
1079
|
+
let switched = false;
|
|
1080
|
+
if (agent.model && app.store.get(session).model !== agent.model) {
|
|
1081
|
+
flashHint(`Switching to ${agent.name}'s model ${agent.model}…`, 60_000);
|
|
1082
|
+
const created = await app.switchModel(agent.model);
|
|
1083
|
+
if (created) {
|
|
1084
|
+
activeProvider = app.providerInfo;
|
|
1085
|
+
modelList = undefined;
|
|
1086
|
+
session = created.id;
|
|
1087
|
+
reset(initialViewState(created.model));
|
|
1088
|
+
switched = true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
notice(`Active agent: ${agent.name}${agent.readOnly ? " (read-only)" : ""}. Applies from the next prompt.`);
|
|
1092
|
+
if (agent.model && switched)
|
|
1093
|
+
notice(`Agent ${agent.name} switches the session model to ${app.store.get(session).model}.`);
|
|
1094
|
+
else if (agent.model)
|
|
1095
|
+
flashHint(`Agent ${agent.name} keeps the current model (already ${view.model}). Override any time with /model.`);
|
|
1096
|
+
else
|
|
1097
|
+
flashHint(`Active agent: ${agent.name} (was ${previous}).`);
|
|
1098
|
+
refreshEffort();
|
|
1099
|
+
primeModels();
|
|
1100
|
+
tui.requestRender();
|
|
1101
|
+
}
|
|
1102
|
+
catch (cause) {
|
|
1103
|
+
error(cause);
|
|
1104
|
+
}
|
|
1105
|
+
})();
|
|
1106
|
+
};
|
|
1107
|
+
/**
|
|
1108
|
+
* `/effort [level]`: without an argument, a picker over the ACTIVE model's advertised effort
|
|
1109
|
+
* levels (marking the model default); with an argument, the level is validated against
|
|
1110
|
+
* `effort.supportedLevels` and persisted globally. The chosen level is sent from the next
|
|
1111
|
+
* prompt; if the model later changes to one that does not support it, the model's default is
|
|
1112
|
+
* used silently with a one-time notice.
|
|
1113
|
+
*/
|
|
1114
|
+
const setEffort = async (args) => {
|
|
1115
|
+
const capability = effortSupportFor(view.model);
|
|
1116
|
+
if (!args) {
|
|
1117
|
+
if (!capability?.supportedLevels?.length)
|
|
1118
|
+
return notice(`${view.model} does not advertise reasoning effort levels (ModelInfo.effort), so /effort is not available for it.`);
|
|
1119
|
+
const items = effortPickerItems(capability, currentEffort());
|
|
1120
|
+
showPicker(new Picker(`Reasoning effort · ${modelDisplayName(view.model)}`, items, (item) => void applyEffort(item.value), closePicker, true));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const invalid = validateEffortLevel(args, capability);
|
|
1124
|
+
if (invalid)
|
|
1125
|
+
return notice(invalid);
|
|
1126
|
+
await applyEffort(args);
|
|
1127
|
+
};
|
|
1128
|
+
const applyEffort = async (value) => {
|
|
1129
|
+
const clear = value === "!clear";
|
|
1130
|
+
closePicker();
|
|
1131
|
+
try {
|
|
1132
|
+
await app.updateSetting("agents.effort", clear ? undefined : value);
|
|
1133
|
+
const capability = effortSupportFor(view.model);
|
|
1134
|
+
const effective = effectiveEffort(clear ? undefined : value, capability);
|
|
1135
|
+
notice(clear
|
|
1136
|
+
? `Reasoning effort cleared; ${capability?.defaultLevel ? `the model default "${capability.defaultLevel}" is used` : "the provider default is used"}.`
|
|
1137
|
+
: `Reasoning effort: ${value}${value === capability?.defaultLevel ? ` (default for ${modelDisplayName(view.model)})` : ""}${effective && effective !== value ? ` — effective: ${effective}` : ""}. Applies from the next prompt.`);
|
|
1138
|
+
refreshEffort();
|
|
1139
|
+
tui.requestRender();
|
|
1140
|
+
}
|
|
1141
|
+
catch (cause) {
|
|
1142
|
+
error(cause);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
956
1145
|
/**
|
|
957
1146
|
* Navigation rows at the bottom of `/settings`: they route to the existing managers and one-shot
|
|
958
1147
|
* actions exactly like the old picker, so every prior entry stays reachable. `compact` and
|
|
@@ -1166,7 +1355,7 @@ export async function runTui(options) {
|
|
|
1166
1355
|
"- `/init` (above) writes AGENTS.md; the shell command `alisio setup` only scaffolds `.alisio/config.json`",
|
|
1167
1356
|
"- `/command plugin.id:name args` — run a plugin command",
|
|
1168
1357
|
"",
|
|
1169
|
-
"**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 · PgUp/PgDn or mouse wheel scroll · Ctrl+X agent panel · Ctrl+B background running agents",
|
|
1358
|
+
"**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",
|
|
1170
1359
|
`**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`,
|
|
1171
1360
|
].join("\n");
|
|
1172
1361
|
const mutating = new Set([
|
|
@@ -1256,6 +1445,30 @@ export async function runTui(options) {
|
|
|
1256
1445
|
`(reuse the user's own wording for the question text). Do not answer in plain text ` +
|
|
1257
1446
|
`first; call the tool right away.`);
|
|
1258
1447
|
}
|
|
1448
|
+
case "agents": {
|
|
1449
|
+
// The subagents plugin registers its own built-in `/agents` command for subagent task
|
|
1450
|
+
// management (list/open/cancel/kill/resume/merge/discard/defs). With arguments, route
|
|
1451
|
+
// to it verbatim (still available while a turn runs, exactly as before); without
|
|
1452
|
+
// arguments, this opens the ACTIVE-agent picker.
|
|
1453
|
+
if (parsed.args) {
|
|
1454
|
+
const handler = app.plugins.commands.get("agents");
|
|
1455
|
+
if (handler)
|
|
1456
|
+
return info(await handler(parsed.args, { sessionId: session }));
|
|
1457
|
+
}
|
|
1458
|
+
if (busy) {
|
|
1459
|
+
editor.setText(raw);
|
|
1460
|
+
flashHint("A turn is running: wait for it to finish before switching the active agent");
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
return openAgentsPicker();
|
|
1464
|
+
}
|
|
1465
|
+
case "effort":
|
|
1466
|
+
if (busy) {
|
|
1467
|
+
editor.setText(raw);
|
|
1468
|
+
flashHint("A turn is running: wait for it to finish before changing the effort");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
return await setEffort(parsed.args);
|
|
1259
1472
|
case "clear": {
|
|
1260
1473
|
await endSession("clear");
|
|
1261
1474
|
session = app.store.create(app.workspace, app.provider.id, view.model).id;
|
|
@@ -1433,6 +1646,20 @@ export async function runTui(options) {
|
|
|
1433
1646
|
removeAttachment();
|
|
1434
1647
|
return { consume: true };
|
|
1435
1648
|
}
|
|
1649
|
+
// `x` on an EMPTY editor line toggles the nearest collapsible transcript row — a finished
|
|
1650
|
+
// `Thought` section, a grouped batch of tool calls, or a long command output. Same
|
|
1651
|
+
// empty-input convention as Enter, Ctrl+D and `c`/`y`; typing `x` mid-message is never
|
|
1652
|
+
// intercepted, and with nothing collapsible it types normally.
|
|
1653
|
+
if (foldToggleKey(data, {
|
|
1654
|
+
text: editor.getText(),
|
|
1655
|
+
autocomplete: editor.isShowingAutocomplete(),
|
|
1656
|
+
})) {
|
|
1657
|
+
const target = foldCandidates(visibleTranscript().items).at(-1);
|
|
1658
|
+
if (target) {
|
|
1659
|
+
toggleFold(target);
|
|
1660
|
+
return { consume: true };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1436
1663
|
}
|
|
1437
1664
|
if (!picker) {
|
|
1438
1665
|
if (matchesKey(data, Key.ctrl("x"))) {
|
|
@@ -1469,6 +1696,24 @@ export async function runTui(options) {
|
|
|
1469
1696
|
return { consume: true };
|
|
1470
1697
|
}
|
|
1471
1698
|
}
|
|
1699
|
+
// `c`/`y` on an EMPTY editor line copies the last assistant response as raw text, the same
|
|
1700
|
+
// as /copy; typing `c` or `y` mid-message is never intercepted.
|
|
1701
|
+
if (editorCopyKey(data, {
|
|
1702
|
+
text: editor.getText(),
|
|
1703
|
+
autocomplete: editor.isShowingAutocomplete(),
|
|
1704
|
+
busy,
|
|
1705
|
+
})) {
|
|
1706
|
+
const last = lastAssistantText(view.items);
|
|
1707
|
+
if (last) {
|
|
1708
|
+
void copy(last).then((result) => {
|
|
1709
|
+
const message = copyMessage(result);
|
|
1710
|
+
tui.flash(message);
|
|
1711
|
+
if (!result.ok)
|
|
1712
|
+
notice(message);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
return { consume: true };
|
|
1716
|
+
}
|
|
1472
1717
|
}
|
|
1473
1718
|
if (matchesKey(data, Key.escape)) {
|
|
1474
1719
|
if (busy && !picker && !editor.isShowingAutocomplete()) {
|