mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
feat(mobile): add Shift arrow keys for Codex prompt navigation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"aicodeman": patch
|
||||
---
|
||||
|
||||
Add Shift+Left and Shift+Right buttons to the default and extended mobile agent keyboard bars, enabling Codex queued-message editing and prompt-stack navigation. Flush locally buffered drafts before navigation and keep terminal focus after taps.
|
||||
@@ -209,7 +209,7 @@ The most responsive AI coding agent experience on any phone. Full xterm.js termi
|
||||
<tr><td>Password typing on phone</td><td><b>QR code scan — instant auth</b></td></tr>
|
||||
</table>
|
||||
|
||||
- **Keyboard accessory bar** — `/init`, `/clear`, `/compact` quick-action buttons above the virtual keyboard; destructive commands require a double-press to confirm, so you never fire one by accident
|
||||
- **Keyboard accessory bar** — `⇧←` / `⇧→` send Shift+Left / Shift+Right in both default and extended agent layouts (Codex defaults: edit the last queued message / return through the prompt stack); `/init`, `/clear`, `/compact` quick-action buttons above the virtual keyboard; destructive commands require a double-press to confirm, so you never fire one by accident
|
||||
- **Dedicated Enter button** — replays the keypress through the terminal, so text buffered by local echo is flushed first rather than stranded
|
||||
- **Swipe navigation & smart keyboard handling** — swipe left/right to switch sessions; toolbar and terminal shift up when the keyboard opens (`visualViewport` API)
|
||||
- **Built for phones** — safe-area insets for notch and home indicator, 44px touch targets, bottom-sheet case picker, native momentum scrolling
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Defines three exports:
|
||||
*
|
||||
* - KeyboardAccessoryBar (singleton object) — Quick action buttons shown above the virtual
|
||||
* keyboard on mobile: arrow up/down, /init, Tab, paste, Esc, and dismiss (the extended
|
||||
* keyboard on mobile: arrow up/down, /init, Tab, Shift+Left/Right, paste, Esc, and dismiss (the extended
|
||||
* bar adds /clear, /compact, Shift+Tab and more). Tab flushes any locally-buffered
|
||||
* prompt text to the PTY before sending \t, so completion applies to what was typed.
|
||||
* The paste button opens a dialog that handles both text paste and image attach
|
||||
@@ -492,6 +492,8 @@ const KeyboardAccessoryBar = {
|
||||
</button>
|
||||
<button class="accessory-btn" data-action="init" title="/init">/init</button>
|
||||
<button class="accessory-btn" data-action="tab" title="Tab">Tab</button>
|
||||
<button class="accessory-btn" data-action="shift-left" title="Shift+Left (Codex: edit queued message)" aria-label="Shift+Left (Codex: edit queued message)">⇧←</button>
|
||||
<button class="accessory-btn" data-action="shift-right" title="Shift+Right (Codex: prompt stack back)" aria-label="Shift+Right (Codex: prompt stack back)">⇧→</button>
|
||||
<button class="accessory-btn" data-action="paste" title="Paste from clipboard">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
@@ -577,6 +579,8 @@ const KeyboardAccessoryBar = {
|
||||
<button class="accessory-btn" data-action="clear-input" title="Clear the current unsent input">⌫ All</button>
|
||||
<button class="accessory-btn accessory-btn-rmm" data-action="readmymind" title="Read My Mind: predict your next prompt">🧠</button>
|
||||
<button class="accessory-btn" data-action="tab" title="Tab">Tab</button>
|
||||
<button class="accessory-btn" data-action="shift-left" title="Shift+Left (Codex: edit queued message)" aria-label="Shift+Left (Codex: edit queued message)">⇧←</button>
|
||||
<button class="accessory-btn" data-action="shift-right" title="Shift+Right (Codex: prompt stack back)" aria-label="Shift+Right (Codex: prompt stack back)">⇧→</button>
|
||||
<button class="accessory-btn" data-action="shift-tab" title="Shift+Tab">⇧Tab</button>
|
||||
<button class="accessory-btn" data-action="effort-max" title="/effort max">Max</button>
|
||||
<button class="accessory-btn" data-action="ctrl-o" title="Ctrl+O">⌃O</button>
|
||||
@@ -615,7 +619,7 @@ const KeyboardAccessoryBar = {
|
||||
this.handleAction(action, btn);
|
||||
|
||||
// Refocus terminal so keyboard stays open (tap blurs terminal → keyboard dismisses → toolbar shifts)
|
||||
const refocusActions = new Set(['scroll-up', 'scroll-down', 'arrow-left', 'arrow-right', 'tab', 'shift-tab', 'ctrl', 'ctrl-o', 'opt-enter', 'esc', 'effort-max', 'clear-input']);
|
||||
const refocusActions = new Set(['scroll-up', 'scroll-down', 'arrow-left', 'arrow-right', 'tab', 'shift-tab', 'shift-left', 'shift-right', 'ctrl', 'ctrl-o', 'opt-enter', 'esc', 'effort-max', 'clear-input']);
|
||||
if (refocusActions.has(action) ||
|
||||
((action === 'clear' || action === 'compact') && this._confirmAction)) {
|
||||
if (typeof app !== 'undefined' && app.terminal) {
|
||||
@@ -744,6 +748,12 @@ const KeyboardAccessoryBar = {
|
||||
case 'arrow-right':
|
||||
this.sendNavKey('\x1b[C');
|
||||
break;
|
||||
case 'shift-left':
|
||||
this.sendNavKey('\x1b[1;2D');
|
||||
break;
|
||||
case 'shift-right':
|
||||
this.sendNavKey('\x1b[1;2C');
|
||||
break;
|
||||
case 'esc':
|
||||
this.sendKey('\x1b');
|
||||
break;
|
||||
@@ -875,7 +885,7 @@ const KeyboardAccessoryBar = {
|
||||
},
|
||||
|
||||
/**
|
||||
* A composer nav key (the four arrows) from the bar, under the SAME contract
|
||||
* A composer nav key (arrows, including Shift+Left/Right) from the bar, under the SAME contract
|
||||
* as pressing one on a hardware keyboard (the `isComposerNavKey` branch of
|
||||
* terminal-ui.js's onData): flush the unsent draft so the key edits the real
|
||||
* composer, then hand the session to plain PTY echo until Enter or Ctrl+C,
|
||||
|
||||
@@ -597,6 +597,34 @@ describe('composer nav keys from the bar', () => {
|
||||
const sentKeys = (fetchMock: { mock: { calls: unknown[][] } }) =>
|
||||
fetchMock.mock.calls.map((call) => JSON.parse((call[1] as { body: string }).body).input);
|
||||
|
||||
it.each(['simple', 'extended'])('exposes Shift arrows in the %s agent layout', (mode) => {
|
||||
const { bar, barElement } = loadBar('codex');
|
||||
bar.setMode(mode);
|
||||
expect(barElement.actions).toContain('shift-left');
|
||||
expect(barElement.actions).toContain('shift-right');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['shift-left', '\x1b[1;2D'],
|
||||
['shift-right', '\x1b[1;2C'],
|
||||
])('%s flushes the draft before navigation and hands editing to the PTY', (action, sequence) => {
|
||||
const { bar, app, overlay, fetchMock } = barWithDraft('unfinished follow-up');
|
||||
const events: string[] = [];
|
||||
app.sendInput = vi.fn(() => events.push('draft'));
|
||||
fetchMock.mockImplementation(() => {
|
||||
events.push('key');
|
||||
return Promise.resolve({ ok: true, catch: () => {} });
|
||||
});
|
||||
|
||||
bar.handleAction(action);
|
||||
|
||||
expect(app.sendInput).toHaveBeenCalledWith('unfinished follow-up');
|
||||
expect(events).toEqual(['draft', 'key']);
|
||||
expect(sentKeys(fetchMock)).toEqual([sequence]);
|
||||
expect(overlay.pendingText).toBe('');
|
||||
expect([...(app._echoPassthroughSessions as Set<string>)]).toEqual(['session-1']);
|
||||
});
|
||||
|
||||
it('flushes the unsent draft before sending the arrow', () => {
|
||||
// On a phone the typed text lives in the overlay and has NEVER reached the
|
||||
// PTY, so an arrow sent on its own arrives at a composer the CLI still
|
||||
|
||||
@@ -471,7 +471,18 @@ describe('Virtual Keyboard', () => {
|
||||
});
|
||||
// Tab replaced /clear in the simple bar; /clear and /compact live in the
|
||||
// extended bar only.
|
||||
expect(actions).toEqual(['scroll-up', 'scroll-down', 'init', 'tab', 'paste', 'esc', 'dismiss']);
|
||||
expect(actions).toEqual([
|
||||
'scroll-up',
|
||||
'scroll-down',
|
||||
'init',
|
||||
'tab',
|
||||
'shift-left',
|
||||
'shift-right',
|
||||
'paste',
|
||||
'readmymind',
|
||||
'esc',
|
||||
'dismiss',
|
||||
]);
|
||||
});
|
||||
|
||||
it('double-tap confirm on /clear button', async () => {
|
||||
|
||||
Reference in New Issue
Block a user