Skip to content

Commit 6c15859

Browse files
committed
Release v1.0.25
1 parent 30b11f9 commit 6c15859

7 files changed

Lines changed: 163 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/).
55

6+
## 1.0.25 (2026-06-28)
7+
8+
### Bug Fixes
9+
10+
- **TUI task wizard: reliable textarea confirmation fallback** ([#13](https://github.com/oxgeneral/ORCH/issues/13)) — `Tab` now confirms every `FormWizard` step, including multiline `Description` fields. This keeps `Enter` available for textarea newlines while giving Windows, WSL, CMD, PowerShell, Git Bash, and terminals without reliable `Ctrl+Enter` support a deterministic way to finish the step.
11+
- **Wizard hints updated for the new shortcut** — text/select/multiselect steps now show `Enter/Tab confirm`; textarea steps show `⌘+Enter/Tab confirm` on macOS and `Ctrl+Enter/Tab confirm` elsewhere.
12+
13+
### Tests
14+
15+
- Added component coverage for `Tab` confirmation in text, textarea, required textarea, and multiselect steps.
16+
- Added App-level coverage for creating a task with a textarea description confirmed by `Tab`.
17+
- Verified the fix manually in a real terminal PTY: task creation persisted the description after `Tab` confirmation.
18+
619
## 1.0.24 (2026-05-19)
720

821
### New Features

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@oxgeneral/orch",
3-
"version": "1.0.24",
3+
"version": "1.0.25",
44
"description": "AI agent runtime — orchestrate Claude, Cursor, Codex & OpenCode as one team. Multi-agent task automation with state machine, auto-retry, inter-agent messaging, goals and teams. Zero-config CLI + programmatic API.",
55
"type": "module",
66
"engines": {

‎src/bin/cli.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const program = new Command();
4242
program
4343
.name('orchestry')
4444
.description('Agents Organizations — CLI orchestrator for AI agents')
45-
.version('1.0.24')
45+
.version('1.0.25')
4646
.option('--json', 'Output as JSON')
4747
.option('--quiet', 'Minimal output (IDs only)')
4848
.option('--no-color', 'Disable colors')

‎src/tui/components/FormWizard.tsx‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,8 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
398398
// fall through to normal text handling below
399399
}
400400

401-
// Enter: submit text step
402-
if (key.return) {
401+
// Enter / Tab: submit text step
402+
if (key.return || key.tab) {
403403
const val = textInput.trim();
404404
if (step.required && !val) { setDirty(true); return; }
405405
if (validationError !== null) {
@@ -434,8 +434,9 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
434434
}
435435

436436
if (step.type === 'textarea') {
437-
// Ctrl+Enter / Cmd+Enter: confirm textarea
438-
if (key.return && (key.ctrl || key.meta)) {
437+
// Ctrl+Enter / Cmd+Enter confirm textarea. Tab is a fallback for
438+
// terminals that cannot distinguish Ctrl+Enter from plain Enter.
439+
if ((key.return && (key.ctrl || key.meta)) || key.tab) {
439440
const val = taLines.join('\n').trim();
440441
if (step.required && !val) { setDirty(true); return; }
441442
if (validationError !== null) {
@@ -664,7 +665,7 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
664665
return;
665666
}
666667
} else {
667-
// Multiselect: Space toggle, Enter confirm
668+
// Multiselect: Space toggle, Enter / Tab confirm
668669
if (input === ' ') {
669670
const opt = options[clampedSelectIndex];
670671
if (opt) {
@@ -680,7 +681,7 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
680681
}
681682
return;
682683
}
683-
if (key.return) {
684+
if (key.return || key.tab) {
684685
const selected = Array.from(multiSelected).join(',');
685686
goToNextStep(selected);
686687
return;
@@ -728,7 +729,7 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
728729
<Box marginTop={0}>
729730
<Text color={tuiColors.white} bold> {step.label}</Text>
730731
{step.required && <Text color={tuiColors.red}> *</Text>}
731-
{!step.required && <Text color={tuiColors.dim}> (optional, {step.type === 'textarea' ? `${CMD_KEY}+Enter` : 'Enter'} to skip)</Text>}
732+
{!step.required && <Text color={tuiColors.dim}> (optional, {step.type === 'textarea' ? `${CMD_KEY}+Enter/Tab` : 'Enter/Tab'} to skip)</Text>}
732733
</Box>
733734

734735
{/* Step description / guidance */}
@@ -895,16 +896,16 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
895896
<Text color={tuiColors.ghost}>
896897
{' '}
897898
{step.type === 'select'
898-
? '\u2191\u2193 select Enter confirm'
899+
? '\u2191\u2193 select Enter/Tab confirm'
899900
: step.type === 'multiselect'
900-
? '\u2191\u2193 move Space toggle Enter confirm'
901+
? '\u2191\u2193 move Space toggle Enter/Tab confirm'
901902
: step.type === 'textarea'
902-
? `Enter newline ${CMD_KEY}+Enter confirm \u2190\u2191\u2192\u2193 navigate`
903+
? `Enter newline ${CMD_KEY}+Enter/Tab confirm \u2190\u2191\u2192\u2193 navigate`
903904
: browsingSuggestions
904-
? '\u2191\u2193 browse Enter select \u2191 back to input'
905+
? '\u2191\u2193 browse Enter select Tab confirm \u2191 back to input'
905906
: step.suggestions
906-
? '\u2190\u2192 move Enter confirm \u2193 browse templates'
907-
: '\u2190\u2192 move Enter confirm'}
907+
? '\u2190\u2192 move Enter/Tab confirm \u2193 browse templates'
908+
: '\u2190\u2192 move Enter/Tab confirm'}
908909
{onPasteImage && (step.type === 'text' || step.type === 'textarea') ? ` ${CMD_KEY}+V paste image` : ''}
909910
{' Esc '}
910911
{currentStep > 0 ? 'back' : 'cancel'}

‎test/unit/tui/app.test.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,36 @@ describe('App', () => {
758758
expect(output).toContain('Created');
759759
});
760760

761+
it('creates task via wizard with textarea confirmed by Tab', async () => {
762+
const state: OrchestratorState = { ...DEFAULT_STATE };
763+
let createdDescription: string | undefined;
764+
const onCreateTask = async (title: string, opts?: { description?: string }) => {
765+
createdDescription = opts?.description;
766+
return makeTask({ id: 'tsk_tab', title, description: opts?.description });
767+
};
768+
const { stdin, lastFrame } = render(
769+
React.createElement(App, { projectName: 'test', tasks: [], state, onCreateTask }),
770+
);
771+
772+
stdin.write('n');
773+
await delay(50);
774+
stdin.write('Build API');
775+
await delay(50);
776+
stdin.write('\r');
777+
await delay(50);
778+
stdin.write('\r');
779+
await delay(50);
780+
expect(lastFrame()!).toContain('Enter/Tab confirm');
781+
782+
stdin.write('Created through Tab fallback');
783+
await delay(50);
784+
stdin.write('\t');
785+
await delay(100);
786+
787+
expect(createdDescription).toBe('Created through Tab fallback');
788+
expect(lastFrame()!).toContain('Created');
789+
});
790+
761791
it('does not create task on Enter with empty title in wizard', async () => {
762792
const state: OrchestratorState = { ...DEFAULT_STATE };
763793
let called = false;

‎test/unit/tui/form-wizard.test.tsx‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe('FormWizard textarea', () => {
109109
await delay(50);
110110
const output = lastFrame()!;
111111
expect(output).toContain('Enter newline');
112-
expect(output).toContain('Enter confirm');
112+
expect(output).toContain('Enter/Tab confirm');
113113
expect(output).toContain('navigate');
114114
});
115115

@@ -129,7 +129,30 @@ describe('FormWizard textarea', () => {
129129
await delay(50);
130130
const output = lastFrame()!;
131131
expect(output).toContain('←→ move');
132-
expect(output).toContain('Enter confirm');
132+
expect(output).toContain('Enter/Tab confirm');
133+
});
134+
135+
it('Tab confirms text step without inserting a tab character', async () => {
136+
const onComplete = vi.fn();
137+
const onCancel = vi.fn();
138+
const { stdin } = render(
139+
React.createElement(FormWizard, {
140+
title: 'Test',
141+
steps: [{ id: 'title', label: 'Title', type: 'text' }],
142+
onComplete,
143+
onCancel,
144+
width: 60,
145+
height: 20,
146+
}),
147+
);
148+
await delay(50);
149+
150+
stdin.write('Hello');
151+
await delay(50);
152+
stdin.write('\t');
153+
await delay(50);
154+
155+
expect(onComplete).toHaveBeenCalledWith({ title: 'Hello' });
133156
});
134157

135158
/* ── Enter adds new line (Shift+Enter also works) ── */
@@ -194,6 +217,29 @@ describe('FormWizard textarea', () => {
194217
expect(onComplete).toHaveBeenCalledWith({ body: 'Hello world' });
195218
});
196219

220+
it('Tab confirms textarea as fallback for terminals without Ctrl+Enter support', async () => {
221+
const onComplete = vi.fn();
222+
const onCancel = vi.fn();
223+
const { stdin } = render(
224+
React.createElement(FormWizard, {
225+
title: 'Test',
226+
steps: makeTextareaSteps(),
227+
onComplete,
228+
onCancel,
229+
width: 60,
230+
height: 20,
231+
}),
232+
);
233+
await delay(50);
234+
235+
stdin.write('Hello from tab');
236+
await delay(50);
237+
stdin.write('\t');
238+
await delay(50);
239+
240+
expect(onComplete).toHaveBeenCalledWith({ body: 'Hello from tab' });
241+
});
242+
197243
it('Ctrl+Enter confirms multiline textarea with newlines joined', async () => {
198244
const onComplete = vi.fn();
199245
const onCancel = vi.fn();
@@ -243,6 +289,27 @@ describe('FormWizard textarea', () => {
243289
expect(onComplete).not.toHaveBeenCalled();
244290
});
245291

292+
it('Tab on empty required textarea does not confirm', async () => {
293+
const onComplete = vi.fn();
294+
const onCancel = vi.fn();
295+
const { stdin } = render(
296+
React.createElement(FormWizard, {
297+
title: 'Test',
298+
steps: makeTextareaSteps({ required: true }),
299+
onComplete,
300+
onCancel,
301+
width: 60,
302+
height: 20,
303+
}),
304+
);
305+
await delay(50);
306+
307+
stdin.write('\t');
308+
await delay(50);
309+
310+
expect(onComplete).not.toHaveBeenCalled();
311+
});
312+
246313
/* ── Cursor navigation ── */
247314

248315
it('←→ arrows move cursor in textarea', async () => {
@@ -1104,6 +1171,39 @@ describe('FormWizard inline validation', () => {
11041171
expect(onComplete).not.toHaveBeenCalled();
11051172
});
11061173

1174+
it('Tab confirms multiselect step with current selection', async () => {
1175+
const onComplete = vi.fn();
1176+
const { stdin, lastFrame } = render(
1177+
React.createElement(FormWizard, {
1178+
title: 'Test',
1179+
steps: [
1180+
{
1181+
id: 'skills',
1182+
label: 'Skills',
1183+
type: 'multiselect',
1184+
options: [
1185+
{ value: 'frontend', label: 'Frontend' },
1186+
{ value: 'backend', label: 'Backend' },
1187+
],
1188+
},
1189+
],
1190+
onComplete,
1191+
onCancel: vi.fn(),
1192+
width: 60,
1193+
height: 20,
1194+
}),
1195+
);
1196+
await delay(50);
1197+
expect(lastFrame()!).toContain('Enter/Tab confirm');
1198+
1199+
stdin.write(' ');
1200+
await delay(50);
1201+
stdin.write('\t');
1202+
await delay(50);
1203+
1204+
expect(onComplete).toHaveBeenCalledWith({ skills: 'frontend' });
1205+
});
1206+
11071207
/* ── validationError reset on step navigation ── */
11081208

11091209
it('validationError is reset when advancing to next step', async () => {

0 commit comments

Comments
 (0)