@mobius-os/mobius 0.3.3 → 0.3.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.
- package/package.json +1 -1
- package/src/aimux.ts +23 -2
- package/src/components/Chat.tsx +7 -4
- package/src/components/PrepScreen.tsx +1 -1
- package/src/components/primitives.tsx +7 -2
- package/src/main.tsx +3 -3
package/package.json
CHANGED
package/src/aimux.ts
CHANGED
|
@@ -209,6 +209,27 @@ export function tuiAimuxIdentifier(): string {
|
|
|
209
209
|
return `tui-${host || 'pc'}`
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Build the reverse-connect command in one place. The TUI can launch AIMUX
|
|
214
|
+
* through either a venv executable or bundled Python; both paths must request
|
|
215
|
+
* hidden Windows shells or every remote command flashes a console and steals
|
|
216
|
+
* keyboard focus from the TUI.
|
|
217
|
+
*/
|
|
218
|
+
export function reverseConnectArgs(
|
|
219
|
+
server: string,
|
|
220
|
+
identifier: string,
|
|
221
|
+
token: string,
|
|
222
|
+
platform: NodeJS.Platform = process.platform,
|
|
223
|
+
): string[] {
|
|
224
|
+
return [
|
|
225
|
+
'reverse', 'connect', `${server.replace(/\/$/, '')}/aimux_bridge`,
|
|
226
|
+
'--identifier', identifier,
|
|
227
|
+
'--token', token,
|
|
228
|
+
'--replace',
|
|
229
|
+
...(platform === 'win32' ? ['--silent-shell'] : []),
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
212
233
|
export async function probeAimuxBridgeConnection(
|
|
213
234
|
server: string,
|
|
214
235
|
token: string,
|
|
@@ -261,7 +282,7 @@ export class AimuxSupervisor {
|
|
|
261
282
|
onStatus({ state: 'starting', phase: 'connecting', detail: '正在连接 Mobius AIMUX bridge…', identifier, attempt: this.reconnectAttempt })
|
|
262
283
|
const child = this.opts.spawnProcess?.() ?? spawn(
|
|
263
284
|
aimuxExe(),
|
|
264
|
-
|
|
285
|
+
reverseConnectArgs(server, identifier, token),
|
|
265
286
|
{ windowsHide: true },
|
|
266
287
|
)
|
|
267
288
|
this.child = child
|
|
@@ -402,7 +423,7 @@ export async function startAimuxConnection(opts: { server: string; token: string
|
|
|
402
423
|
const launcher = ready.launcher
|
|
403
424
|
supervisor = new AimuxSupervisor({
|
|
404
425
|
server: opts.server, token: opts.token, identifier, onStatus,
|
|
405
|
-
spawnProcess: () => spawnLauncher(launcher,
|
|
426
|
+
spawnProcess: () => spawnLauncher(launcher, reverseConnectArgs(opts.server, identifier, opts.token)),
|
|
406
427
|
})
|
|
407
428
|
supervisor.start()
|
|
408
429
|
})().finally(() => { installing = null })
|
package/src/components/Chat.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import type { ReadyState } from './PrepScreen.js'
|
|
|
17
17
|
import type { AnyEntry } from '../types.js'
|
|
18
18
|
import type { AimuxStatus } from '../aimux.js'
|
|
19
19
|
import { AimuxStatusLine, aimuxStatusText } from './AimuxStatus.js'
|
|
20
|
+
import { isEscapeKeypress } from './primitives.js'
|
|
20
21
|
|
|
21
22
|
interface ChatProps {
|
|
22
23
|
client: MobiusClient
|
|
@@ -35,7 +36,8 @@ interface TerminalSize {
|
|
|
35
36
|
isTty: boolean
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
import { createRequire } from 'node:module'
|
|
40
|
+
const VERSION = createRequire(import.meta.url)('../../package.json').version
|
|
39
41
|
|
|
40
42
|
const SLASH_COMMANDS = [
|
|
41
43
|
{ cmd: '/clear', desc: '清空当前对话,开启新会话' },
|
|
@@ -422,7 +424,8 @@ function Composer({ onSubmit, onStop, onQuit, typing, commands }: ComposerProps)
|
|
|
422
424
|
}
|
|
423
425
|
|
|
424
426
|
useInput((input, key) => {
|
|
425
|
-
|
|
427
|
+
const escape = isEscapeKeypress(input, key)
|
|
428
|
+
if (typing && escape) { void onStop(); return }
|
|
426
429
|
|
|
427
430
|
if (popupOpen) {
|
|
428
431
|
if (key.upArrow) { setPopupIdx(i => (i <= 0 ? filtered.length - 1 : i - 1)); return }
|
|
@@ -431,7 +434,7 @@ function Composer({ onSubmit, onStop, onQuit, typing, commands }: ComposerProps)
|
|
|
431
434
|
const pick = filtered[popupIdx >= 0 ? popupIdx : 0]
|
|
432
435
|
if (pick) { edit(`${pick.cmd} `, pick.cmd.length + 1); setPopupDismissed(true); return }
|
|
433
436
|
}
|
|
434
|
-
if (
|
|
437
|
+
if (escape) { setPopupDismissed(true); return }
|
|
435
438
|
}
|
|
436
439
|
|
|
437
440
|
if (key.return) {
|
|
@@ -475,7 +478,7 @@ function Composer({ onSubmit, onStop, onQuit, typing, commands }: ComposerProps)
|
|
|
475
478
|
if (key.ctrl && input === 'u') { edit('', 0); return }
|
|
476
479
|
if (key.ctrl && input === 'k') { edit(value.slice(0, cursor), cursor); return }
|
|
477
480
|
if (key.ctrl && input === 'j') { edit(value.slice(0, cursor) + '\n' + value.slice(cursor), cursor + 1); return }
|
|
478
|
-
if (key.ctrl || key.meta ||
|
|
481
|
+
if (key.ctrl || key.meta || escape || !input) return
|
|
479
482
|
edit(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length)
|
|
480
483
|
})
|
|
481
484
|
|
|
@@ -293,7 +293,7 @@ function IssuePicker({ issues, onPick, onCreate }: {
|
|
|
293
293
|
<Text bold color="cyan">创建新任务</Text>
|
|
294
294
|
<Text color="gray">输入任务名称(不使用 git worktree)</Text>
|
|
295
295
|
<TextInput value={name} onChange={setName} focused placeholder="命令行任务"
|
|
296
|
-
onSubmit={() => onCreate(name)} />
|
|
296
|
+
onSubmit={() => onCreate(name)} onEscape={() => setMode('list')} />
|
|
297
297
|
<Text color="gray">回车创建 · Esc 返回</Text>
|
|
298
298
|
</Box>
|
|
299
299
|
)
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import React, { useEffect, useRef, useState } from 'react'
|
|
6
6
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
7
7
|
|
|
8
|
+
/** Windows Terminal/ConPTY may expose Esc as a named key, a raw byte, or Ctrl+[. */
|
|
9
|
+
export function isEscapeKeypress(input: string, key: { escape?: boolean; ctrl?: boolean }): boolean {
|
|
10
|
+
return key.escape === true || input === '\x1b' || (key.ctrl === true && input === '[')
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
// ─── TextInput ───────────────────────────────────────────────────────────────
|
|
9
14
|
export interface TextInputProps {
|
|
10
15
|
value: string
|
|
@@ -46,7 +51,7 @@ export function TextInput(props: TextInputProps) {
|
|
|
46
51
|
if (key.return) { props.onSubmit?.(); return }
|
|
47
52
|
if (key.upArrow) { props.onArrowUp?.(); return }
|
|
48
53
|
if (key.downArrow) { props.onArrowDown?.(); return }
|
|
49
|
-
if (key
|
|
54
|
+
if (isEscapeKeypress(input, key)) { props.onEscape?.(); return }
|
|
50
55
|
if (key.tab) { props.onTab?.(); return }
|
|
51
56
|
// Ink labels the \x7f that virtually every terminal's Backspace key emits
|
|
52
57
|
// as `key.delete` (see its parse-keypress.js TODO). Treat either signal as
|
|
@@ -175,7 +180,7 @@ export function Select(props: SelectProps) {
|
|
|
175
180
|
if (key.return) { props.onConfirm?.(Array.from(selectedSet)); return }
|
|
176
181
|
if (input === ' ') { props.onToggle?.(items[active].value); return }
|
|
177
182
|
}
|
|
178
|
-
if (key
|
|
183
|
+
if (isEscapeKeypress(input, key)) { props.onBack?.(); return }
|
|
179
184
|
}, { isActive: props.focused !== false })
|
|
180
185
|
|
|
181
186
|
// viewport: keep the active item on screen. Without this a long list renders
|
package/src/main.tsx
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Run: npx tsx src/main.tsx (or `npm start`)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Ink owns Ctrl+C globally so Windows users can always exit, including from
|
|
7
|
+
* setup screens that do not mount the chat composer.
|
|
8
8
|
*/
|
|
9
9
|
import React from 'react'
|
|
10
10
|
import { render } from 'ink'
|
|
11
11
|
import { App } from './App.js'
|
|
12
12
|
|
|
13
|
-
render(React.createElement(App), { exitOnCtrlC:
|
|
13
|
+
render(React.createElement(App), { exitOnCtrlC: true })
|