Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/dev-link.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
echo "Building..."
npm run build
echo "Linking..."
npm link
echo "Done — 'orch' now points to local build."
4 changes: 4 additions & 0 deletions scripts/dev-unlink.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
npm unlink -g @oxgeneral/orch
echo "Done — 'orch' restored to globally installed version."
9 changes: 8 additions & 1 deletion src/application/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../domain/transitions.js';
import { NoAgentsError, TaskAlreadyRunningError, LockConflictError, WorkspaceError, classifyAdapterError } from '../domain/errors.js';
import { scopesOverlap, ScopeIndex } from '../domain/scope.js';
import { isModelTier, resolveModel } from '../domain/model-tiers.js';
import { acquireLock, releaseLock, touchLock } from '../infrastructure/storage/lock.js';
import type { ITaskStore, IAgentStore, IRunStore, IStateStore, IContextStore, IGoalStore } from '../infrastructure/storage/interfaces.js';
import { CachedTaskStore, CachedAgentStore, CachedGoalStore } from '../infrastructure/storage/cached-stores.js';
Expand Down Expand Up @@ -1097,6 +1098,12 @@ export class Orchestrator {
const abortController = new AbortController();
this.abortControllers.set(taskId, abortController);

// Resolve semantic tier aliases (capable/balanced/fast) → concrete model strings
const resolvedConfig = { ...agentData.config };
if (resolvedConfig.model && isModelTier(resolvedConfig.model)) {
resolvedConfig.model = resolveModel(agent.adapter, resolvedConfig.model) || undefined;
}

const handle = adapter.execute({
prompt,
systemPrompt,
Expand All @@ -1107,7 +1114,7 @@ export class Orchestrator {
ORCH_AGENT_NAME: agent.name,
ORCH_TASK_ID: task.id,
},
config: agentData.config,
config: resolvedConfig,
signal: abortController.signal,
});

Expand Down
4 changes: 2 additions & 2 deletions src/domain/model-tiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export type ModelTier = 'capable' | 'balanced' | 'fast';
*/
export const MODEL_TIER_MAP: Record<AdapterKind, Record<ModelTier, string>> = {
claude: {
capable: 'claude-opus-4-6',
capable: 'claude-opus-4-7',
balanced: 'claude-sonnet-4-6',
fast: 'claude-haiku-4-6',
},
opencode: {
capable: 'openrouter/anthropic/claude-opus-4.6',
capable: 'openrouter/anthropic/claude-opus-4.7',
balanced: '',
fast: 'openrouter/google/gemini-2.5-flash',
},
Expand Down
8 changes: 7 additions & 1 deletion src/tui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export function App({
const inputHook = useTextInput();
const inputValue = inputHook.value;
const [wizardConfig, setWizardConfig] = useState<WizardConfig | null>(null);
const [wizardStepType, setWizardStepType] = useState<'text' | 'select' | 'textarea' | 'multiselect' | null>(null);
/** Temp file paths for images pasted via Ctrl+I during task wizard */
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);

Expand Down Expand Up @@ -1009,7 +1010,7 @@ export function App({
targetId: agent.id,
});
setInputMode('wizard');
}, [liveTeams]);
}, [liveTeams, liveAgents]);

const launchConfigWizard = useCallback(() => {
setWizardConfig({
Expand Down Expand Up @@ -1122,6 +1123,9 @@ export function App({
onUpdateAgent(targetId, { name: fields.name, role: fields.role, model: fields.model, effort: fields.effort }).then(
(agent) => {
addMessage(`\u2713 Updated agent "${agent.name}"`, tuiColors.green);
// Optimistically merge returned agent into local state so reopening the
// editor immediately after save shows fresh values without waiting for refreshAll.
setLiveAgents((prev) => prev.map((a) => a.id === agent.id ? agent : a));
// Handle team change
const teamOps: Promise<unknown>[] = [];
if (oldTeamId && oldTeamId !== newTeamId && onLeaveTeam) {
Expand Down Expand Up @@ -2521,6 +2525,7 @@ export function App({
height={feedH}
onPasteImage={isPasteCapable ? handlePasteImage : undefined}
onSuggestionSelected={wizardConfig.kind === 'agent' ? handleSuggestionSelected : undefined}
onStepChange={(step) => setWizardStepType(step ? step.type : null)}
footerExtra={
pendingAttachments.length > 0 && isPasteCapable ? `\uD83D\uDCCE${pendingAttachments.length}`
: undefined
Expand Down Expand Up @@ -2627,6 +2632,7 @@ export function App({
width={W}
hasSuggestions={showSuggestions}
onboardingCompleted={initialState.onboardingCompleted}
wizardStepType={inputMode === 'wizard' ? wizardStepType : null}
/>
</Box>
);
Expand Down
65 changes: 64 additions & 1 deletion src/tui/components/CommandBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ export interface CommandBarProps {
width: number;
hasSuggestions?: boolean;
onboardingCompleted?: boolean;
/** When a wizard is open, render a wizard-specific hint row instead of the app nav hints. */
wizardStepType?: 'text' | 'select' | 'textarea' | 'multiselect' | null;
}

export const CommandBar = React.memo(function CommandBar({
mode, value, completion, activeView, canRun, canNew, canApprove, canReject, canCancel, canDelete, canUndo, canEdit, canForceStop, canToggleAuto, autoActive, canPause, isPaused, canToggleShowAll, showAllActive, hasDetail,
itemCount, itemLabel, width, hasSuggestions, onboardingCompleted,
itemCount, itemLabel, width, hasSuggestions, onboardingCompleted, wizardStepType,
}: CommandBarProps) {
if (mode === 'command') {
const hintText = hasSuggestions
Expand All @@ -64,6 +66,67 @@ export const CommandBar = React.memo(function CommandBar({
);
}

// Wizard mode — show step-specific hints (overrides the app nav hints)
if (wizardStepType) {
const hint =
wizardStepType === 'textarea' ? (
<>
<Text bold color={tuiColors.amber}>Ctrl+S</Text>
<Text color={tuiColors.dim}> save description</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Enter</Text>
<Text color={tuiColors.dim}> newline</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>{'\u2190\u2191\u2192\u2193'}</Text>
<Text color={tuiColors.dim}> navigate</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Esc</Text>
<Text color={tuiColors.dim}> back</Text>
</>
) : wizardStepType === 'select' ? (
<>
<Text bold color={tuiColors.gray}>{'\u2191\u2193'}</Text>
<Text color={tuiColors.dim}> select</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Enter</Text>
<Text color={tuiColors.dim}> confirm</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Esc</Text>
<Text color={tuiColors.dim}> back</Text>
</>
) : wizardStepType === 'multiselect' ? (
<>
<Text bold color={tuiColors.gray}>{'\u2191\u2193'}</Text>
<Text color={tuiColors.dim}> move</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Space</Text>
<Text color={tuiColors.dim}> toggle</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Enter</Text>
<Text color={tuiColors.dim}> confirm</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Esc</Text>
<Text color={tuiColors.dim}> back</Text>
</>
) : (
<>
<Text bold color={tuiColors.gray}>Enter</Text>
<Text color={tuiColors.dim}> confirm</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>{'\u2190\u2192'}</Text>
<Text color={tuiColors.dim}> move</Text>
<Text color={tuiColors.dim}>{' '}</Text>
<Text bold color={tuiColors.gray}>Esc</Text>
<Text color={tuiColors.dim}> back</Text>
</>
);
return (
<Box paddingX={2} justifyContent="space-between" width={width}>
<Text>{hint}</Text>
</Box>
);
}

// Navigate mode — hotkey hints
return (
<Box paddingX={2} justifyContent="space-between" width={width}>
Expand Down
16 changes: 11 additions & 5 deletions src/tui/components/FormWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface FormWizardProps {
footerExtra?: string;
/** Called when user selects a suggestion from a text step's suggestion list */
onSuggestionSelected?: (suggestionValue: string) => void;
/** Called whenever the active step changes, so parent can reflect step type in the bottom bar. */
onStepChange?: (step: WizardStep | null) => void;
}

const CURSOR = '\u2588'; // █ (used by textarea render)
Expand Down Expand Up @@ -98,8 +100,12 @@ function wordBoundaryForward(text: string, pos: number): number {
return i;
}

export function FormWizard({ title, steps, onComplete, onCancel, width, height, onPasteImage, footerExtra, onSuggestionSelected }: FormWizardProps) {
export function FormWizard({ title, steps, onComplete, onCancel, width, height, onPasteImage, footerExtra, onSuggestionSelected, onStepChange }: FormWizardProps) {
const [currentStep, setCurrentStep] = useState(0);
useEffect(() => {
onStepChange?.(steps[currentStep] ?? null);
return () => { onStepChange?.(null); };
}, [currentStep, steps, onStepChange]);
const [values, setValues] = useState<Record<string, string>>({});

// Unified text input for single-line text steps (replaces textInput + cursorPos).
Expand Down Expand Up @@ -434,8 +440,8 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
}

if (step.type === 'textarea') {
// Ctrl+Enter / Cmd+Enter: confirm textarea
if (key.return && (key.ctrl || key.meta)) {
// Ctrl+Enter / Cmd+Enter / Ctrl+S: confirm textarea
if ((key.return && (key.ctrl || key.meta)) || (key.ctrl && input === 's')) {
const val = taLines.join('\n').trim();
if (step.required && !val) { setDirty(true); return; }
if (validationError !== null) {
Expand Down Expand Up @@ -728,7 +734,7 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
<Box marginTop={0}>
<Text color={tuiColors.white} bold> {step.label}</Text>
{step.required && <Text color={tuiColors.red}> *</Text>}
{!step.required && <Text color={tuiColors.dim}> (optional, {step.type === 'textarea' ? `${CMD_KEY}+Enter` : 'Enter'} to skip)</Text>}
{!step.required && <Text color={tuiColors.dim}> (optional, {step.type === 'textarea' ? `Ctrl+S` : 'Enter'} to skip)</Text>}
</Box>

{/* Step description / guidance */}
Expand Down Expand Up @@ -899,7 +905,7 @@ export function FormWizard({ title, steps, onComplete, onCancel, width, height,
: step.type === 'multiselect'
? '\u2191\u2193 move Space toggle Enter confirm'
: step.type === 'textarea'
? `Enter newline ${CMD_KEY}+Enter confirm \u2190\u2191\u2192\u2193 navigate`
? `Ctrl+S save Enter newline \u2190\u2191\u2192\u2193 navigate`
: browsingSuggestions
? '\u2191\u2193 browse Enter select \u2191 back to input'
: step.suggestions
Expand Down
2 changes: 1 addition & 1 deletion src/tui/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export function TextInput({
return (
<Box borderStyle={borderStyle} borderColor={borderColor}>
{prefixStr && <Text color={prefixColor}>{prefixStr}</Text>}
{placeholder && <Text color={placeholderColor}>{placeholder}</Text>}
{showCursor && <Text color={cursorColor}>{CURSOR_CHAR}</Text>}
{placeholder && <Text color={placeholderColor}>{placeholder}</Text>}
</Box>
);
}
Expand Down
9 changes: 7 additions & 2 deletions src/tui/wizardConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { isMcpSkill } from '../application/agent-factory.js';
// ── Model catalogs per adapter ──

const CLAUDE_MODELS = [
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'most capable' },
{ value: 'capable', label: 'Capable tier (auto)', hint: 'always resolves to latest flagship' },
{ value: 'balanced', label: 'Balanced tier (auto)', hint: 'always resolves to latest sonnet' },
{ value: 'fast', label: 'Fast tier (auto)', hint: 'always resolves to latest haiku' },
{ value: 'claude-opus-4-7', label: 'Claude Opus 4.7', hint: 'most capable, latest' },
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'previous flagship' },
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', hint: 'fast, balanced' },
{ value: 'claude-haiku-4-6', label: 'Claude Haiku 4.6', hint: 'fastest, cheapest' },
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', hint: 'extended thinking' },
Expand Down Expand Up @@ -48,8 +52,9 @@ const CURSOR_MODELS = [

const OPENCODE_MODELS = [
{ value: '', label: 'Default', hint: 'use model configured in opencode' },
{ value: 'openrouter/anthropic/claude-opus-4.7', label: 'Claude Opus 4.7', hint: 'most capable, latest' },
{ value: 'openrouter/anthropic/claude-sonnet-4.6', label: 'Claude Sonnet 4.6', hint: 'fast, balanced' },
{ value: 'openrouter/anthropic/claude-opus-4.6', label: 'Claude Opus 4.6', hint: 'most capable' },
{ value: 'openrouter/anthropic/claude-opus-4.6', label: 'Claude Opus 4.6', hint: 'previous flagship' },
{ value: 'openrouter/google/gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'Google' },
{ value: 'openrouter/google/gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'Google, fast' },
{ value: 'openrouter/deepseek/deepseek-v3.2', label: 'DeepSeek V3.2', hint: 'open-source' },
Expand Down
Loading