@alisio/alisio-code 0.1.0-alpha.15 → 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/tui/app.js +84 -13
- package/dist/tui/components.d.ts +71 -11
- package/dist/tui/components.js +465 -57
- package/dist/tui/state.d.ts +166 -1
- package/dist/tui/state.js +429 -12
- package/dist/tui/theme.d.ts +42 -1
- package/dist/tui/theme.js +516 -1
- package/package.json +4 -4
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
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";
|
|
@@ -14,7 +14,7 @@ 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, effectiveEffort, effortPickerItems, formatContext, formatDuration, formatTokens, hostOf, initialViewState, itemsFromHistory, lastAssistantText, mcpServerItems, mcpToolItems, parseCommand, pluginCatalogItems, pluginToggleNeedsConfirmation, providerModelItems, reduceEvent, reservedCommandNames, resolveCommand, shortenPath, shortId, slashCompletionCommands, summarizeToolArgs, validateEffortLevel, } 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";
|
|
18
18
|
import { editorTheme, selectListTheme, style } from "./theme.js";
|
|
19
19
|
const VERSION = loadVersion(import.meta.url);
|
|
20
20
|
/** Inline selection list with type-to-filter, rendered above the editor. */
|
|
@@ -170,6 +170,23 @@ export async function runTui(options) {
|
|
|
170
170
|
// Read-only views of child sessions (subagents), fed by their events.
|
|
171
171
|
const childViews = new Map();
|
|
172
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
|
+
};
|
|
173
190
|
let panelState = initialPanelState();
|
|
174
191
|
const startupDiagnostics = [];
|
|
175
192
|
let hint;
|
|
@@ -312,18 +329,32 @@ export async function runTui(options) {
|
|
|
312
329
|
bottom.addChild(editor);
|
|
313
330
|
bottom.addChild(treePanel);
|
|
314
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
|
+
});
|
|
315
355
|
tui.setLayoutRoot(new VStack([
|
|
316
356
|
{ component: header, basis: "auto" },
|
|
317
|
-
{
|
|
318
|
-
component: new ScrollView(new Switch(() => (viewedState() ? childView.container : transcript)), {
|
|
319
|
-
follow: "end",
|
|
320
|
-
primary: true,
|
|
321
|
-
overscroll: "chain",
|
|
322
|
-
}),
|
|
323
|
-
basis: 0,
|
|
324
|
-
grow: 1,
|
|
325
|
-
minSize: 1,
|
|
326
|
-
},
|
|
357
|
+
{ component: transcriptScroll, basis: 0, grow: 1, minSize: 1 },
|
|
327
358
|
{ component: bottom, basis: "auto", shrink: 1, minSize: 3 },
|
|
328
359
|
]));
|
|
329
360
|
// Prime the active provider's catalog so the context bar shows the model's real window
|
|
@@ -373,6 +404,14 @@ export async function runTui(options) {
|
|
|
373
404
|
const info = (text) => push({ kind: "info", text });
|
|
374
405
|
let terminalEvent = false;
|
|
375
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
|
+
}
|
|
376
415
|
if (event.sessionId !== session) {
|
|
377
416
|
// Child session (e.g. a subagent): keep a view model for its read-only view.
|
|
378
417
|
const child = childViews.get(event.sessionId) ?? initialViewState(view.model);
|
|
@@ -1316,7 +1355,7 @@ export async function runTui(options) {
|
|
|
1316
1355
|
"- `/init` (above) writes AGENTS.md; the shell command `alisio setup` only scaffolds `.alisio/config.json`",
|
|
1317
1356
|
"- `/command plugin.id:name args` — run a plugin command",
|
|
1318
1357
|
"",
|
|
1319
|
-
"**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",
|
|
1320
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`,
|
|
1321
1360
|
].join("\n");
|
|
1322
1361
|
const mutating = new Set([
|
|
@@ -1607,6 +1646,20 @@ export async function runTui(options) {
|
|
|
1607
1646
|
removeAttachment();
|
|
1608
1647
|
return { consume: true };
|
|
1609
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
|
+
}
|
|
1610
1663
|
}
|
|
1611
1664
|
if (!picker) {
|
|
1612
1665
|
if (matchesKey(data, Key.ctrl("x"))) {
|
|
@@ -1643,6 +1696,24 @@ export async function runTui(options) {
|
|
|
1643
1696
|
return { consume: true };
|
|
1644
1697
|
}
|
|
1645
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
|
+
}
|
|
1646
1717
|
}
|
|
1647
1718
|
if (matchesKey(data, Key.escape)) {
|
|
1648
1719
|
if (busy && !picker && !editor.isShowingAutocomplete()) {
|
package/dist/tui/components.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import type { PanelNode } from "@alisio/sdk";
|
|
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
4
|
import { type QuestionPanelState, type QuestionSpec } from "./questions.ts";
|
|
5
|
-
import { type ContextBudget, type IdentityInput, type TranscriptItem, type ViewState } from "./state.ts";
|
|
5
|
+
import { type ContextBudget, type FoldableKind, type FoldCandidate, type GroupedTool, type IdentityInput, type TranscriptEntry, type TranscriptItem, type ViewState } from "./state.ts";
|
|
6
6
|
export declare const SPINNER: string[];
|
|
7
7
|
/** Shared animation clock advanced by the app while work is running. */
|
|
8
8
|
export declare const clock: {
|
|
9
9
|
frame: number;
|
|
10
10
|
now: number;
|
|
11
11
|
};
|
|
12
|
+
/** Fold state the components receive: neither knows keys nor defaults (components stay dumb). */
|
|
13
|
+
export interface FoldInfo {
|
|
14
|
+
expanded: boolean;
|
|
15
|
+
kind: FoldableKind;
|
|
16
|
+
}
|
|
17
|
+
/** `Command exited with code 0.` (green) / `Command exited with code N.` (red) when known. */
|
|
18
|
+
export declare const exitCodeLine: (code: number | undefined, indent?: string) => string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Tree rendering with branch glyphs: `├─`/`└─`/`│` when the terminal supports Unicode, ASCII
|
|
21
|
+
* `|-`/`` `- ``/`|` otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderTreeLines(nodes: TreeNode[], unicode: boolean): string[];
|
|
24
|
+
/** Renders a `{type:"ui"}` block to styled lines for the given inner width. */
|
|
25
|
+
export declare function renderUiBlock(block: UiBlock, width: number, unicode: boolean): string[];
|
|
12
26
|
export type PermissionState = "on" | "ask" | "off";
|
|
13
27
|
export interface HeaderInfo {
|
|
14
28
|
version: string;
|
|
@@ -53,13 +67,17 @@ export declare class UserBlock implements Component {
|
|
|
53
67
|
}
|
|
54
68
|
export declare class AssistantBlock implements Component {
|
|
55
69
|
private markdown;
|
|
70
|
+
/** Bumped by TranscriptSync when this row's content changes; render caches on it. */
|
|
71
|
+
version: number;
|
|
72
|
+
fold?: FoldInfo;
|
|
56
73
|
private item;
|
|
74
|
+
private cache?;
|
|
57
75
|
constructor(item: Extract<TranscriptItem, {
|
|
58
76
|
kind: "assistant";
|
|
59
77
|
}>);
|
|
60
78
|
update(item: Extract<TranscriptItem, {
|
|
61
79
|
kind: "assistant";
|
|
62
|
-
}
|
|
80
|
+
}>, fold?: FoldInfo | undefined): void;
|
|
63
81
|
invalidate(): void;
|
|
64
82
|
render(width: number): string[];
|
|
65
83
|
}
|
|
@@ -67,9 +85,34 @@ export declare class ToolBlock implements Component {
|
|
|
67
85
|
item: Extract<TranscriptItem, {
|
|
68
86
|
kind: "tool";
|
|
69
87
|
}>;
|
|
88
|
+
/** Bumped by TranscriptSync when this row's content changes; render caches on it. */
|
|
89
|
+
version: number;
|
|
90
|
+
fold?: FoldInfo;
|
|
91
|
+
private markdown?;
|
|
92
|
+
private cache?;
|
|
70
93
|
constructor(item: Extract<TranscriptItem, {
|
|
71
94
|
kind: "tool";
|
|
72
95
|
}>);
|
|
96
|
+
update(item: Extract<TranscriptItem, {
|
|
97
|
+
kind: "tool";
|
|
98
|
+
}>, fold?: FoldInfo | undefined): void;
|
|
99
|
+
invalidate(): void;
|
|
100
|
+
render(width: number): string[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One collapsible row for a GROUPED batch of finished tool calls (e.g. `✓ Read File — 3 reads`).
|
|
104
|
+
* Collapsed it shows the aggregate header; expanded, each call with its status, duration,
|
|
105
|
+
* summary and capped preview/rich body. All members are terminal, so lines are cached per
|
|
106
|
+
* version/width/fold just like the other terminal rows.
|
|
107
|
+
*/
|
|
108
|
+
export declare class ToolGroupBlock implements Component {
|
|
109
|
+
group: GroupedTool;
|
|
110
|
+
/** Bumped by TranscriptSync when this row's content changes; render caches on it. */
|
|
111
|
+
version: number;
|
|
112
|
+
fold?: FoldInfo;
|
|
113
|
+
private cache?;
|
|
114
|
+
constructor(group: GroupedTool);
|
|
115
|
+
update(group: GroupedTool, fold?: FoldInfo | undefined): void;
|
|
73
116
|
invalidate(): void;
|
|
74
117
|
render(width: number): string[];
|
|
75
118
|
}
|
|
@@ -86,7 +129,31 @@ export declare class InfoBlock implements Component {
|
|
|
86
129
|
invalidate(): void;
|
|
87
130
|
render(width: number): string[];
|
|
88
131
|
}
|
|
89
|
-
export declare function componentFor(
|
|
132
|
+
export declare function componentFor(entry: TranscriptEntry): Component;
|
|
133
|
+
/**
|
|
134
|
+
* Keeps a container of transcript components in step with view-model items. Rows are keyed
|
|
135
|
+
* (stable per fold candidate: tool ids, group ids, append-only item indices) and reused across
|
|
136
|
+
* syncs; batch groups replace their members' singleton components with one row once all of them
|
|
137
|
+
* finish. Content versions bump only when a row's entry actually changed, so clock frames
|
|
138
|
+
* between events reuse each component's cached lines instead of re-rendering. `setFoldResolver`
|
|
139
|
+
* supplies the effective expanded state; the same resolution feeds `entryAt`, so keybindings
|
|
140
|
+
* and mouse clicks agree with the renderer.
|
|
141
|
+
*/
|
|
142
|
+
export declare class TranscriptSync {
|
|
143
|
+
readonly container: Container;
|
|
144
|
+
private rows;
|
|
145
|
+
private order;
|
|
146
|
+
private foldOf;
|
|
147
|
+
setFoldResolver(resolver: (candidate: FoldCandidate) => boolean): void;
|
|
148
|
+
sync(items: TranscriptItem[]): void;
|
|
149
|
+
/**
|
|
150
|
+
* Fold candidate at a transcript content row, when the click landed on the row's header
|
|
151
|
+
* (assistant entries start with a blank spacer line, so their first two rows count). Used by
|
|
152
|
+
* click-to-toggle; keyboard toggling uses `foldCandidates` instead and needs no heights.
|
|
153
|
+
*/
|
|
154
|
+
entryAt(contentY: number, width: number): FoldCandidate | undefined;
|
|
155
|
+
reset(): void;
|
|
156
|
+
}
|
|
90
157
|
/** Startup screen rendered by core for the current width (cached until the width changes). */
|
|
91
158
|
export declare class BannerBlock implements Component {
|
|
92
159
|
private renderLines;
|
|
@@ -95,13 +162,6 @@ export declare class BannerBlock implements Component {
|
|
|
95
162
|
invalidate(): void;
|
|
96
163
|
render(width: number): string[];
|
|
97
164
|
}
|
|
98
|
-
/** Keeps a container of transcript components in step with view-model items. */
|
|
99
|
-
export declare class TranscriptSync {
|
|
100
|
-
readonly container: Container;
|
|
101
|
-
private rendered;
|
|
102
|
-
sync(items: TranscriptItem[]): void;
|
|
103
|
-
reset(): void;
|
|
104
|
-
}
|
|
105
165
|
/** Renders one of several components (main conversation or a read-only child view). */
|
|
106
166
|
export declare class Switch implements Component {
|
|
107
167
|
private pick;
|