@antst/dashi 0.1.0-alpha.5

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/lib/application.d.ts +43 -0
  4. package/lib/application.js +260 -0
  5. package/lib/at-trigger.d.ts +8 -0
  6. package/lib/at-trigger.js +13 -0
  7. package/lib/catalogs.d.ts +23 -0
  8. package/lib/catalogs.js +218 -0
  9. package/lib/clipboard-image.d.ts +3 -0
  10. package/lib/clipboard-image.js +74 -0
  11. package/lib/completion-trigger.d.ts +2 -0
  12. package/lib/completion-trigger.js +7 -0
  13. package/lib/external-editor.d.ts +2 -0
  14. package/lib/external-editor.js +39 -0
  15. package/lib/history-records.d.ts +4 -0
  16. package/lib/history-records.js +14 -0
  17. package/lib/image-input.d.ts +8 -0
  18. package/lib/image-input.js +37 -0
  19. package/lib/index.d.ts +4 -0
  20. package/lib/index.js +250 -0
  21. package/lib/input.d.ts +10 -0
  22. package/lib/input.js +220 -0
  23. package/lib/mouse-input.d.ts +1 -0
  24. package/lib/mouse-input.js +6 -0
  25. package/lib/presentation.d.ts +9 -0
  26. package/lib/presentation.js +54 -0
  27. package/lib/renderer.d.ts +19 -0
  28. package/lib/renderer.js +414 -0
  29. package/lib/rewind.d.ts +18 -0
  30. package/lib/rewind.js +84 -0
  31. package/lib/session-export.d.ts +3 -0
  32. package/lib/session-export.js +27 -0
  33. package/lib/session-list.d.ts +15 -0
  34. package/lib/session-list.js +65 -0
  35. package/lib/session-runtime.d.ts +35 -0
  36. package/lib/session-runtime.js +1027 -0
  37. package/lib/shell-command.d.ts +9 -0
  38. package/lib/shell-command.js +62 -0
  39. package/lib/state.d.ts +493 -0
  40. package/lib/state.js +641 -0
  41. package/lib/terminal-guard.d.ts +14 -0
  42. package/lib/terminal-guard.js +76 -0
  43. package/lib/tool-presentation.d.ts +14 -0
  44. package/lib/tool-presentation.js +156 -0
  45. package/lib/transcript.d.ts +12 -0
  46. package/lib/transcript.js +304 -0
  47. package/lib/tui-root.d.ts +20 -0
  48. package/lib/tui-root.js +25 -0
  49. package/package.json +65 -0
  50. package/validated-dsh-versions.json +3 -0
@@ -0,0 +1,74 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { draftImage } from './image-input.js';
3
+ const APPLE_SCRIPT = `
4
+ ObjC.import('AppKit')
5
+ const pasteboard = $.NSPasteboard.generalPasteboard
6
+ let data = pasteboard.dataForType('public.png')
7
+ if (!data) {
8
+ const tiff = pasteboard.dataForType('public.tiff')
9
+ if (tiff) {
10
+ const image = $.NSBitmapImageRep.imageRepWithData(tiff)
11
+ if (image) data = image.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $())
12
+ }
13
+ }
14
+ if (!data) $.exit(2)
15
+ console.log(ObjC.unwrap(data.base64EncodedStringWithOptions(0)))
16
+ `.trim();
17
+ function capture(command, args, base64, path) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(command, args, {
20
+ ...(path === undefined ? {} : { env: { ...process.env, PATH: path } }),
21
+ stdio: ['ignore', 'pipe', 'ignore'],
22
+ });
23
+ const chunks = [];
24
+ child.stdout.on('data', (chunk) => { chunks.push(chunk); });
25
+ child.once('error', error => {
26
+ if (error.code === 'ENOENT')
27
+ resolve({ found: false });
28
+ else
29
+ reject(error);
30
+ });
31
+ child.once('close', code => {
32
+ if (code !== 0)
33
+ return resolve({ found: true });
34
+ const output = Buffer.concat(chunks);
35
+ if (output.length === 0)
36
+ return resolve({ found: true });
37
+ if (!base64)
38
+ return resolve({ found: true, output: new Uint8Array(output) });
39
+ try {
40
+ const decoded = Buffer.from(output.toString('utf8').trim(), 'base64');
41
+ resolve({ found: true, ...(decoded.length === 0 ? {} : { output: new Uint8Array(decoded) }) });
42
+ }
43
+ catch (error) {
44
+ reject(error);
45
+ }
46
+ });
47
+ });
48
+ }
49
+ /** Read a PNG from an installed platform clipboard helper. */
50
+ export async function readClipboardImage(platform = process.platform, path) {
51
+ const attempts = platform === 'darwin'
52
+ ? [
53
+ ['pngpaste', ['-'], false],
54
+ ['osascript', ['-l', 'JavaScript', '-e', APPLE_SCRIPT], true],
55
+ ]
56
+ : platform === 'linux'
57
+ ? [
58
+ ['wl-paste', ['--no-newline', '--type', 'image/png'], false],
59
+ ['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], false],
60
+ ]
61
+ : [];
62
+ let helperFound = false;
63
+ for (const [command, args, base64] of attempts) {
64
+ const result = await capture(command, args, base64, path);
65
+ helperFound ||= result.found;
66
+ if (result.output !== undefined)
67
+ return draftImage(result.output, 'image/png', 'clipboard.png', 'clipboard');
68
+ }
69
+ if (attempts.length === 0)
70
+ throw new Error('Clipboard image paste is supported on Linux and macOS.');
71
+ throw new Error(helperFound
72
+ ? 'The clipboard does not contain a PNG image.'
73
+ : `Clipboard image paste needs ${platform === 'darwin' ? 'pngpaste or osascript' : 'wl-paste or xclip'}.`);
74
+ }
@@ -0,0 +1,2 @@
1
+ export declare function slashCompletionQuery(input: string): string | undefined;
2
+ export declare function hasCompletionTrigger(input: string, caret: number): boolean;
@@ -0,0 +1,7 @@
1
+ import { detectAtTrigger } from './at-trigger.js';
2
+ export function slashCompletionQuery(input) {
3
+ return /^\/([a-z0-9_-]*)$/u.exec(input)?.[1];
4
+ }
5
+ export function hasCompletionTrigger(input, caret) {
6
+ return slashCompletionQuery(input) !== undefined || detectAtTrigger(input, caret) !== undefined;
7
+ }
@@ -0,0 +1,2 @@
1
+ /** Edit one draft through the user's terminal editor and always remove its file. */
2
+ export declare function editDraftExternally(text: string, cwd: string): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ function runEditor(command, file, cwd) {
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn('/bin/sh', ['-c', `exec ${command} "$1"`, 'dashi-editor', file], {
8
+ cwd, stdio: 'inherit',
9
+ });
10
+ child.once('error', reject);
11
+ child.once('exit', (code, signal) => {
12
+ if (code === 0)
13
+ resolve();
14
+ else
15
+ reject(new Error(`editor exited with ${code === null ? signal ?? 'unknown status' : `code ${String(code)}`}`));
16
+ });
17
+ });
18
+ }
19
+ /** Edit one draft through the user's terminal editor and always remove its file. */
20
+ export async function editDraftExternally(text, cwd) {
21
+ const command = process.env.VISUAL?.trim() || process.env.EDITOR?.trim();
22
+ if (command === undefined || command === '')
23
+ throw new Error('set $VISUAL or $EDITOR to use Ctrl+G');
24
+ const directory = await mkdtemp(join(tmpdir(), 'dashi-editor-'));
25
+ const file = join(directory, 'draft.md');
26
+ try {
27
+ await writeFile(file, text, { encoding: 'utf8', mode: 0o600 });
28
+ await runEditor(command, file, cwd);
29
+ return await readFile(file, 'utf8');
30
+ }
31
+ finally {
32
+ try {
33
+ await rm(file, { force: true });
34
+ }
35
+ finally {
36
+ await rmdir(directory);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { SessionHistoryRecord } from '@deepseek-ai/dsh-api-session-controller';
2
+ import { type SessionEvent } from '@deepseek-ai/dsh-session';
3
+ /** Expand the Controller's compact history wire records to canonical Session events. */
4
+ export declare function eventsFromRecords(records: readonly SessionHistoryRecord[]): SessionEvent[];
@@ -0,0 +1,14 @@
1
+ import { decodeStorageRecord } from '@deepseek-ai/dsh-session';
2
+ /** Expand the Controller's compact history wire records to canonical Session events. */
3
+ export function eventsFromRecords(records) {
4
+ return records.flatMap((record) => {
5
+ if (record.type === 'event')
6
+ return [record.event];
7
+ const encoded = record.event.type === 'chunkrow/text-chunks'
8
+ ? { type: 'text-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
9
+ : record.event.type === 'chunkrow/reasoning-chunks'
10
+ ? { type: 'reasoning-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
11
+ : { type: 'tool-call-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data };
12
+ return decodeStorageRecord(encoded);
13
+ });
14
+ }
@@ -0,0 +1,8 @@
1
+ import type { AttachmentStore, EncodedImageAttachment, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
2
+ import type { DraftAttachment } from './state.js';
3
+ export declare function isImagePath(path: string): boolean;
4
+ export declare function draftImage(data: Uint8Array, mediaType: ImageMediaType, name: string, path: string): DraftAttachment;
5
+ /** Snapshot explicit user paths into one disposable composer draft. */
6
+ export declare function readDraftImages(cwd: string, paths: readonly string[]): Promise<DraftAttachment[]>;
7
+ export declare function encodeDraftImages(images: readonly DraftAttachment[]): EncodedImageAttachment[];
8
+ export declare function admitDraftImages(attachments: AttachmentStore, images: readonly DraftAttachment[]): Promise<readonly ImageAttachmentRef[]>;
@@ -0,0 +1,37 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename, extname, resolve } from 'node:path';
3
+ const MEDIA_TYPES = {
4
+ '.gif': 'image/gif',
5
+ '.jpeg': 'image/jpeg',
6
+ '.jpg': 'image/jpeg',
7
+ '.png': 'image/png',
8
+ '.webp': 'image/webp',
9
+ };
10
+ export function isImagePath(path) {
11
+ return MEDIA_TYPES[extname(path).toLocaleLowerCase()] !== undefined;
12
+ }
13
+ export function draftImage(data, mediaType, name, path) {
14
+ return { bytes: data.byteLength, data, mediaType, name, path };
15
+ }
16
+ /** Snapshot explicit user paths into one disposable composer draft. */
17
+ export async function readDraftImages(cwd, paths) {
18
+ return Promise.all(paths.map(async (path) => {
19
+ const absolute = resolve(cwd, path);
20
+ const mediaType = MEDIA_TYPES[extname(absolute).toLocaleLowerCase()];
21
+ if (mediaType === undefined) {
22
+ throw new Error(`unsupported image extension for ${path}; accepted: .png, .jpg, .jpeg, .webp, .gif`);
23
+ }
24
+ const data = new Uint8Array(await readFile(absolute));
25
+ return draftImage(data, mediaType, basename(absolute), absolute);
26
+ }));
27
+ }
28
+ export function encodeDraftImages(images) {
29
+ return images.map(image => ({
30
+ data: Buffer.from(image.data).toString('base64'), mediaType: image.mediaType, name: image.name,
31
+ }));
32
+ }
33
+ export function admitDraftImages(attachments, images) {
34
+ return attachments.saveImages(images.map(image => ({
35
+ data: image.data, mediaType: image.mediaType, name: image.name,
36
+ })));
37
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "dashi";
3
+ export declare const inject: string[];
4
+ export declare function apply(ctx: Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,250 @@
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { exitOnStdinEnd } from '@deepseek-ai/dsh-cmdline';
5
+ import { createTerminalShell } from './application.js';
6
+ import { sessionOverlay } from './catalogs.js';
7
+ import { readClipboardImage } from './clipboard-image.js';
8
+ import { editDraftExternally } from './external-editor.js';
9
+ import { formatSessionList, isSessionId, parseSessionListArgs, sessionMatches, sessionNotFound, } from './session-list.js';
10
+ import { createSessionRuntime } from './session-runtime.js';
11
+ import { TuiRoot } from './tui-root.js';
12
+ export const name = 'dashi';
13
+ export const inject = [
14
+ 'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', 'permissionPresets', 'sessionController',
15
+ 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'shell', 'skills', 'tools',
16
+ ];
17
+ const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
18
+ function parseArgs(args) {
19
+ let accessible = false;
20
+ let inline = true;
21
+ let name;
22
+ let resume;
23
+ let resumeAll = false;
24
+ let resumePicker = false;
25
+ let useContinue = false;
26
+ const images = [];
27
+ const prompt = [];
28
+ for (let index = 0; index < args.length; index++) {
29
+ const argument = args[index];
30
+ if (argument === '--accessible')
31
+ accessible = true;
32
+ else if (argument === '--inline')
33
+ inline = true;
34
+ else if (argument === '--fullscreen')
35
+ inline = false;
36
+ else if (argument === '--name' || argument === '--image') {
37
+ const value = args[++index];
38
+ if (value === undefined || value === '')
39
+ return undefined;
40
+ if (argument === '--name')
41
+ name = value;
42
+ else
43
+ images.push(value);
44
+ }
45
+ else if (argument === '--resume' || argument === '-r') {
46
+ const value = args[index + 1];
47
+ if (value === '')
48
+ return undefined;
49
+ if (value === undefined || value.startsWith('-'))
50
+ resumePicker = true;
51
+ else
52
+ resume = args[++index];
53
+ }
54
+ else if (argument === '--all')
55
+ resumeAll = true;
56
+ else if (argument === '--continue' || argument === '-c')
57
+ useContinue = true;
58
+ else if (argument?.startsWith('-') === true)
59
+ return undefined;
60
+ else if (argument !== undefined)
61
+ prompt.push(argument);
62
+ }
63
+ if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
64
+ || name !== undefined && (resume !== undefined || resumePicker || useContinue)
65
+ || resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue)
66
+ return undefined;
67
+ return {
68
+ accessible,
69
+ continue: useContinue,
70
+ inline,
71
+ resumeAll,
72
+ resumePicker,
73
+ ...(images.length === 0 ? {} : { images }),
74
+ ...(name === undefined ? {} : { name }),
75
+ ...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
76
+ ...(resume === undefined ? {} : { resume }),
77
+ };
78
+ }
79
+ function packageVersion(path) {
80
+ const manifest = JSON.parse(readFileSync(path, 'utf8'));
81
+ return typeof manifest.version === 'string' ? manifest.version : 'unknown';
82
+ }
83
+ function runningDshVersions() {
84
+ try {
85
+ const entry = realpathSync(process.argv[1] ?? '');
86
+ const manifest = resolve(dirname(entry), '../package.json');
87
+ const version = packageVersion(manifest);
88
+ try {
89
+ return [version, packageVersion(createRequire(manifest).resolve('@deepseek-ai/dsh-base/package.json'))];
90
+ }
91
+ catch {
92
+ return [version, 'unknown'];
93
+ }
94
+ }
95
+ catch {
96
+ return ['unknown', 'unknown'];
97
+ }
98
+ }
99
+ function warnDshVersion() {
100
+ const [version, baseVersion] = runningDshVersions();
101
+ if (baseVersion !== version) {
102
+ process.stderr.write(`dashi: warning: DSH ${version} loads @deepseek-ai/dsh-base ${baseVersion}; versions must match\n`);
103
+ }
104
+ if (!validatedVersions.includes(version)) {
105
+ process.stderr.write(`dashi: warning: DSH ${version} is not validated; validated: ${validatedVersions.join(', ')}\n`);
106
+ }
107
+ }
108
+ function installSessionList(ctx, options, ready, exit) {
109
+ const abort = new AbortController();
110
+ let listing;
111
+ const announce = ready.onReady(() => {
112
+ warnDshVersion();
113
+ listing = ctx.sessionController.list({}, abort.signal).then(({ items }) => {
114
+ const cwd = options.all ? undefined : resolve(options.cwd ?? process.cwd());
115
+ process.stdout.write(`${formatSessionList(items, cwd, options.json)}\n`);
116
+ exit(0);
117
+ }).catch((error) => {
118
+ process.stderr.write(`dashi: sessions list failed: ${error instanceof Error ? error.message : String(error)}\n`);
119
+ exit(1);
120
+ });
121
+ });
122
+ ctx.effect(() => async () => {
123
+ announce();
124
+ abort.abort();
125
+ await listing;
126
+ }, 'dashi.sessions-list');
127
+ }
128
+ export function apply(ctx) {
129
+ const exit = ctx.get('appExit');
130
+ const ready = ctx.get('appReady');
131
+ const cmdline = ctx.get('cmdlineArgs');
132
+ if (exit === undefined || ready === undefined || cmdline === undefined) {
133
+ throw new Error('dashi: the launcher must provide ctx.appExit, ctx.appReady, and ctx.cmdlineArgs');
134
+ }
135
+ const args = cmdline.get();
136
+ if (args[0] === 'sessions') {
137
+ const options = parseSessionListArgs(args);
138
+ if (options === undefined) {
139
+ process.stderr.write(`dashi: unsupported arguments: ${args.join(' ')}\n`);
140
+ exit(2);
141
+ return;
142
+ }
143
+ installSessionList(ctx, options, ready, exit);
144
+ return;
145
+ }
146
+ const parsed = parseArgs(args);
147
+ if (parsed === undefined) {
148
+ process.stderr.write(`dashi: unsupported arguments: ${args.join(' ')}\n`);
149
+ exit(2);
150
+ return;
151
+ }
152
+ const tuiRoot = new TuiRoot(ctx);
153
+ let shell;
154
+ let runtime;
155
+ let boot;
156
+ const stdoutError = (error) => { void shell?.fail(error); };
157
+ process.stdout.on('error', stdoutError);
158
+ const changed = ctx.on('tui/root-changed', (_previous, current) => {
159
+ if (current === undefined)
160
+ shell?.dispatch({ type: 'root-cleared' });
161
+ });
162
+ const announce = ready.onReady(() => {
163
+ warnDshVersion();
164
+ boot = (async () => {
165
+ const cwd = process.cwd();
166
+ const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
167
+ cancel: id => { shell?.cancelDecision(id); },
168
+ request: decision => shell?.requestDecision(decision)
169
+ ?? Promise.reject(new Error('dashi terminal is not ready')),
170
+ });
171
+ const sendPrompt = () => {
172
+ if (parsed.prompt !== undefined)
173
+ shell?.dispatch({ type: 'composer-changed', text: parsed.prompt });
174
+ if (parsed.prompt !== undefined)
175
+ shell?.dispatch({ type: 'submit' });
176
+ };
177
+ let pickerItems = parsed.resumePicker
178
+ ? (await ctx.sessionController.list({}, new AbortController().signal)).items : undefined;
179
+ if (parsed.resume !== undefined && !isSessionId(parsed.resume)) {
180
+ const matches = sessionMatches((await ctx.sessionController.list({}, new AbortController().signal)).items, parsed.resume, parsed.resumeAll ? undefined : cwd);
181
+ const [match] = matches;
182
+ if (match === undefined)
183
+ throw sessionNotFound(parsed.resume);
184
+ if (matches.length > 1)
185
+ pickerItems = matches;
186
+ else
187
+ runtime = await makeRuntime({ ...parsed, cwd, resume: String(match.sessionId) });
188
+ }
189
+ else if (!parsed.resumePicker)
190
+ runtime = await makeRuntime({ ...parsed, cwd });
191
+ const initial = runtime;
192
+ shell = createTerminalShell({
193
+ accessible: parsed.accessible,
194
+ activateOverlay: async (value) => {
195
+ if (runtime !== undefined)
196
+ return runtime.activate(value);
197
+ if (value.kind !== 'resume')
198
+ return;
199
+ runtime = await makeRuntime({ ...parsed, continue: false, cwd, resume: value.sessionId });
200
+ shell?.dispatch({ type: 'root-bound', cells: runtime.initialCells, hasMore: runtime.initialHasMore,
201
+ prompts: runtime.initialPrompts, root: runtime.root });
202
+ for (const attachment of runtime.initialAttachments)
203
+ shell?.dispatch({ type: 'attachment-added', attachment });
204
+ runtime.start();
205
+ sendPrompt();
206
+ },
207
+ attach: path => runtime?.attach(path) ?? Promise.resolve(undefined),
208
+ beforeExit: () => runtime?.shutdown() ?? Promise.resolve(),
209
+ complete: (query, caret) => runtime?.complete(query, caret) ?? Promise.resolve([]),
210
+ cwd,
211
+ cyclePermission: () => runtime?.cyclePermission() ?? Promise.resolve(),
212
+ exit,
213
+ externalEdit: editDraftExternally,
214
+ ...(initial === undefined ? {} : {
215
+ initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
216
+ initialPrompts: initial.initialPrompts, initialRoot: initial.root,
217
+ }),
218
+ inline: parsed.inline || parsed.accessible,
219
+ interrupt: () => { runtime?.interrupt(); },
220
+ loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
221
+ onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
222
+ pasteImage: readClipboardImage,
223
+ search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
224
+ submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
225
+ });
226
+ shell.start();
227
+ if (pickerItems !== undefined) {
228
+ shell.dispatch({ type: 'open-overlay', overlay: sessionOverlay(pickerItems, '', parsed.resumeAll ? undefined : cwd) });
229
+ }
230
+ else {
231
+ runtime?.start();
232
+ sendPrompt();
233
+ }
234
+ })().catch(async (error) => {
235
+ process.stderr.write(`dashi: startup failed: ${error instanceof Error ? error.message : String(error)}\n`);
236
+ await runtime?.shutdown();
237
+ await shell?.dispose();
238
+ exit(1);
239
+ });
240
+ });
241
+ ctx.effect(() => async () => {
242
+ announce();
243
+ changed();
244
+ await boot;
245
+ await runtime?.shutdown();
246
+ await shell?.dispose();
247
+ process.stdout.off('error', stdoutError);
248
+ }, 'dashi.profile-boot');
249
+ exitOnStdinEnd(ctx, 'dashi.stdin');
250
+ }
package/lib/input.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type TUI } from '@earendil-works/pi-tui';
2
+ import type { ComposerCursor, UiAction, ViewState } from './state.js';
3
+ export interface InputBindings {
4
+ readonly dispatch: (action: UiAction) => void;
5
+ readonly insertNewline?: () => void;
6
+ readonly readComposerCursor?: () => ComposerCursor;
7
+ readonly readState: () => ViewState;
8
+ }
9
+ export declare function caretOffset(text: string, cursor: ComposerCursor): number;
10
+ export declare function installInput(tui: TUI, bindings: InputBindings): () => void;