talon-agent 4.3.1 → 4.3.3
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/package.json +1 -1
- package/src/app.ts +1 -1
- package/src/backend/claude-sdk/doctor.ts +1 -1
- package/src/backend/codex/doctor.ts +1 -1
- package/src/backend/openai-agents/doctor.ts +1 -1
- package/src/bootstrap.ts +3 -3
- package/src/cli/doctor.ts +1 -1
- package/src/cli/skill.ts +2 -2
- package/src/core/agent-runtime/backend-registry.ts +1 -1
- package/src/core/auth/expiry-monitor.ts +1 -1
- package/src/core/background/cron-spec.ts +1 -1
- package/src/core/background/cron.ts +1 -1
- package/src/core/background/heartbeat/agent.ts +1 -1
- package/src/core/background/triggers/command.ts +1 -1
- package/src/core/background/triggers/exit.ts +1 -1
- package/src/core/background/triggers/output.ts +1 -1
- package/src/core/background/triggers/resume.ts +1 -1
- package/src/core/background/triggers/spawn.ts +1 -1
- package/src/core/{doctor.ts → doctor/index.ts} +11 -11
- package/src/core/engine/gateway-actions/cron.ts +2 -2
- package/src/core/engine/gateway-actions/goals.ts +2 -2
- package/src/core/engine/gateway-actions/scripts.ts +1 -1
- package/src/core/engine/gateway-actions/skills.ts +1 -1
- package/src/core/engine/gateway-actions/triggers.ts +2 -2
- package/src/core/engine/gateway-actions/{shared.ts → validation.ts} +2 -2
- package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
- package/src/core/plugin/native-runtimes.ts +1 -1
- package/src/core/prompt/assemble.ts +2 -2
- package/src/core/scripts/runner.ts +1 -1
- package/src/core/soul/forgetting.ts +2 -2
- package/src/core/soul/salience.ts +8 -4
- package/src/frontend/discord/actions/{shared.ts → buttons.ts} +3 -56
- package/src/frontend/discord/actions/channels.ts +34 -0
- package/src/frontend/discord/actions/chat-info.ts +1 -1
- package/src/frontend/discord/actions/index.ts +1 -1
- package/src/frontend/discord/actions/media.ts +1 -1
- package/src/frontend/discord/actions/messaging.ts +4 -2
- package/src/frontend/discord/actions/try-action.ts +24 -0
- package/src/frontend/discord/admin.ts +2 -2
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/callbacks/modals.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +3 -3
- package/src/frontend/discord/commands/info.ts +2 -2
- package/src/frontend/discord/commands/{shared.ts → interaction.ts} +2 -1
- package/src/frontend/discord/commands/router.ts +1 -1
- package/src/frontend/discord/commands/session.ts +2 -2
- package/src/frontend/discord/commands/settings.ts +2 -2
- package/src/frontend/discord/{helpers.ts → render.ts} +3 -2
- package/src/frontend/native/extensions.ts +1 -1
- package/src/frontend/teams/chat-discovery.ts +69 -0
- package/src/frontend/teams/commands.ts +141 -0
- package/src/frontend/teams/index.ts +24 -350
- package/src/frontend/teams/outbound.ts +44 -0
- package/src/frontend/teams/poll.ts +101 -0
- package/src/frontend/teams/runtime.ts +51 -0
- package/src/frontend/teams/turn.ts +97 -0
- package/src/frontend/telegram/actions/index.ts +1 -1
- package/src/frontend/telegram/actions/messaging.ts +1 -1
- package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
- package/src/frontend/telegram/actions/moderation/index.ts +74 -0
- package/src/frontend/telegram/actions/moderation/members.ts +77 -0
- package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
- package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
- package/src/frontend/telegram/actions/moderation/types.ts +16 -0
- package/src/frontend/telegram/actions/shared.ts +4 -4
- package/src/frontend/telegram/admin/background.ts +66 -0
- package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
- package/src/frontend/telegram/admin/health.ts +81 -0
- package/src/frontend/telegram/admin/sessions.ts +105 -0
- package/src/frontend/telegram/admin.ts +56 -261
- package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
- package/src/frontend/telegram/callbacks/model/control.ts +29 -0
- package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
- package/src/frontend/telegram/callbacks/model/select.ts +100 -0
- package/src/frontend/telegram/callbacks/model/types.ts +39 -0
- package/src/frontend/telegram/callbacks/model/views.ts +116 -0
- package/src/frontend/telegram/callbacks/model.ts +61 -331
- package/src/frontend/telegram/commands/admin.ts +108 -77
- package/src/frontend/telegram/commands/settings.ts +65 -43
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
- package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
- package/src/frontend/telegram/middleware.ts +166 -201
- package/src/frontend/telegram/sticker-library.ts +2 -2
- package/src/frontend/terminal/builtins/context.ts +184 -0
- package/src/frontend/terminal/builtins/help.ts +35 -0
- package/src/frontend/terminal/builtins/index.ts +23 -0
- package/src/frontend/terminal/builtins/model.ts +198 -0
- package/src/frontend/terminal/builtins/session.ts +101 -0
- package/src/frontend/terminal/builtins/status.ts +202 -0
- package/src/frontend/terminal/command-registry.ts +80 -0
- package/src/frontend/terminal/commands.ts +11 -674
- package/src/frontend/terminal/input/history.ts +64 -0
- package/src/frontend/terminal/input/keys.ts +143 -0
- package/src/frontend/terminal/input/parts.ts +33 -0
- package/src/frontend/terminal/input/paste.ts +53 -0
- package/src/frontend/terminal/input/render.ts +42 -0
- package/src/frontend/terminal/input/state.ts +60 -0
- package/src/frontend/terminal/input.ts +27 -342
- package/src/frontend/whatsapp/actions/account.ts +3 -6
- package/src/frontend/whatsapp/actions/chat-info.ts +2 -1
- package/src/frontend/whatsapp/actions/coerce.ts +30 -0
- package/src/frontend/whatsapp/actions/history.ts +1 -1
- package/src/frontend/whatsapp/actions/media-upload.ts +82 -0
- package/src/frontend/whatsapp/actions/media.ts +3 -8
- package/src/frontend/whatsapp/actions/messaging.ts +2 -7
- package/src/frontend/whatsapp/actions/moderation.ts +2 -1
- package/src/frontend/whatsapp/actions/{shared.ts → send.ts} +4 -124
- package/src/frontend/whatsapp/actions/try-action.ts +24 -0
- package/src/frontend/whatsapp/commands.ts +1 -1
- package/src/frontend/whatsapp/connection.ts +1 -1
- package/src/frontend/whatsapp/inbound.ts +1 -1
- package/src/frontend/whatsapp/index.ts +2 -2
- package/src/frontend/whatsapp/pairing-service.ts +1 -1
- package/src/plugins/github/provision.ts +1 -1
- package/src/plugins/mempalace/provision.ts +1 -1
- package/src/plugins/playwright/provision.ts +1 -1
- package/src/storage/repositories/cron-repo.ts +1 -1
- package/src/storage/repositories/goals-repo.ts +1 -1
- package/src/storage/repositories/scripts-repo.ts +1 -1
- package/src/storage/repositories/triggers-repo.ts +1 -1
- package/src/storage/sql/schema.sql +1 -1
- package/src/storage/sql/statements.generated.ts +2 -2
- package/src/storage/sql/triggers.sql +1 -1
- package/src/frontend/telegram/actions/moderation.ts +0 -255
- /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
- /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
- /package/src/storage/{cron-store.ts → cron.ts} +0 -0
- /package/src/storage/{goal-store.ts → goals.ts} +0 -0
- /package/src/storage/{scheduled-store.ts → scheduled.ts} +0 -0
- /package/src/storage/{script-store.ts → scripts.ts} +0 -0
- /package/src/storage/{skill-store.ts → skills.ts} +0 -0
- /package/src/storage/{sticker-store.ts → stickers.ts} +0 -0
- /package/src/storage/{trigger-store.ts → triggers.ts} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cloneParts, partsText, type InputPart } from "./parts.js";
|
|
2
|
+
|
|
3
|
+
const MAX_HISTORY_ITEMS = 100;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-process prompt history. Keeping this out of the persistent stores avoids
|
|
7
|
+
* creating a second plaintext transcript while still giving terminal users
|
|
8
|
+
* the expected Up/Down workflow.
|
|
9
|
+
*/
|
|
10
|
+
export class PromptHistory {
|
|
11
|
+
private readonly entries: InputPart[][] = [];
|
|
12
|
+
private cursor = 0;
|
|
13
|
+
private draft: InputPart[] | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly maxItems = MAX_HISTORY_ITEMS) {}
|
|
16
|
+
|
|
17
|
+
add(parts: readonly InputPart[]): void {
|
|
18
|
+
const text = partsText(parts);
|
|
19
|
+
if (!text) return;
|
|
20
|
+
|
|
21
|
+
const latest = this.entries.at(-1);
|
|
22
|
+
if (!latest || partsText(latest) !== text) {
|
|
23
|
+
this.entries.push(cloneParts(parts));
|
|
24
|
+
if (this.entries.length > this.maxItems) this.entries.shift();
|
|
25
|
+
}
|
|
26
|
+
this.resetNavigation();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
move(
|
|
30
|
+
direction: "previous" | "next",
|
|
31
|
+
current: readonly InputPart[],
|
|
32
|
+
): InputPart[] | undefined {
|
|
33
|
+
if (this.entries.length === 0) return undefined;
|
|
34
|
+
|
|
35
|
+
if (direction === "previous") {
|
|
36
|
+
if (this.cursor === this.entries.length) {
|
|
37
|
+
this.draft = cloneParts(current);
|
|
38
|
+
}
|
|
39
|
+
if (this.cursor > 0) this.cursor--;
|
|
40
|
+
return cloneParts(this.entries[this.cursor]!);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.cursor === this.entries.length) return undefined;
|
|
44
|
+
if (this.cursor < this.entries.length - 1) {
|
|
45
|
+
this.cursor++;
|
|
46
|
+
return cloneParts(this.entries[this.cursor]!);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.cursor = this.entries.length;
|
|
50
|
+
return cloneParts(this.draft ?? [{ type: "text", content: "" }]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Preserve an edited recalled prompt as the current draft. */
|
|
54
|
+
detach(parts: readonly InputPart[]): void {
|
|
55
|
+
if (this.cursor === this.entries.length) return;
|
|
56
|
+
this.cursor = this.entries.length;
|
|
57
|
+
this.draft = cloneParts(parts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
resetNavigation(): void {
|
|
61
|
+
this.cursor = this.entries.length;
|
|
62
|
+
this.draft = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key dispatch — one handler per control code. Anything not in the table is
|
|
3
|
+
* either a stray control character (dropped) or a printable one (typed).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ensureTrailingText, lastPart } from "./parts.js";
|
|
7
|
+
import { redraw } from "./render.js";
|
|
8
|
+
import { clearInput, fullText, submitInput, type InputState } from "./state.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles the key at `chunk[i]`. Returns the index of the last byte it
|
|
12
|
+
* consumed, so a multi-byte escape sequence can skip its tail.
|
|
13
|
+
*/
|
|
14
|
+
type KeyHandler = (state: InputState, chunk: string, i: number) => number;
|
|
15
|
+
|
|
16
|
+
function insertText(state: InputState, text: string): void {
|
|
17
|
+
ensureTrailingText(state.parts).content += text;
|
|
18
|
+
state.history.detach(state.parts);
|
|
19
|
+
redraw(state);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function navigateHistory(
|
|
23
|
+
state: InputState,
|
|
24
|
+
direction: "previous" | "next",
|
|
25
|
+
): void {
|
|
26
|
+
const recalled = state.history.move(direction, state.parts);
|
|
27
|
+
if (!recalled) return;
|
|
28
|
+
state.parts = recalled;
|
|
29
|
+
redraw(state);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function onCtrlC(): number {
|
|
33
|
+
process.stdout.write("\n\x1b[?2004l");
|
|
34
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onCtrlU(state: InputState, _chunk: string, i: number): number {
|
|
39
|
+
clearInput(state);
|
|
40
|
+
redraw(state);
|
|
41
|
+
return i;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onEnter(state: InputState, _chunk: string, i: number): number {
|
|
45
|
+
if (fullText(state)) {
|
|
46
|
+
submitInput(state);
|
|
47
|
+
} else {
|
|
48
|
+
process.stdout.write("\n");
|
|
49
|
+
redraw(state);
|
|
50
|
+
}
|
|
51
|
+
return i;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function onBackspace(state: InputState, _chunk: string, i: number): number {
|
|
55
|
+
const parts = state.parts;
|
|
56
|
+
const last = lastPart(parts);
|
|
57
|
+
if (last.type === "text" && last.content.length > 0) {
|
|
58
|
+
// Delete last char from text
|
|
59
|
+
last.content = last.content.slice(0, -1);
|
|
60
|
+
} else if (last.type === "text" && last.content === "" && parts.length > 1) {
|
|
61
|
+
// Empty trailing text — remove it, then remove the paste before it
|
|
62
|
+
parts.pop();
|
|
63
|
+
parts.pop();
|
|
64
|
+
// Ensure we always have at least one text part
|
|
65
|
+
if (parts.length === 0) parts.push({ type: "text", content: "" });
|
|
66
|
+
ensureTrailingText(parts);
|
|
67
|
+
} else if (last.type === "paste") {
|
|
68
|
+
// Remove the paste block
|
|
69
|
+
parts.pop();
|
|
70
|
+
if (parts.length === 0) parts.push({ type: "text", content: "" });
|
|
71
|
+
ensureTrailingText(parts);
|
|
72
|
+
}
|
|
73
|
+
state.history.detach(parts);
|
|
74
|
+
redraw(state);
|
|
75
|
+
return i;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onEscape(state: InputState, chunk: string, i: number): number {
|
|
79
|
+
if (i + 1 < chunk.length && chunk[i + 1] === "[") {
|
|
80
|
+
// CSI escape sequence. Up/Down navigate history; the rest are
|
|
81
|
+
// ignored until cursor editing is implemented.
|
|
82
|
+
let end = i + 2;
|
|
83
|
+
while (end < chunk.length && chunk.charCodeAt(end) < 0x40) end++;
|
|
84
|
+
const final = chunk[end];
|
|
85
|
+
if (final === "A") navigateHistory(state, "previous");
|
|
86
|
+
if (final === "B") navigateHistory(state, "next");
|
|
87
|
+
return end;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
i + 2 < chunk.length &&
|
|
91
|
+
chunk[i + 1] === "O" &&
|
|
92
|
+
(chunk[i + 2] === "A" || chunk[i + 2] === "B")
|
|
93
|
+
) {
|
|
94
|
+
// Some terminals use application-cursor sequences (ESC O A/B).
|
|
95
|
+
navigateHistory(state, chunk[i + 2] === "A" ? "previous" : "next");
|
|
96
|
+
return i + 2;
|
|
97
|
+
}
|
|
98
|
+
if (state.pendingResolve) {
|
|
99
|
+
// Bare Escape during waitForInput — cancel
|
|
100
|
+
const resolve = state.pendingResolve;
|
|
101
|
+
state.pendingResolve = null;
|
|
102
|
+
clearInput(state);
|
|
103
|
+
process.stdout.write("\n");
|
|
104
|
+
resolve("");
|
|
105
|
+
}
|
|
106
|
+
return i;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onTab(state: InputState, _chunk: string, i: number): number {
|
|
110
|
+
insertText(state, " ");
|
|
111
|
+
return i;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Control code → handler. Enter and Backspace each answer to two codes. */
|
|
115
|
+
export const KEY_HANDLERS: Record<number, KeyHandler> = Object.assign(
|
|
116
|
+
Object.create(null),
|
|
117
|
+
{
|
|
118
|
+
0x03: onCtrlC, // Ctrl+C
|
|
119
|
+
0x15: onCtrlU, // Ctrl+U
|
|
120
|
+
0x0d: onEnter, // Enter (CR)
|
|
121
|
+
0x0a: onEnter, // Enter (LF)
|
|
122
|
+
0x7f: onBackspace, // Backspace (DEL)
|
|
123
|
+
0x08: onBackspace, // Backspace (BS)
|
|
124
|
+
0x1b: onEscape, // Escape / CSI
|
|
125
|
+
0x09: onTab, // Tab
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/** Normal (non-paste) input: dispatch each key in the chunk. */
|
|
130
|
+
export function handleKeyChunk(state: InputState, chunk: string): void {
|
|
131
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
132
|
+
const code = chunk.charCodeAt(i);
|
|
133
|
+
const handler = KEY_HANDLERS[code];
|
|
134
|
+
if (handler) {
|
|
135
|
+
i = handler(state, chunk, i);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (code < 0x20) continue;
|
|
139
|
+
|
|
140
|
+
// Printable char
|
|
141
|
+
insertText(state, chunk[i]!);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input parts — the ordered list of typed text segments and collapsed paste
|
|
3
|
+
* blocks that makes up the current line.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type InputPart =
|
|
7
|
+
{ type: "text"; content: string } | { type: "paste"; content: string };
|
|
8
|
+
type TextPart = Extract<InputPart, { type: "text" }>;
|
|
9
|
+
export type PastePart = Extract<InputPart, { type: "paste" }>;
|
|
10
|
+
|
|
11
|
+
export function cloneParts(parts: readonly InputPart[]): InputPart[] {
|
|
12
|
+
return parts.map((part) => ({ ...part }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function partsText(parts: readonly InputPart[]): string {
|
|
16
|
+
return parts
|
|
17
|
+
.map((part) => part.content)
|
|
18
|
+
.join("\n")
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function lastPart(parts: InputPart[]): InputPart {
|
|
23
|
+
return parts[parts.length - 1]!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Ensure the last part is a text part (for typing into). */
|
|
27
|
+
export function ensureTrailingText(parts: InputPart[]): TextPart {
|
|
28
|
+
const last = lastPart(parts);
|
|
29
|
+
if (last.type === "text") return last;
|
|
30
|
+
const t: TextPart = { type: "text", content: "" };
|
|
31
|
+
parts.push(t);
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Bracketed paste — accumulate between the start/end markers, then collapse. */
|
|
2
|
+
|
|
3
|
+
import { ensureTrailingText } from "./parts.js";
|
|
4
|
+
import { redraw } from "./render.js";
|
|
5
|
+
import type { InputState } from "./state.js";
|
|
6
|
+
|
|
7
|
+
const PASTE_COLLAPSE_LINES = 3;
|
|
8
|
+
const PASTE_COLLAPSE_CHARS = 150;
|
|
9
|
+
const PASTE_START = "\x1b[200~";
|
|
10
|
+
const PASTE_END = "\x1b[201~";
|
|
11
|
+
|
|
12
|
+
function handlePasteComplete(state: InputState, text: string): void {
|
|
13
|
+
const lineCount = text.split("\n").length;
|
|
14
|
+
if (lineCount >= PASTE_COLLAPSE_LINES || text.length > PASTE_COLLAPSE_CHARS) {
|
|
15
|
+
// Collapse into a paste part
|
|
16
|
+
state.parts.push({ type: "paste", content: text });
|
|
17
|
+
} else {
|
|
18
|
+
// Short paste — inline into current text part
|
|
19
|
+
ensureTrailingText(state.parts).content += text.replace(/\n/g, " ");
|
|
20
|
+
}
|
|
21
|
+
state.history.detach(state.parts);
|
|
22
|
+
redraw(state);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Feed a stdin chunk to the paste accumulator. Returns true when the chunk
|
|
27
|
+
* belonged to a paste (started one, continued one, or finished one) and so
|
|
28
|
+
* must not reach the key dispatcher.
|
|
29
|
+
*/
|
|
30
|
+
export function consumePasteChunk(state: InputState, chunk: string): boolean {
|
|
31
|
+
if (chunk.includes(PASTE_START)) {
|
|
32
|
+
state.inPaste = true;
|
|
33
|
+
state.pasteAccum = chunk.split(PASTE_START).slice(1).join(PASTE_START);
|
|
34
|
+
if (state.pasteAccum.includes(PASTE_END)) {
|
|
35
|
+
state.inPaste = false;
|
|
36
|
+
handlePasteComplete(state, state.pasteAccum.split(PASTE_END)[0]!);
|
|
37
|
+
state.pasteAccum = "";
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (state.inPaste) {
|
|
42
|
+
if (chunk.includes(PASTE_END)) {
|
|
43
|
+
state.pasteAccum += chunk.split(PASTE_END)[0]!;
|
|
44
|
+
state.inPaste = false;
|
|
45
|
+
handlePasteComplete(state, state.pasteAccum);
|
|
46
|
+
state.pasteAccum = "";
|
|
47
|
+
} else {
|
|
48
|
+
state.pasteAccum += chunk;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Drawing — rewrite the prompt line(s) in place. */
|
|
2
|
+
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import type { PastePart } from "./parts.js";
|
|
5
|
+
import type { InputState } from "./state.js";
|
|
6
|
+
|
|
7
|
+
function pasteTag(p: PastePart): string {
|
|
8
|
+
const lines = p.content.split("\n").length;
|
|
9
|
+
return lines > 1
|
|
10
|
+
? `[Pasted ~${lines} lines]`
|
|
11
|
+
: `[Pasted ${p.content.length} chars]`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Track how many visual rows the last render occupied.
|
|
16
|
+
* Move up to the first row, clear to end of screen, rewrite.
|
|
17
|
+
*/
|
|
18
|
+
export function redraw(state: InputState): void {
|
|
19
|
+
const cols = process.stdout.columns || 80;
|
|
20
|
+
|
|
21
|
+
// Build display string and measure visible length (strip ANSI)
|
|
22
|
+
let display = state.promptStr;
|
|
23
|
+
let visLen = 4; // " ❯ " = 4 visible chars
|
|
24
|
+
for (const p of state.parts) {
|
|
25
|
+
if (p.type === "text") {
|
|
26
|
+
display += p.content;
|
|
27
|
+
visLen += p.content.length;
|
|
28
|
+
} else {
|
|
29
|
+
const tag = pasteTag(p);
|
|
30
|
+
display += pc.dim(tag);
|
|
31
|
+
visLen += tag.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Move cursor to start of the previous render, clear to end of screen
|
|
36
|
+
if (state.prevRows > 1) {
|
|
37
|
+
process.stdout.write(`\x1b[${state.prevRows - 1}A`); // move up
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(`\r\x1b[J${display}\x1b[?25h`); // col 0, clear to EOS, write, show cursor
|
|
40
|
+
|
|
41
|
+
state.prevRows = Math.max(1, Math.ceil(visLen / cols));
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input state — everything the keypress, paste and render modules share,
|
|
3
|
+
* lifted out of the `createInput` closure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PromptHistory } from "./history.js";
|
|
7
|
+
import { partsText, type InputPart } from "./parts.js";
|
|
8
|
+
|
|
9
|
+
export type InputState = {
|
|
10
|
+
readonly promptStr: string;
|
|
11
|
+
lineCallback: ((text: string) => void) | null;
|
|
12
|
+
pendingResolve: ((value: string) => void) | null;
|
|
13
|
+
paused: boolean;
|
|
14
|
+
/** Input is an ordered list of parts. */
|
|
15
|
+
parts: InputPart[];
|
|
16
|
+
readonly history: PromptHistory;
|
|
17
|
+
/** Bracketed paste accumulation. */
|
|
18
|
+
inPaste: boolean;
|
|
19
|
+
pasteAccum: string;
|
|
20
|
+
/** How many visual rows the last render occupied. */
|
|
21
|
+
prevRows: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createInputState(promptStr: string): InputState {
|
|
25
|
+
return {
|
|
26
|
+
promptStr,
|
|
27
|
+
lineCallback: null,
|
|
28
|
+
pendingResolve: null,
|
|
29
|
+
paused: false,
|
|
30
|
+
parts: [{ type: "text", content: "" }],
|
|
31
|
+
history: new PromptHistory(),
|
|
32
|
+
inPaste: false,
|
|
33
|
+
pasteAccum: "",
|
|
34
|
+
prevRows: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function fullText(state: InputState): string {
|
|
39
|
+
return partsText(state.parts);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function clearInput(state: InputState): void {
|
|
43
|
+
state.parts = [{ type: "text", content: "" }];
|
|
44
|
+
state.history.resetNavigation();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function submitInput(state: InputState): void {
|
|
48
|
+
const text = fullText(state);
|
|
49
|
+
if (!state.pendingResolve) state.history.add(state.parts);
|
|
50
|
+
clearInput(state);
|
|
51
|
+
process.stdout.write("\n");
|
|
52
|
+
|
|
53
|
+
if (state.pendingResolve) {
|
|
54
|
+
const resolve = state.pendingResolve;
|
|
55
|
+
state.pendingResolve = null;
|
|
56
|
+
resolve(text);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (state.lineCallback) state.lineCallback(text);
|
|
60
|
+
}
|