mirai-cli 1.0.0
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/bin/config.ts +99 -0
- package/bin/mirai.js +17 -0
- package/bin/mirai.ts +4 -0
- package/bin/provider.ts +149 -0
- package/bin/router.ts +134 -0
- package/dist/mirai.mjs +28316 -0
- package/package.json +29 -0
- package/src/app/index.tsx +274 -0
- package/src/components/chat.tsx +254 -0
- package/src/components/dialog/help-dialog.tsx +101 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/dialog/provider.tsx +96 -0
- package/src/components/header/index.tsx +78 -0
- package/src/components/input/command-palette.tsx +129 -0
- package/src/components/input/commands.ts +46 -0
- package/src/components/input/index.tsx +284 -0
- package/src/components/matrix-rain/index.tsx +122 -0
- package/src/components/permission-modal.tsx +66 -0
- package/src/components/scroll-bar/index.tsx +56 -0
- package/src/components/status-bar/index.tsx +43 -0
- package/src/components/tool-result.tsx +11 -0
- package/src/hooks/use-chat.ts +208 -0
- package/src/hooks/use-mouse.tsx +121 -0
- package/src/hooks/use-permission.ts +35 -0
- package/src/hooks/use-runtime.ts +99 -0
- package/src/hooks/use-scroll-bar-drag.ts +115 -0
- package/src/hooks/use-scroll.ts +70 -0
- package/src/index.ts +39 -0
- package/src/renderers/builtins/BashResult.tsx +65 -0
- package/src/renderers/builtins/EditFileResult.tsx +69 -0
- package/src/renderers/builtins/GenericToolResult.tsx +39 -0
- package/src/renderers/builtins/GlobSearchResult.tsx +40 -0
- package/src/renderers/builtins/GrepSearchResult.tsx +49 -0
- package/src/renderers/builtins/ReadFileResult.tsx +54 -0
- package/src/renderers/builtins/WriteFileResult.tsx +24 -0
- package/src/renderers/constants.ts +7 -0
- package/src/renderers/register-builtins.ts +27 -0
- package/src/renderers/registry.ts +37 -0
- package/src/renderers/status.ts +22 -0
- package/src/renderers/utils.ts +70 -0
- package/src/services/hit-test.ts +49 -0
- package/src/services/mouse-input.ts +237 -0
- package/src/services/scroll-registry.ts +64 -0
- package/src/services/tui-permission-provider.ts +35 -0
- package/src/theme.ts +38 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerRenderer } from './registry.js';
|
|
2
|
+
|
|
3
|
+
export async function registerBuiltinRenderers(): Promise<void> {
|
|
4
|
+
const [
|
|
5
|
+
{ ReadFileResult },
|
|
6
|
+
{ WriteFileResult },
|
|
7
|
+
{ EditFileResult },
|
|
8
|
+
{ GlobSearchResult },
|
|
9
|
+
{ GrepSearchResult },
|
|
10
|
+
{ BashResult },
|
|
11
|
+
] = await Promise.all([
|
|
12
|
+
import('./builtins/ReadFileResult.js'),
|
|
13
|
+
import('./builtins/WriteFileResult.js'),
|
|
14
|
+
import('./builtins/EditFileResult.js'),
|
|
15
|
+
import('./builtins/GlobSearchResult.js'),
|
|
16
|
+
import('./builtins/GrepSearchResult.js'),
|
|
17
|
+
import('./builtins/BashResult.js'),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
registerRenderer('read_file', ReadFileResult);
|
|
21
|
+
registerRenderer('write_file', WriteFileResult);
|
|
22
|
+
registerRenderer('edit_file', EditFileResult);
|
|
23
|
+
registerRenderer('glob_search', GlobSearchResult);
|
|
24
|
+
registerRenderer('grep_search', GrepSearchResult);
|
|
25
|
+
registerRenderer('bash', BashResult);
|
|
26
|
+
registerRenderer('shell', BashResult);
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { ToolResultBlock } from '@mirai/core/types';
|
|
3
|
+
import { GenericToolResult } from './builtins/GenericToolResult.js';
|
|
4
|
+
|
|
5
|
+
export type ToolRenderer = ComponentType<{ block: ToolResultBlock }>;
|
|
6
|
+
|
|
7
|
+
const rendererRegistry = new Map<string, ToolRenderer>();
|
|
8
|
+
|
|
9
|
+
export function registerRenderer(
|
|
10
|
+
toolName: string,
|
|
11
|
+
renderer: ToolRenderer
|
|
12
|
+
): void {
|
|
13
|
+
if (rendererRegistry.has(toolName)) {
|
|
14
|
+
console.warn(
|
|
15
|
+
`[renderer] Overwriting existing renderer for "${toolName}"`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
rendererRegistry.set(toolName, renderer);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getRenderer(
|
|
22
|
+
toolName: string
|
|
23
|
+
): ToolRenderer | undefined {
|
|
24
|
+
return rendererRegistry.get(toolName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveRenderer(
|
|
28
|
+
toolName: string
|
|
29
|
+
): ToolRenderer {
|
|
30
|
+
return rendererRegistry.get(toolName) ?? GenericToolResult;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hasRenderer(
|
|
34
|
+
toolName: string
|
|
35
|
+
): boolean {
|
|
36
|
+
return rendererRegistry.has(toolName);
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { theme } from "../theme.js";
|
|
2
|
+
|
|
3
|
+
export const STATUS_CONFIG = {
|
|
4
|
+
success: {
|
|
5
|
+
icon: '✓',
|
|
6
|
+
color: theme.success.primary,
|
|
7
|
+
},
|
|
8
|
+
error: {
|
|
9
|
+
icon: '✗',
|
|
10
|
+
color: theme.error.primary,
|
|
11
|
+
},
|
|
12
|
+
timeout: {
|
|
13
|
+
icon: '⏱',
|
|
14
|
+
color: theme.warning.primary,
|
|
15
|
+
},
|
|
16
|
+
cancelled: {
|
|
17
|
+
icon: '⊘',
|
|
18
|
+
color: theme.info.primary,
|
|
19
|
+
},
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type ToolStatus = keyof typeof STATUS_CONFIG;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface TruncatedOutput {
|
|
2
|
+
content: string;
|
|
3
|
+
truncated: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function truncateOutputSimple(
|
|
7
|
+
content: string,
|
|
8
|
+
maxLines: number,
|
|
9
|
+
maxChars: number
|
|
10
|
+
): TruncatedOutput {
|
|
11
|
+
const original = content.trimEnd();
|
|
12
|
+
if (original.length === 0) {
|
|
13
|
+
return { content: '', truncated: false };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lines = original.split('\n');
|
|
17
|
+
const previewLines: string[] = [];
|
|
18
|
+
let usedChars = 0;
|
|
19
|
+
let truncated = false;
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
if (i >= maxLines) {
|
|
23
|
+
truncated = true;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
const newlineCost = i > 0 ? 1 : 0;
|
|
29
|
+
const available = maxChars - usedChars - newlineCost;
|
|
30
|
+
|
|
31
|
+
if (available <= 0) {
|
|
32
|
+
truncated = true;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (line.length > available) {
|
|
37
|
+
previewLines.push(line.slice(0, available) + '\u2026');
|
|
38
|
+
truncated = true;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
previewLines.push(line);
|
|
43
|
+
usedChars += newlineCost + line.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (lines.length > maxLines || usedChars >= maxChars) {
|
|
47
|
+
truncated = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: previewLines.join('\n'),
|
|
52
|
+
truncated,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function truncateForSummary(content: string, limit: number): string {
|
|
57
|
+
const trimmed = content.trim();
|
|
58
|
+
if (trimmed.length <= limit) {
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
return trimmed.slice(0, limit) + '\u2026';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function safeParseJson(content: string): unknown {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(content);
|
|
67
|
+
} catch {
|
|
68
|
+
return content;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface Rect {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function inRect(x: number, y: number, rect: Rect): boolean {
|
|
9
|
+
return (
|
|
10
|
+
x >= rect.left &&
|
|
11
|
+
x < rect.left + rect.width &&
|
|
12
|
+
y >= rect.top &&
|
|
13
|
+
y < rect.top + rect.height
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HitTestEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
zIndex: number;
|
|
20
|
+
order: number;
|
|
21
|
+
getRect: () => Rect | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hitTest(
|
|
25
|
+
x: number,
|
|
26
|
+
y: number,
|
|
27
|
+
entries: HitTestEntry[],
|
|
28
|
+
skipId?: string,
|
|
29
|
+
): string | null {
|
|
30
|
+
let best: HitTestEntry | null = null;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < entries.length; i++) {
|
|
33
|
+
const entry = entries[i];
|
|
34
|
+
if (entry.id === skipId) continue;
|
|
35
|
+
|
|
36
|
+
const rect = entry.getRect();
|
|
37
|
+
if (!rect || !inRect(x, y, rect)) continue;
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
!best ||
|
|
41
|
+
entry.zIndex > best.zIndex ||
|
|
42
|
+
(entry.zIndex === best.zIndex && entry.order > best.order)
|
|
43
|
+
) {
|
|
44
|
+
best = entry;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return best?.id ?? null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { PassThrough } from "stream";
|
|
2
|
+
|
|
3
|
+
// ─── State machine ──────────────────────────────────────────
|
|
4
|
+
// Only tracks the states needed for filtering SGR mouse sequences.
|
|
5
|
+
// GROUND → ESC → CSI is the only path we need to intercept.
|
|
6
|
+
const enum SeqState {
|
|
7
|
+
/** Normal text bytes, passed through immediately */
|
|
8
|
+
GROUND,
|
|
9
|
+
/** After receiving \x1b (ESC), waiting for next byte */
|
|
10
|
+
ESC,
|
|
11
|
+
/** After receiving \x1b[ (CSI prefix), accumulating params until final byte */
|
|
12
|
+
CSI,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── SGR mouse regex (matches complete CSI sequences only) ──
|
|
16
|
+
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
17
|
+
|
|
18
|
+
// CSI final bytes are in range 0x40-0x7e
|
|
19
|
+
const isFinal = (b: number) => b >= 0x40 && b <= 0x7e;
|
|
20
|
+
|
|
21
|
+
export interface MouseState {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
button: "left" | "middle" | "right" | "none";
|
|
25
|
+
isPressed: boolean;
|
|
26
|
+
wheelDelta: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Listener = (state: MouseState) => void;
|
|
30
|
+
|
|
31
|
+
class MouseInputService {
|
|
32
|
+
private onExitHandler: (() => void) | null = null;
|
|
33
|
+
private pass = new PassThrough();
|
|
34
|
+
private listeners = new Set<Listener>();
|
|
35
|
+
private _enabled = false;
|
|
36
|
+
private _state: MouseState = { x: 0, y: 0, button: "none", isPressed: false, wheelDelta: 0 };
|
|
37
|
+
|
|
38
|
+
// ── Tokenizer state ──
|
|
39
|
+
private seqState = SeqState.GROUND;
|
|
40
|
+
/** Buffer for the current escape sequence bytes (ESC + subsequent) */
|
|
41
|
+
private escSeq: number[] = [];
|
|
42
|
+
/** Buffer for normal text bytes accumulated between escapes */
|
|
43
|
+
private textBuf: number[] = [];
|
|
44
|
+
|
|
45
|
+
// ── Watchdog ──
|
|
46
|
+
private watchdogTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private readonly WATCHDOUT_MS = 50;
|
|
48
|
+
|
|
49
|
+
readonly stdin = this.pass;
|
|
50
|
+
|
|
51
|
+
get isTTY(): boolean {
|
|
52
|
+
return process.stdin.isTTY === true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get state(): MouseState {
|
|
56
|
+
return this._state;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
on(cb: Listener): () => void {
|
|
60
|
+
this.listeners.add(cb);
|
|
61
|
+
return () => this.listeners.delete(cb);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onExit(cb: () => void): void {
|
|
65
|
+
this.onExitHandler = cb;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
enable() {
|
|
69
|
+
if (this._enabled || !process.stdin.isTTY) return;
|
|
70
|
+
this._enabled = true;
|
|
71
|
+
|
|
72
|
+
process.stdin.setRawMode?.(true);
|
|
73
|
+
process.stdin.on("readable", this.onReadable);
|
|
74
|
+
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
disable() {
|
|
78
|
+
if (!this._enabled) return;
|
|
79
|
+
this._enabled = false;
|
|
80
|
+
|
|
81
|
+
this.stopWatchdog();
|
|
82
|
+
process.stdin.off("readable", this.onReadable);
|
|
83
|
+
process.stdin.setRawMode?.(false);
|
|
84
|
+
process.stdout.write("\x1b[?1002l\x1b[?1006l");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Readable handler ──
|
|
88
|
+
private onReadable = () => {
|
|
89
|
+
let chunk: Buffer;
|
|
90
|
+
while ((chunk = process.stdin.read()) !== null) {
|
|
91
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
92
|
+
this.feedByte(chunk[i]!);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Flush any text still in buffer so it reaches Ink promptly
|
|
96
|
+
this.flushText();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── Byte-at-a-time tokenizer ──
|
|
100
|
+
private feedByte(byte: number) {
|
|
101
|
+
if (byte === 0x03) {
|
|
102
|
+
this.disable();
|
|
103
|
+
this.onExitHandler?.();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (this.seqState) {
|
|
108
|
+
// ──── GROUND ────
|
|
109
|
+
case SeqState.GROUND: {
|
|
110
|
+
if (byte === 0x1b) {
|
|
111
|
+
this.flushText();
|
|
112
|
+
this.seqState = SeqState.ESC;
|
|
113
|
+
this.escSeq = [byte];
|
|
114
|
+
this.startWatchdog();
|
|
115
|
+
} else {
|
|
116
|
+
this.textBuf.push(byte);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ──── ESC ────
|
|
122
|
+
case SeqState.ESC: {
|
|
123
|
+
this.escSeq.push(byte);
|
|
124
|
+
if (byte === 0x5b /* [ */) {
|
|
125
|
+
this.seqState = SeqState.CSI;
|
|
126
|
+
// Watchdog already armed from ESC
|
|
127
|
+
} else {
|
|
128
|
+
this.flushEscSeq();
|
|
129
|
+
this.seqState = SeqState.GROUND;
|
|
130
|
+
this.stopWatchdog();
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ──── CSI ────
|
|
136
|
+
case SeqState.CSI: {
|
|
137
|
+
this.escSeq.push(byte);
|
|
138
|
+
// Restart watchdog on each byte so the timeout is measured from
|
|
139
|
+
// the most recent byte, not from when CSI was entered.
|
|
140
|
+
this.restartWatchdog();
|
|
141
|
+
if (isFinal(byte)) {
|
|
142
|
+
const seq = Buffer.from(this.escSeq);
|
|
143
|
+
const s = seq.toString("utf8");
|
|
144
|
+
const match = SGR_MOUSE_RE.exec(s);
|
|
145
|
+
|
|
146
|
+
if (match) {
|
|
147
|
+
const btn = parseInt(match[1]!, 10);
|
|
148
|
+
const x = parseInt(match[2]!, 10) - 1;
|
|
149
|
+
const y = parseInt(match[3]!, 10) - 1;
|
|
150
|
+
const isRelease = match[4] === "m";
|
|
151
|
+
const wheel = btn & 0b11000000;
|
|
152
|
+
|
|
153
|
+
if (wheel) {
|
|
154
|
+
if (isRelease) {
|
|
155
|
+
// skip wheel release events (no listener dispatch)
|
|
156
|
+
} else {
|
|
157
|
+
this._state = {
|
|
158
|
+
x,
|
|
159
|
+
y,
|
|
160
|
+
button: "none",
|
|
161
|
+
isPressed: false,
|
|
162
|
+
wheelDelta: btn === 64 ? 1 : -1,
|
|
163
|
+
};
|
|
164
|
+
for (const cb of this.listeners) cb(this._state);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
const btnId = btn & 0b11;
|
|
168
|
+
this._state = {
|
|
169
|
+
x,
|
|
170
|
+
y,
|
|
171
|
+
button:
|
|
172
|
+
btnId === 0 ? "left" : btnId === 1 ? "middle" : "right",
|
|
173
|
+
isPressed: !isRelease,
|
|
174
|
+
wheelDelta: 0,
|
|
175
|
+
};
|
|
176
|
+
for (const cb of this.listeners) cb(this._state);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
this.pass.write(seq);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.escSeq = [];
|
|
183
|
+
this.seqState = SeqState.GROUND;
|
|
184
|
+
this.stopWatchdog();
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Buffered writes ──
|
|
192
|
+
private flushText() {
|
|
193
|
+
if (this.textBuf.length > 0) {
|
|
194
|
+
this.pass.write(Buffer.from(this.textBuf));
|
|
195
|
+
this.textBuf = [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private flushEscSeq() {
|
|
200
|
+
if (this.escSeq.length > 0) {
|
|
201
|
+
this.pass.write(Buffer.from(this.escSeq));
|
|
202
|
+
this.escSeq = [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Watchdog (ESCDELAY) ──
|
|
207
|
+
private startWatchdog() {
|
|
208
|
+
if (this.watchdogTimer) clearTimeout(this.watchdogTimer);
|
|
209
|
+
this.watchdogTimer = setTimeout(this.onWatchdog, this.WATCHDOUT_MS);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private restartWatchdog() {
|
|
213
|
+
if (this.watchdogTimer) clearTimeout(this.watchdogTimer);
|
|
214
|
+
this.watchdogTimer = setTimeout(this.onWatchdog, this.WATCHDOUT_MS);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private stopWatchdog() {
|
|
218
|
+
if (this.watchdogTimer) {
|
|
219
|
+
clearTimeout(this.watchdogTimer);
|
|
220
|
+
this.watchdogTimer = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Fires when stdin has been quiet for WATCHDOG_MS while in ESC or CSI.
|
|
225
|
+
* Flushes the incomplete sequence as raw bytes (ESCDELAY handling)
|
|
226
|
+
* so a lone \x1b (Escape key) reaches Ink as a keypress. */
|
|
227
|
+
private onWatchdog = () => {
|
|
228
|
+
this.watchdogTimer = null;
|
|
229
|
+
|
|
230
|
+
if (this.seqState === SeqState.ESC || this.seqState === SeqState.CSI) {
|
|
231
|
+
this.flushEscSeq();
|
|
232
|
+
this.seqState = SeqState.GROUND;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const mouseInput = new MouseInputService();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { hitTest, type Rect, type HitTestEntry } from "./hit-test.js";
|
|
2
|
+
|
|
3
|
+
type ScrollHandler = (delta: number) => boolean;
|
|
4
|
+
|
|
5
|
+
export interface Region {
|
|
6
|
+
id: string;
|
|
7
|
+
zIndex?: number;
|
|
8
|
+
getRect: () => Rect | null;
|
|
9
|
+
onScroll: ScrollHandler;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface InternalRegion extends Region {
|
|
13
|
+
zIndex: number;
|
|
14
|
+
order: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ScrollRegistryService {
|
|
18
|
+
private regions = new Map<string, InternalRegion>();
|
|
19
|
+
private nextOrder = 0;
|
|
20
|
+
|
|
21
|
+
register(region: Region): () => void {
|
|
22
|
+
const entry: InternalRegion = {
|
|
23
|
+
...region,
|
|
24
|
+
zIndex: region.zIndex ?? 0,
|
|
25
|
+
order: this.nextOrder++,
|
|
26
|
+
};
|
|
27
|
+
this.regions.set(region.id, entry);
|
|
28
|
+
return () => this.regions.delete(region.id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
unregister(id: string): void {
|
|
32
|
+
this.regions.delete(id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dispatch(x: number, y: number, delta: number): boolean {
|
|
36
|
+
const entries: HitTestEntry[] = [];
|
|
37
|
+
for (const region of this.regions.values()) {
|
|
38
|
+
entries.push({
|
|
39
|
+
id: region.id,
|
|
40
|
+
zIndex: region.zIndex,
|
|
41
|
+
order: region.order,
|
|
42
|
+
getRect: region.getRect,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let skipId: string | undefined;
|
|
47
|
+
let handled = false;
|
|
48
|
+
|
|
49
|
+
for (;;) {
|
|
50
|
+
const id = hitTest(x, y, entries, skipId);
|
|
51
|
+
if (!id) break;
|
|
52
|
+
handled = true;
|
|
53
|
+
|
|
54
|
+
const region = this.regions.get(id)!;
|
|
55
|
+
const shouldBubble = region.onScroll(delta);
|
|
56
|
+
if (!shouldBubble) break;
|
|
57
|
+
skipId = id;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return handled;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const scrollRegistry = new ScrollRegistryService();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PermissionProvider, PermissionRequest, PermissionResponse } from "@mirai/permission";
|
|
2
|
+
|
|
3
|
+
export type PermissionListener = (request: PermissionRequest | null) => void;
|
|
4
|
+
|
|
5
|
+
export class TuiPermissionProvider implements PermissionProvider {
|
|
6
|
+
private pendingResolve: ((response: PermissionResponse) => void) | null = null;
|
|
7
|
+
private currentRequest: PermissionRequest | null = null;
|
|
8
|
+
private listener: PermissionListener | null = null;
|
|
9
|
+
|
|
10
|
+
onRequest(listener: PermissionListener): void {
|
|
11
|
+
this.listener = listener;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async requestPermission(req: PermissionRequest): Promise<PermissionResponse> {
|
|
15
|
+
this.currentRequest = req;
|
|
16
|
+
this.listener?.(req);
|
|
17
|
+
|
|
18
|
+
return new Promise<PermissionResponse>((resolve) => {
|
|
19
|
+
this.pendingResolve = resolve;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolve(response: PermissionResponse): void {
|
|
24
|
+
if (this.pendingResolve) {
|
|
25
|
+
this.pendingResolve(response);
|
|
26
|
+
this.pendingResolve = null;
|
|
27
|
+
this.currentRequest = null;
|
|
28
|
+
this.listener?.(null);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getCurrentRequest(): PermissionRequest | null {
|
|
33
|
+
return this.currentRequest;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════
|
|
2
|
+
Mirai Theme — Neon Matrix color tokens
|
|
3
|
+
═══════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
import { NEON_COLORS } from "@mirai/core/constants";
|
|
6
|
+
|
|
7
|
+
const [g1, g2, g3, g4] = NEON_COLORS;
|
|
8
|
+
// g1=#39ff14 (bright), g2=#00ff41, g3=#00d936, g4=#a3ff12 (yellow-green)
|
|
9
|
+
|
|
10
|
+
export const theme = {
|
|
11
|
+
/** Surfaces & backgrounds */
|
|
12
|
+
bg: {
|
|
13
|
+
surface: "#1a1b26",
|
|
14
|
+
alt: "#16161e",
|
|
15
|
+
code: "#24283b",
|
|
16
|
+
overlay: "#000000",
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
/** Text hierarchy */
|
|
20
|
+
text: {
|
|
21
|
+
primary: "#c0caf5",
|
|
22
|
+
dim: "#565f89",
|
|
23
|
+
muted: "#3b4261",
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
/** Semantic role colors */
|
|
27
|
+
role: {
|
|
28
|
+
user: "#7aa2f7",
|
|
29
|
+
assistant: "#9ece6a",
|
|
30
|
+
thinking: "#bb9af7",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/** Semantic status colors */
|
|
34
|
+
success: { primary: "#9ece6a", dim: "#1e3b2a" },
|
|
35
|
+
error: { primary: "#f7768e", dim: "#3a1620" },
|
|
36
|
+
warning: { primary: "#e0af68", dim: "#3d2e1a" },
|
|
37
|
+
info: { primary: "#2ac3de", dim: "#143a42" },
|
|
38
|
+
} as const;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "preserve",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": false,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "../..",
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"typeRoots": ["../../node_modules/@types"],
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src",
|
|
19
|
+
"bin",
|
|
20
|
+
"../core",
|
|
21
|
+
"../protocol",
|
|
22
|
+
"../llm",
|
|
23
|
+
"../runtime",
|
|
24
|
+
"../tools",
|
|
25
|
+
],
|
|
26
|
+
"exclude": ["node_modules", "dist"],
|
|
27
|
+
}
|