@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobius-os/mobius",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "description": "Mobius terminal client. Reuses Mobius frontend TypeScript types and jsonl entry shapes.",
6
6
  "bin": {
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
- ['reverse', 'connect', `${server.replace(/\/$/, '')}/aimux_bridge`, '--identifier', identifier, '--token', token, '--replace', ...(process.platform === 'win32' ? ['--silent-shell'] : [])],
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, ['reverse', 'connect', `${opts.server.replace(/\/$/, '')}/aimux_bridge`, '--identifier', identifier, '--token', opts.token, '--replace']),
426
+ spawnProcess: () => spawnLauncher(launcher, reverseConnectArgs(opts.server, identifier, opts.token)),
406
427
  })
407
428
  supervisor.start()
408
429
  })().finally(() => { installing = null })
@@ -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
- const VERSION = '0.2.8'
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
- if (typing && key.escape) { void onStop(); return }
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 (key.escape) { setPopupDismissed(true); return }
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 || key.escape || !input) return
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.escape) { props.onEscape?.(); return }
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.escape) { props.onBack?.(); return }
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
- * exitOnCtrlC is disabled the composer interprets Ctrl+C itself (stop the
7
- * current generation while busy; quit when idle). Use /quit to exit explicitly.
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: false })
13
+ render(React.createElement(App), { exitOnCtrlC: true })