improvements and hardening

This commit is contained in:
Samee Zahid
2026-04-15 10:48:03 -07:00
parent 27f35c3358
commit 042bd7fd40
15 changed files with 300 additions and 180 deletions
+16
View File
@@ -436,6 +436,7 @@ export const AppContainer = (props: AppContainerProps) => {
const [isOfflineMode, setIsOfflineMode] = useState(
config.isOfflineModeEnabled(),
);
const [cloudSubagentActive, setCloudSubagentActive] = useState(false);
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
const [quotaStats, setQuotaStats] = useState<QuotaStats | undefined>(() => {
@@ -573,6 +574,11 @@ export const AppContainer = (props: AppContainerProps) => {
const handleOfflineModeChanged = (payload: { enabled: boolean }) => {
setIsOfflineMode(payload.enabled);
};
const handleCloudSubagentExecution = (payload: {
state: 'started' | 'ended';
}) => {
setCloudSubagentActive(payload.state === 'started');
};
const handleQuotaChanged = (payload: {
remaining: number | undefined;
@@ -588,10 +594,18 @@ export const AppContainer = (props: AppContainerProps) => {
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
coreEvents.on(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
coreEvents.on(
CoreEvent.CloudSubagentExecution,
handleCloudSubagentExecution,
);
coreEvents.on(CoreEvent.QuotaChanged, handleQuotaChanged);
return () => {
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
coreEvents.off(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
coreEvents.off(
CoreEvent.CloudSubagentExecution,
handleCloudSubagentExecution,
);
coreEvents.off(CoreEvent.QuotaChanged, handleQuotaChanged);
};
}, [config]);
@@ -2502,6 +2516,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
showApprovalModeIndicator,
allowPlanMode,
isOfflineMode,
cloudSubagentActive,
currentModel,
contextFileNames,
errorCount,
@@ -2614,6 +2629,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
showApprovalModeIndicator,
allowPlanMode,
isOfflineMode,
cloudSubagentActive,
contextFileNames,
errorCount,
availableTerminalHeight,
@@ -0,0 +1,43 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Text } from 'ink';
import { usePulsingColor } from '../hooks/usePulsingColor.js';
interface PulsingDotProps {
/** Full-brightness color */
color: string;
/** Dim color at the trough of the pulse */
dimColor: string;
/** Duration of one full pulse cycle in ms */
cycleDurationMs: number;
/** Whether the dot is actively pulsing. When false, renders static at full color. */
active: boolean;
/** Optional label text rendered after the dot */
label?: string;
}
export const PulsingDot: React.FC<PulsingDotProps> = ({
color,
dimColor,
cycleDurationMs,
active,
label,
}) => {
const currentColor = usePulsingColor(
color,
dimColor,
cycleDurationMs,
active,
);
return (
<Text color={currentColor}>
{active ? '◉' : '●'} {label}
</Text>
);
};
@@ -47,6 +47,8 @@ describe('<StatusRow />', () => {
showWit: true,
modeContentObj: null,
showMinimalContext: false,
isOfflineMode: false,
cloudSubagentActive: false,
});
const uiState: Partial<UIState> = {
@@ -87,6 +89,8 @@ describe('<StatusRow />', () => {
showWit: false,
modeContentObj: null,
showMinimalContext: false,
isOfflineMode: false,
cloudSubagentActive: false,
});
const { lastFrame, waitUntilReady } = await renderWithProviders(
@@ -116,6 +120,8 @@ describe('<StatusRow />', () => {
showWit: true,
modeContentObj: null,
showMinimalContext: false,
isOfflineMode: false,
cloudSubagentActive: false,
});
const uiState: Partial<UIState> = {
@@ -150,6 +156,8 @@ describe('<StatusRow />', () => {
showWit: false,
modeContentObj: null,
showMinimalContext: false,
isOfflineMode: true,
cloudSubagentActive: false,
});
const uiState: Partial<UIState> = {
@@ -175,4 +183,43 @@ describe('<StatusRow />', () => {
await waitUntilReady();
expect(lastFrame()).toContain('offline');
});
it('renders cloud indicator when cloud subagent is active', async () => {
(useComposerStatus as Mock).mockReturnValue({
isInteractiveShellWaiting: false,
showLoadingIndicator: false,
showTips: false,
showWit: false,
modeContentObj: null,
showMinimalContext: false,
isOfflineMode: true,
cloudSubagentActive: true,
});
const uiState: Partial<UIState> = {
...defaultUiState,
isOfflineMode: true,
cloudSubagentActive: true,
};
const { lastFrame, waitUntilReady } = await renderWithProviders(
<StatusRow
showUiDetails={true}
isNarrow={false}
terminalWidth={100}
hideContextSummary={false}
hideUiDetailsForSuggestions={false}
hasPendingActionRequired={false}
/>,
{
width: 100,
uiState,
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('offline');
expect(output).toContain('cloud');
});
});
+32 -4
View File
@@ -12,7 +12,7 @@ import {
type ThoughtSummary,
} from '@google/gemini-cli-core';
import stripAnsi from 'strip-ansi';
import { type ActiveHook } from '../types.js';
import { type ActiveHook, StreamingState } from '../types.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { theme } from '../semantic-colors.js';
@@ -25,7 +25,9 @@ import { HorizontalLine } from './shared/HorizontalLine.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
import { PulsingDot } from './PulsingDot.js';
import { useComposerStatus } from '../hooks/useComposerStatus.js';
import { useStreamingContext } from '../contexts/StreamingContext.js';
/**
* Layout constants to prevent magic numbers.
@@ -173,7 +175,14 @@ export const StatusRow: React.FC<StatusRowProps> = ({
showWit,
modeContentObj,
showMinimalContext,
isOfflineMode,
cloudSubagentActive,
} = useComposerStatus();
const streamingState = useStreamingContext();
const isLocalActive =
isOfflineMode &&
streamingState === StreamingState.Responding &&
!cloudSubagentActive;
const [statusWidth, setStatusWidth] = useState(0);
const [tipWidth, setTipWidth] = useState(0);
@@ -411,9 +420,28 @@ export const StatusRow: React.FC<StatusRowProps> = ({
<RawMarkdownIndicator />
</Box>
)}
{uiState.isOfflineMode && (
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
<Text color={theme.status.success}> offline</Text>
{isOfflineMode && (
<Box
marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}
flexDirection="row"
gap={1}
>
<PulsingDot
color={theme.status.success}
dimColor={theme.ui.dark}
cycleDurationMs={1500}
active={isLocalActive}
label="offline"
/>
{cloudSubagentActive && (
<PulsingDot
color={theme.status.warning}
dimColor={theme.ui.dark}
cycleDurationMs={800}
active={true}
label="cloud"
/>
)}
</Box>
)}
</>
@@ -62,18 +62,25 @@ export const SubagentProgressDisplay: React.FC<
let headerText: string | undefined;
let headerColor = theme.text.secondary;
const isCloud =
progress.agentName === 'cloud-subagent' ||
progress.agentName === 'cloud_subagent';
const prefix = isCloud ? '☁ Cloud' : `Subagent ${progress.agentName}`;
if (progress.state === 'cancelled') {
headerText = `Subagent ${progress.agentName} was cancelled.`;
headerText = `${prefix} was cancelled.`;
headerColor = theme.status.warning;
} else if (progress.state === 'error') {
headerText = `Subagent ${progress.agentName} failed.`;
headerText = `${prefix} failed.`;
headerColor = theme.status.error;
} else if (progress.state === 'completed') {
headerText = `Subagent ${progress.agentName} completed.`;
headerText = `${prefix} completed.`;
headerColor = theme.status.success;
} else {
headerText = `Running subagent ${progress.agentName}...`;
headerColor = theme.text.primary;
headerText = isCloud
? `☁ Running cloud subagent...`
: `Running subagent ${progress.agentName}...`;
headerColor = isCloud ? theme.status.warning : theme.text.primary;
}
return (
@@ -100,9 +100,9 @@ describe('ToolConfirmationMessage', () => {
it('should use allow/always allow/deny labels for cloud-subagent confirmations', async () => {
const confirmationDetails: SerializableConfirmationDetails = {
type: 'info',
title: 'Delegate to cloud-subagent',
title: 'Delegate to cloud subagent',
prompt:
'Delegating to cloud-subagent for cloud execution.\nReason: Complex task.\nTask: Analyze migration risks.',
'This will run with full tool access in cloud mode.\n\nAnalyze migration risks across the codebase.',
};
const { lastFrame, unmount } = await renderWithProviders(
@@ -372,7 +372,9 @@ export const ToolConfirmationMessage: React.FC<
});
} else if (confirmationDetails.type === 'info') {
const isCloudSubagentConfirmation =
toolName === 'cloud-subagent' || toolName === 'cloud_subagent';
toolName === 'cloud-subagent' ||
toolName === 'cloud_subagent' ||
confirmationDetails.title?.includes('cloud subagent');
options.push({
label: isCloudSubagentConfirmation ? 'Allow' : 'Allow once',
@@ -8,7 +8,7 @@ exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml
4. Modify with external editor
@@ -16,92 +16,6 @@ Apply this change?
"
`;
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large edit diffs 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ... 10 hidden (Ctrl+O) ... │
│ 6 - const oldLine6 = true; │
│ 6 + const newLine6 = true; │
│ 7 - const oldLine7 = true; │
│ 7 + const newLine7 = true; │
│ 8 - const oldLine8 = true; │
│ 8 + const newLine8 = true; │
│ 9 - const oldLine9 = true; │
│ 9 + const newLine9 = true; │
│ 10 - const oldLine10 = true; │
│ 10 + const newLine10 = true; │
│ 11 - const oldLine11 = true; │
│ 11 + const newLine11 = true; │
│ 12 - const oldLine12 = true; │
│ 12 + const newLine12 = true; │
│ 13 - const oldLine13 = true; │
│ 13 + const newLine13 = true; │
│ 14 - const oldLine14 = true; │
│ 14 + const newLine14 = true; │
│ 15 - const oldLine15 = true; │
│ 15 + const newLine15 = true; │
│ 16 - const oldLine16 = true; │
│ 16 + const newLine16 = true; │
│ 17 - const oldLine17 = true; │
│ 17 + const newLine17 = true; │
│ 18 - const oldLine18 = true; │
│ 18 + const newLine18 = true; │
│ 19 - const oldLine19 = true; │
│ 19 + const newLine19 = true; │
│ 20 - const oldLine20 = true; │
│ 20 + const newLine20 = true; │
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
2. Allow for this session
3. Modify with external editor
4. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large exec commands 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ... 19 hidden (Ctrl+O) ... │
│ echo "Line 20" │
│ echo "Line 21" │
│ echo "Line 22" │
│ echo "Line 23" │
│ echo "Line 24" │
│ echo "Line 25" │
│ echo "Line 26" │
│ echo "Line 27" │
│ echo "Line 28" │
│ echo "Line 29" │
│ echo "Line 30" │
│ echo "Line 31" │
│ echo "Line 32" │
│ echo "Line 33" │
│ echo "Line 34" │
│ echo "Line 35" │
│ echo "Line 36" │
│ echo "Line 37" │
│ echo "Line 38" │
│ echo "Line 39" │
│ echo "Line 40" │
│ echo "Line 41" │
│ echo "Line 42" │
│ echo "Line 43" │
│ echo "Line 44" │
│ echo "Line 45" │
│ echo "Line 46" │
│ echo "Line 47" │
│ echo "Line 48" │
│ echo "Line 49" │
│ echo "Line 50" │
╰──────────────────────────────────────────────────────────────────────────────╯
Allow execution of [echo]?
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ echo "hello" │
@@ -112,7 +26,7 @@ exports[`ToolConfirmationMessage > should display multiple commands for exec typ
╰──────────────────────────────────────────────────────────────────────────────╯
Allow execution of [echo, ls, whoami]?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -125,7 +39,7 @@ URLs to fetch:
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -135,32 +49,18 @@ exports[`ToolConfirmationMessage > should not display urls if prompt and url are
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ echo "hello" │
│ for i in 1 2 3; do │
│ echo $i │
│ done │
╰──────────────────────────────────────────────────────────────────────────────╯
Allow execution of [echo]?
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)"
`;
exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool and server names 1`] = `
"MCP Server: testserver
Tool: testtool
Allow execution of MCP tool "testtool" from server "testserver"?
● 1. Allow once
● 1. Allow once
2. Allow tool for this session
3. Allow all server tools for this session
4. No, suggest changes (esc)
@@ -175,7 +75,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
● 1. Allow once
2. Modify with external editor
3. No, suggest changes (esc)
"
@@ -189,7 +89,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. Modify with external editor
4. No, suggest changes (esc)
@@ -202,7 +102,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Allow execution of [echo]?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -213,7 +113,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Allow execution of [echo]?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -223,7 +123,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -232,7 +132,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -243,7 +143,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -253,7 +153,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Allow once
● 1. Allow once
2. Allow tool for this session
3. Allow all server tools for this session
4. No, suggest changes (esc)
@@ -158,6 +158,7 @@ export interface UIState {
showApprovalModeIndicator: ApprovalMode;
allowPlanMode: boolean;
isOfflineMode?: boolean;
cloudSubagentActive?: boolean;
currentModel: string;
contextFileNames: string[];
errorCount: number;
+15 -2
View File
@@ -22,6 +22,7 @@ export const useComposerStatus = () => {
const quotaState = useQuotaState();
const settings = useSettings();
const isOfflineMode = Boolean(uiState.isOfflineMode);
const cloudSubagentActive = Boolean(uiState.cloudSubagentActive);
const hasPendingToolConfirmation = useMemo(
() =>
@@ -80,14 +81,23 @@ export const useComposerStatus = () => {
})();
if (approvalModeIndicator) {
return isOfflineMode
const suffix = cloudSubagentActive
? ' + cloud'
: isOfflineMode
? ' + offline'
: '';
return suffix
? {
text: `${approvalModeIndicator.text} + offline`,
text: `${approvalModeIndicator.text}${suffix}`,
color: approvalModeIndicator.color,
}
: approvalModeIndicator;
}
if (cloudSubagentActive) {
return { text: 'cloud', color: theme.status.warning };
}
if (isOfflineMode) {
return { text: 'offline', color: theme.status.success };
}
@@ -99,6 +109,7 @@ export const useComposerStatus = () => {
uiState.activeHooks.length,
showApprovalModeIndicator,
isOfflineMode,
cloudSubagentActive,
]);
const showMinimalContext = isContextUsageHigh(
@@ -127,5 +138,7 @@ export const useComposerStatus = () => {
showWit,
modeContentObj,
showMinimalContext,
isOfflineMode,
cloudSubagentActive,
};
};
@@ -0,0 +1,45 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect } from 'react';
import { interpolateColor } from '../themes/color-utils.js';
const FRAME_INTERVAL_MS = 60; // ~16fps — smooth enough for a pulse, cheap on CPU
/**
* Returns a color that pulses between `activeColor` and `dimColor` on a sine
* curve. When `active` is false the hook stops its timer and returns
* `activeColor` at full brightness (static dot).
*/
export function usePulsingColor(
activeColor: string,
dimColor: string,
cycleDurationMs: number,
active: boolean,
): string {
const [time, setTime] = useState(0);
useEffect(() => {
if (!active) {
setTime(0);
return;
}
const interval = setInterval(() => {
setTime((prev) => prev + FRAME_INTERVAL_MS);
}, FRAME_INTERVAL_MS);
return () => clearInterval(interval);
}, [active]);
if (!active) {
return activeColor;
}
// Sine oscillation: 0 → 1 → 0 over one cycle
const progress = (Math.sin((2 * Math.PI * time) / cycleDurationMs) + 1) / 2;
return interpolateColor(dimColor, activeColor, progress) || activeColor;
}
+3 -4
View File
@@ -67,10 +67,9 @@ describe('AgentTool', () => {
inputSchema: {
type: 'object',
properties: {
task: { type: 'string' },
reason: { type: 'string' },
request: { type: 'string' },
},
required: ['task', 'reason'],
required: ['request'],
},
},
modelConfig: { model: 'test', generateContentConfig: {} },
@@ -194,7 +193,7 @@ describe('AgentTool', () => {
expect(result).toMatchObject({
type: 'info',
title: 'Delegate to cloud-subagent',
title: 'Delegate to cloud subagent',
});
// Should NOT delegate to child invocation for confirmation
expect(LocalSubagentInvocation).not.toHaveBeenCalled();
+40 -35
View File
@@ -30,30 +30,18 @@ import {
} from '../telemetry/constants.js';
import { AGENT_TOOL_NAME } from '../tools/tool-names.js';
import { CLOUD_SUBAGENT_NAME } from './cloud-subagent.js';
import { coreEvents } from '../utils/events.js';
const CLOUD_DELEGATION_REASON_MAX_LENGTH = 120;
const CLOUD_DELEGATION_TASK_MAX_LENGTH = 140;
const CLOUD_DELEGATION_REASON_FALLBACK =
'Complex work is better handled by the cloud subagent.';
const CLOUD_DELEGATION_TASK_FALLBACK = 'No task summary provided.';
const CLOUD_DELEGATION_PROMPT_MAX_LENGTH = 280;
function summarizeInputText(
value: unknown,
maxLength: number,
): string | undefined {
if (typeof value !== 'string') {
return undefined;
function truncateText(value: unknown, maxLength: number): string {
if (typeof value !== 'string' || !value.trim()) {
return 'Cloud delegation requested.';
}
const normalized = value.replace(/\s+/g, ' ').trim();
if (!normalized) {
return undefined;
}
if (normalized.length <= maxLength) {
return normalized;
}
return `${normalized.slice(0, maxLength - 3)}...`;
}
@@ -225,21 +213,15 @@ class DelegateInvocation extends BaseToolInvocation<
return false;
}
const reason =
summarizeInputText(
this.mappedInputs['reason'],
CLOUD_DELEGATION_REASON_MAX_LENGTH,
) ?? CLOUD_DELEGATION_REASON_FALLBACK;
const task =
summarizeInputText(
this.mappedInputs['task'],
CLOUD_DELEGATION_TASK_MAX_LENGTH,
) ?? CLOUD_DELEGATION_TASK_FALLBACK;
const prompt = truncateText(
this.mappedInputs['request'] ?? this.params.prompt,
CLOUD_DELEGATION_PROMPT_MAX_LENGTH,
);
return {
type: 'info',
title: 'Delegate to cloud-subagent',
prompt: [`Reason: ${reason}`, `Task: ${task}`].join('\n'),
title: 'Delegate to cloud subagent',
prompt: `This will run with full tool access in cloud mode.\n\n${prompt}`,
onConfirm: async (_outcome) => {
// Policy updates are handled centrally by the scheduler.
},
@@ -250,6 +232,11 @@ class DelegateInvocation extends BaseToolInvocation<
const { abortSignal: signal, updateOutput } = options;
const hintedParams = this.withUserHints(this.mappedInputs);
const invocation = this.buildChildInvocation(hintedParams);
const isCloud = this.definition.name === CLOUD_SUBAGENT_NAME;
if (isCloud) {
coreEvents.emitCloudSubagentExecution(this.definition.name, 'started');
}
return runInDevTraceSpan(
{
@@ -263,12 +250,30 @@ class DelegateInvocation extends BaseToolInvocation<
},
async ({ metadata }) => {
metadata.input = this.params;
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
metadata.output = result;
return result;
try {
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
metadata.output = result;
if (isCloud) {
coreEvents.emitCloudSubagentExecution(
this.definition.name,
'ended',
'success',
);
}
return result;
} catch (error) {
if (isCloud) {
coreEvents.emitCloudSubagentExecution(
this.definition.name,
'ended',
signal.aborted ? 'cancelled' : 'error',
);
}
throw error;
}
},
);
}
+4 -14
View File
@@ -31,17 +31,13 @@ export const CloudSubagent = (
inputSchema: {
type: 'object',
properties: {
task: {
type: 'string',
description: 'The delegated task to execute in the cloud context.',
},
reason: {
request: {
type: 'string',
description:
'Why delegation is necessary (complexity, volume, uncertainty, or long-running work).',
'The delegated task to execute in the cloud context. Include both what to do and why cloud delegation is justified.',
},
},
required: ['task', 'reason'],
required: ['request'],
},
},
outputConfig: {
@@ -63,13 +59,7 @@ export const CloudSubagent = (
},
get promptConfig() {
return {
query: `You are handling a delegated cloud task from the offline-mode orchestrator.
Delegation reason:
${'${reason}'}
Task:
${'${task}'}`,
query: '${request}',
systemPrompt: `${getCoreSystemPrompt(
context.config,
/* useMemory */ undefined,
+24
View File
@@ -62,6 +62,15 @@ export interface OfflineModeChangedPayload {
enabled: boolean;
}
/**
* Payload for cloud subagent execution lifecycle events.
*/
export interface CloudSubagentExecutionPayload {
agentName: string;
state: 'started' | 'ended';
outcome?: 'success' | 'error' | 'cancelled';
}
/**
* Payload for the 'console-log' event.
*/
@@ -192,6 +201,7 @@ export enum CoreEvent {
UserFeedback = 'user-feedback',
ModelChanged = 'model-changed',
OfflineModeChanged = 'offline-mode-changed',
CloudSubagentExecution = 'cloud-subagent-execution',
ConsoleLog = 'console-log',
Output = 'output',
MemoryChanged = 'memory-changed',
@@ -227,6 +237,7 @@ export interface CoreEvents extends ExtensionEvents {
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
[CoreEvent.ModelChanged]: [ModelChangedPayload];
[CoreEvent.OfflineModeChanged]: [OfflineModeChangedPayload];
[CoreEvent.CloudSubagentExecution]: [CloudSubagentExecutionPayload];
[CoreEvent.ConsoleLog]: [ConsoleLogPayload];
[CoreEvent.Output]: [OutputPayload];
[CoreEvent.MemoryChanged]: [MemoryChangedPayload];
@@ -344,6 +355,19 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
this.emit(CoreEvent.OfflineModeChanged, payload);
}
emitCloudSubagentExecution(
agentName: string,
state: 'started' | 'ended',
outcome?: 'success' | 'error' | 'cancelled',
): void {
const payload: CloudSubagentExecutionPayload = {
agentName,
state,
outcome,
};
this.emit(CoreEvent.CloudSubagentExecution, payload);
}
/**
* Notifies subscribers that settings have been modified.
*/