Skip to content

Commit 05aa114

Browse files
committed
fix: align harness UX with preview branch
- InvokeScreen: Ctrl+N for new session (was bare N), hint messages rendered in gray, context-sensitive "Loading..."/"Thinking..." label, directional scroll arrows instead of numeric range - DevScreen: disable keyboard input while exiting (!isExiting guard) - deploy/actions: imperative harness teardown before stack destroy (gated behind isPreviewEnabled) so harnesses aren't orphaned - browser-mode: resolve harness traces via resolveAgentOrHarness instead of ignoring harnessName parameter - resolve-agent: add resolveHarness and resolveAgentOrHarness helpers
1 parent 9c6d334 commit 05aa114

5 files changed

Lines changed: 172 additions & 14 deletions

File tree

src/cli/commands/deploy/actions.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
} from '../../operations/deploy';
3737
import { computeProjectDeployHash } from '../../operations/deploy/change-detection';
3838
import { formatTargetStatus, getGatewayTargetStatuses } from '../../operations/deploy/gateway-status';
39-
import { createDeploymentManager } from '../../operations/deploy/imperative';
39+
import { type ImperativeDeployContext, createDeploymentManager } from '../../operations/deploy/imperative';
4040
import { deleteOrphanedABTests, setupABTests } from '../../operations/deploy/post-deploy-ab-tests';
4141
import {
4242
resolveConfigBundleComponentKeys,
@@ -380,7 +380,37 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
380380
endStep('success');
381381

382382
if (context.isTeardownDeploy) {
383-
// After deploying the empty spec, destroy the stack entirely
383+
if (isPreviewEnabled()) {
384+
const imperativeManager = createDeploymentManager();
385+
const existingTeardownState: DeployedState = await configIO
386+
.readDeployedState()
387+
.catch(() => ({ targets: {} }) as DeployedState);
388+
const teardownContext: ImperativeDeployContext = {
389+
projectSpec: context.projectSpec,
390+
target,
391+
configIO,
392+
deployedState: existingTeardownState,
393+
onProgress: (step: string, status: 'start' | 'done' | 'error') => {
394+
logger.log(`${step}: ${status}`);
395+
},
396+
};
397+
398+
if (imperativeManager.hasDeployersForPhase('post-cdk', teardownContext)) {
399+
startStep('Tear down imperative resources');
400+
const imperativeTeardown = await imperativeManager.teardownAll(teardownContext);
401+
if (!imperativeTeardown.success) {
402+
endStep('error', imperativeTeardown.error);
403+
logger.finalize(false);
404+
return {
405+
success: false,
406+
error: new Error(`Imperative teardown failed: ${imperativeTeardown.error}`),
407+
logPath: logger.getRelativeLogPath(),
408+
};
409+
}
410+
endStep('success');
411+
}
412+
}
413+
384414
startStep('Tear down stack');
385415
const teardown = await performStackTeardown(target.name);
386416
if (!teardown.success) {

src/cli/commands/dev/browser-mode.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../operations/dev/web-ui';
1212
import type { HarnessInfo } from '../../operations/dev/web-ui/constants';
1313
import { listMemoryRecords, retrieveMemoryRecords } from '../../operations/memory';
14-
import { loadDeployedProjectConfig, resolveAgent } from '../../operations/resolve-agent';
14+
import { loadDeployedProjectConfig, resolveAgentOrHarness } from '../../operations/resolve-agent';
1515
import { fetchTraceRecords, listTraces } from '../../operations/traces';
1616
import { LayoutProvider } from '../../tui/context';
1717
import { runCliDeploy } from '../deploy/progress';
@@ -255,11 +255,11 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
255255
? (agentNameParam, startTime, endTime) => collector.listTraces(agentNameParam, startTime, endTime)
256256
: undefined,
257257
onGetTrace: collector ? (agentNameParam, traceId) => collector.getTraceSpans(agentNameParam, traceId) : undefined,
258-
onListCloudWatchTraces: async (agentName, _harnessName, startTime, endTime) => {
258+
onListCloudWatchTraces: async (agentName, harnessName, startTime, endTime) => {
259259
try {
260260
const configIO = new ConfigIO({ baseDir });
261261
const context = await loadDeployedProjectConfig(configIO);
262-
const resolved = resolveAgent(context, { runtime: agentName });
262+
const resolved = await resolveAgentOrHarness(context, { runtime: agentName, harness: harnessName });
263263
if (!resolved.success) return { success: false, error: resolved.error };
264264
const res = await listTraces({
265265
region: resolved.agent.region,
@@ -276,11 +276,11 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
276276
};
277277
}
278278
},
279-
onGetCloudWatchTrace: async (agentName, _harnessName, traceId, startTime, endTime) => {
279+
onGetCloudWatchTrace: async (agentName, harnessName, traceId, startTime, endTime) => {
280280
try {
281281
const configIO = new ConfigIO({ baseDir });
282282
const context = await loadDeployedProjectConfig(configIO);
283-
const resolved = resolveAgent(context, { runtime: agentName });
283+
const resolved = await resolveAgentOrHarness(context, { runtime: agentName, harness: harnessName });
284284
if (!resolved.success) return { success: false, error: resolved.error };
285285
const res = await fetchTraceRecords({
286286
region: resolved.agent.region,

src/cli/operations/resolve-agent.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ConfigIO } from '../../lib';
22
import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../schema';
3+
import { getHarness } from '../aws/agentcore-harness';
34

45
export interface DeployedProjectConfig {
56
project: AgentCoreProjectSpec;
@@ -97,3 +98,126 @@ export function resolveAgent(
9798
},
9899
};
99100
}
101+
102+
/**
103+
* Resolves a harness to a ResolvedAgent by looking up deployed state and
104+
* fetching the underlying agentRuntimeArn via the GetHarness API.
105+
*/
106+
export async function resolveHarness(
107+
context: DeployedProjectConfig,
108+
harnessName: string
109+
): Promise<{ success: true; agent: ResolvedAgent } | { success: false; error: string }> {
110+
const { project, deployedState, awsTargets } = context;
111+
112+
const harnesses = project.harnesses ?? [];
113+
const harnessSpec = harnesses.find(h => h.name === harnessName);
114+
if (!harnessSpec) {
115+
const available = harnesses.map(h => h.name);
116+
return {
117+
success: false,
118+
error:
119+
available.length > 0
120+
? `Harness '${harnessName}' not found. Available: ${available.join(', ')}`
121+
: 'No harnesses defined in agentcore.json',
122+
};
123+
}
124+
125+
const targetNames = Object.keys(deployedState.targets);
126+
if (targetNames.length === 0) {
127+
return { success: false, error: 'No deployed targets found. Run `agentcore deploy` first.' };
128+
}
129+
const selectedTargetName = targetNames[0]!;
130+
131+
const targetState = deployedState.targets[selectedTargetName];
132+
const targetConfig = awsTargets.find(t => t.name === selectedTargetName);
133+
134+
if (!targetConfig) {
135+
return { success: false, error: `Target config '${selectedTargetName}' not found in aws-targets` };
136+
}
137+
138+
const harnessState = targetState?.resources?.harnesses?.[harnessName];
139+
if (!harnessState) {
140+
return {
141+
success: false,
142+
error: `Harness '${harnessName}' is not deployed to target '${selectedTargetName}'. Run 'agentcore deploy' first.`,
143+
};
144+
}
145+
146+
let runtimeId: string | undefined;
147+
148+
if (harnessState.agentRuntimeArn) {
149+
const arnMatch = /runtime\/([^/]+)/.exec(harnessState.agentRuntimeArn);
150+
if (arnMatch) {
151+
runtimeId = arnMatch[1];
152+
}
153+
}
154+
155+
if (!runtimeId) {
156+
try {
157+
await getHarness({ region: targetConfig.region, harnessId: harnessState.harnessId });
158+
runtimeId = harnessState.harnessId;
159+
} catch (err) {
160+
return {
161+
success: false,
162+
error: `Failed to resolve runtime for harness '${harnessName}': ${(err as Error).message}`,
163+
};
164+
}
165+
}
166+
167+
if (!runtimeId) {
168+
return {
169+
success: false,
170+
error: `Could not resolve runtime ID for harness '${harnessName}'. Re-deploy to populate agentRuntimeArn.`,
171+
};
172+
}
173+
174+
return {
175+
success: true,
176+
agent: {
177+
agentName: harnessName,
178+
targetName: selectedTargetName,
179+
region: targetConfig.region,
180+
accountId: targetConfig.account,
181+
runtimeId,
182+
},
183+
};
184+
}
185+
186+
/**
187+
* Resolves either an agent runtime or a harness to a ResolvedAgent.
188+
* - If --harness is specified, resolves that harness.
189+
* - If --runtime is specified, resolves that runtime.
190+
* - If neither is specified, auto-selects: single runtime wins, or if no runtimes
191+
* but harnesses exist, auto-selects the single harness.
192+
*/
193+
export async function resolveAgentOrHarness(
194+
context: DeployedProjectConfig,
195+
options: { runtime?: string; harness?: string }
196+
): Promise<{ success: true; agent: ResolvedAgent } | { success: false; error: string }> {
197+
if (options.harness && options.runtime) {
198+
return { success: false, error: 'Cannot specify both --harness and --runtime' };
199+
}
200+
201+
if (options.harness) {
202+
return resolveHarness(context, options.harness);
203+
}
204+
205+
if (options.runtime || context.project.runtimes.length > 0) {
206+
return resolveAgent(context, options);
207+
}
208+
209+
const harnesses = context.project.harnesses ?? [];
210+
if (harnesses.length === 0) {
211+
return { success: false, error: 'No runtimes or harnesses defined in agentcore.json' };
212+
}
213+
214+
if (harnesses.length > 1) {
215+
const names = harnesses.map(h => h.name);
216+
return {
217+
success: false,
218+
error: `Multiple harnesses found. Use --harness to specify one: ${names.join(', ')}`,
219+
};
220+
}
221+
222+
return resolveHarness(context, harnesses[0]!.name);
223+
}

src/cli/tui/screens/dev/DevScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ export function DevScreen(props: DevScreenProps) {
484484
}
485485
}
486486
},
487-
{ isActive: mode === 'chat' || mode === 'select-agent' }
487+
{ isActive: (mode === 'chat' || mode === 'select-agent') && !isExiting }
488488
);
489489

490490
// Return null while loading (harness mode doesn't need dev server config)

src/cli/tui/screens/invoke/InvokeScreen.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ function formatConversation(
4848
// Skip empty assistant messages (placeholder before streaming starts)
4949
if (msg.role === 'assistant' && !msg.content) continue;
5050

51-
if (msg.role === 'user' && msg.isExec) {
51+
if (msg.isHint) {
52+
lines.push({ text: msg.content, color: 'gray' });
53+
} else if (msg.role === 'user' && msg.isExec) {
5254
lines.push({ text: msg.content, color: 'magenta' });
5355
} else if (msg.role === 'user') {
5456
lines.push({ text: `> ${msg.content}`, color: 'blue' });
@@ -355,7 +357,7 @@ export function InvokeScreen({
355357
}
356358

357359
// New session
358-
if (input === 'n' && phase === 'ready') {
360+
if (key.ctrl && input === 'n' && phase === 'ready') {
359361
newSession();
360362
setScrollOffset(0);
361363
setUserScrolled(false);
@@ -450,9 +452,9 @@ export function InvokeScreen({
450452
: phase === 'invoking'
451453
? '↑↓ scroll'
452454
: messages.length > 0
453-
? `↑↓ scroll · Enter invoke · N new session · ${backOrQuit}`
455+
? `↑↓ scroll · Enter invoke · Ctrl+N new session · ${backOrQuit}`
454456
: isMcp
455-
? `Enter to call a tool · N new session · ${backOrQuit}`
457+
? `Enter to call a tool · Ctrl+N new session · ${backOrQuit}`
456458
: `Enter to send a message · ${backOrQuit}`;
457459

458460
const headerContent = (
@@ -551,14 +553,16 @@ export function InvokeScreen({
551553
</Text>
552554
))}
553555
{/* Thinking indicator - shows while waiting for response to start */}
554-
{showThinking && <GradientText text="Thinking..." />}
556+
{showThinking && <GradientText text={isExecInput ? 'Loading...' : 'Thinking...'} />}
555557
</Box>
556558
)}
557559

558560
{/* Scroll indicator */}
559561
{needsScroll && (
560562
<Text dimColor>
561-
[{effectiveOffset + 1}-{Math.min(effectiveOffset + displayHeight, totalLines)} of {totalLines}]
563+
{effectiveOffset > 0 ? '▲ ' : ' '}
564+
↑↓ scroll
565+
{effectiveOffset < maxScroll ? ' ▼' : ' '}
562566
</Text>
563567
)}
564568

0 commit comments

Comments
 (0)