prompt-language-shell 0.9.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,10 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
37
37
  // Track working directory across commands (persists cd changes)
38
38
  const workdirRef = useRef(undefined);
39
39
  // Ref to collect live output during execution (dispatched every second)
40
- const outputRef = useRef({ stdout: '', stderr: '' });
40
+ const outputRef = useRef({
41
+ chunks: [],
42
+ currentMemory: undefined,
43
+ });
41
44
  // Ref to track if current task execution is cancelled
42
45
  const cancelledRef = useRef(false);
43
46
  const { error, tasks, message, hasProcessed, completionMessage, summary } = localState;
@@ -62,8 +65,8 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
62
65
  index: currentTaskIndex,
63
66
  elapsed,
64
67
  output: {
65
- stdout: outputRef.current.stdout,
66
- stderr: outputRef.current.stderr,
68
+ chunks: outputRef.current.chunks,
69
+ currentMemory: outputRef.current.currentMemory,
67
70
  },
68
71
  },
69
72
  });
@@ -83,8 +86,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
83
86
  ...task,
84
87
  status: ExecutionStatus.Aborted,
85
88
  output: {
86
- stdout: outputRef.current.stdout,
87
- stderr: outputRef.current.stderr,
89
+ chunks: outputRef.current.chunks,
88
90
  },
89
91
  };
90
92
  }
@@ -139,9 +141,9 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
139
141
  lifecycleHandlers.completeActive();
140
142
  return;
141
143
  }
142
- // Create task data from commands
143
- const tasks = result.commands.map((cmd, index) => ({
144
- label: inputTasks[index]?.action ?? cmd.description,
144
+ // Create task data from commands - use descriptions from execute response
145
+ const tasks = result.commands.map((cmd) => ({
146
+ label: cmd.description,
145
147
  command: cmd,
146
148
  status: ExecutionStatus.Pending,
147
149
  elapsed: 0,
@@ -207,7 +209,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
207
209
  payload: { index: currentTaskIndex, startTime: Date.now() },
208
210
  });
209
211
  // Reset output ref for new task
210
- outputRef.current = { stdout: '', stderr: '' };
212
+ outputRef.current = { chunks: [], currentMemory: undefined };
211
213
  // Merge workdir into command
212
214
  const command = workdirRef.current
213
215
  ? { ...currentTask.command, workdir: workdirRef.current }
@@ -215,7 +217,10 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
215
217
  void executeTask(command, currentTaskIndex, {
216
218
  onUpdate: (output) => {
217
219
  if (!cancelledRef.current) {
218
- outputRef.current = { stdout: output.stdout, stderr: output.stderr };
220
+ outputRef.current = {
221
+ chunks: output.chunks,
222
+ currentMemory: output.currentMemory,
223
+ };
219
224
  }
220
225
  },
221
226
  onComplete: (elapsed, execOutput) => {
@@ -229,8 +234,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
229
234
  ? {
230
235
  ...task,
231
236
  output: {
232
- stdout: execOutput.stdout,
233
- stderr: execOutput.stderr,
237
+ chunks: execOutput.chunks,
234
238
  },
235
239
  }
236
240
  : task);
@@ -256,8 +260,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
256
260
  ? {
257
261
  ...task,
258
262
  output: {
259
- stdout: execOutput.stdout,
260
- stderr: execOutput.stderr,
263
+ chunks: execOutput.chunks,
261
264
  },
262
265
  error: execOutput.error || undefined,
263
266
  }
@@ -54,10 +54,8 @@ export function Introspect({ tasks, status, service, children, debug = DebugLeve
54
54
  message,
55
55
  };
56
56
  requestHandlers.onCompleted(finalState);
57
- // Add Report component to queue
58
- workflowHandlers.addToQueue(createReport({ message, capabilities }));
59
- // Signal completion
60
- lifecycleHandlers.completeActive();
57
+ // Signal completion and add Report to timeline (not queue, to preserve order)
58
+ lifecycleHandlers.completeActive(createReport({ message, capabilities }));
61
59
  }
62
60
  }
63
61
  catch (err) {
@@ -90,8 +90,9 @@ export function Validate({ missingConfig, userRequest, status, service, onError,
90
90
  },
91
91
  });
92
92
  // Override descriptions with LLM-generated ones
93
- if ('props' in configDef && 'steps' in configDef.props) {
94
- configDef.props.steps = configDef.props.steps.map((step, index) => ({
93
+ const configProps = configDef.props;
94
+ if (configProps.steps) {
95
+ configProps.steps = configProps.steps.map((step, index) => ({
95
96
  ...step,
96
97
  description: withDescriptions[index].description ||
97
98
  withDescriptions[index].path,
@@ -56,5 +56,5 @@ export const ExecuteView = ({ isLoading, isExecuting, isActive, error, message,
56
56
  const isTerminated = upcomingStatus !== ExecutionStatus.Pending;
57
57
  // Show upcoming during active execution or when terminated (to show skipped tasks)
58
58
  const showUpcoming = upcoming && upcoming.length > 0 && (isActive || isTerminated);
59
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [label && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: getTextColor(isActive), children: label }) })), _jsxs(Box, { marginLeft: label ? 2 : 0, children: [_jsx(Text, { color: Palette.Gray, children: "Preparing commands. " }), _jsx(Spinner, {})] })] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((task, index) => (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(TaskView, { label: task.label, command: task.command, status: task.status, elapsed: task.elapsed, output: task.output, isFinished: isTaskFinished(task), isActive: isActive }) }, index)))] })), showUpcoming && (_jsx(Box, { marginTop: 1, children: _jsx(Upcoming, { items: upcoming, status: upcomingStatus }) })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: error }) }))] }));
59
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [label && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: getTextColor(isActive), children: label }) })), _jsxs(Box, { marginLeft: label ? 2 : 0, children: [_jsx(Text, { color: Palette.Gray, children: "Preparing commands. " }), _jsx(Spinner, {})] })] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((task, index) => (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(TaskView, { label: task.label, command: task.command, status: task.status, elapsed: task.elapsed, currentMemory: task.output?.currentMemory, output: task.output, isFinished: isTaskFinished(task), isActive: isActive }) }, index)))] })), showUpcoming && (_jsx(Box, { marginTop: 1, children: _jsx(Upcoming, { items: upcoming, status: upcomingStatus }) })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: error }) }))] }));
60
60
  };
@@ -1,54 +1,108 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Palette } from '../../services/colors.js';
4
4
  import { ExecutionStatus } from '../../services/shell.js';
5
5
  const MAX_LINES = 8;
6
6
  const MAX_WIDTH = 75;
7
- const SHORT_OUTPUT_THRESHOLD = 4;
8
- const MINIMAL_INFO_THRESHOLD = 2;
7
+ const INCOMPLETE_LINE_DELAY = 3000;
9
8
  /**
10
- * Get the last N lines from text, filtering out empty/whitespace-only lines
9
+ * Determine if an incomplete line should be shown.
10
+ * Shows incomplete lines if they are older than the delay threshold.
11
11
  */
12
- export function getLastLines(text, maxLines = MAX_LINES) {
13
- const lines = text
12
+ function shouldShowIncompleteLine(lastChunk) {
13
+ return Date.now() - lastChunk.timestamp >= INCOMPLETE_LINE_DELAY;
14
+ }
15
+ /**
16
+ * Split a line into chunks of maxWidth characters.
17
+ */
18
+ function splitIntoRows(line, maxWidth) {
19
+ if (line.length <= maxWidth)
20
+ return [line];
21
+ const rows = [];
22
+ for (let i = 0; i < line.length; i += maxWidth) {
23
+ rows.push(line.slice(i, i + maxWidth));
24
+ }
25
+ return rows;
26
+ }
27
+ /**
28
+ * Process text into terminal rows.
29
+ * Handles carriage returns and splits long lines into chunks.
30
+ */
31
+ function textToRows(text, maxWidth) {
32
+ return text
14
33
  .trim()
15
34
  .split(/\r?\n/)
16
- .filter((line) => line.trim().length > 0);
17
- return lines.length <= maxLines ? lines : lines.slice(-maxLines);
35
+ .map((line) => {
36
+ // Handle carriage returns: keep only content after the last \r
37
+ const lastCR = line.lastIndexOf('\r');
38
+ return lastCR >= 0 ? line.slice(lastCR + 1) : line;
39
+ })
40
+ .filter((line) => line.trim().length > 0)
41
+ .flatMap((line) => splitIntoRows(line, maxWidth));
42
+ }
43
+ /**
44
+ * Get the last N terminal rows from text.
45
+ * Splits long lines into chunks of maxWidth characters, then takes the last N.
46
+ * Handles carriage returns used in progress output by keeping only the
47
+ * content after the last \r in each line.
48
+ */
49
+ export function getLastLines(text, maxLines = MAX_LINES, maxWidth = MAX_WIDTH) {
50
+ const rows = textToRows(text, maxWidth);
51
+ return rows.length <= maxLines ? rows : rows.slice(-maxLines);
52
+ }
53
+ /**
54
+ * Convert output chunks to terminal rows.
55
+ * Sorts by timestamp, combines text, deduplicates adjacent lines.
56
+ * When not finished, only shows complete lines (ending with newline),
57
+ * unless the last chunk is older than INCOMPLETE_LINE_DELAY.
58
+ */
59
+ export function chunksToRows(chunks, maxLines = MAX_LINES, maxWidth = MAX_WIDTH, isFinished = true) {
60
+ if (chunks.length === 0)
61
+ return [];
62
+ // Sort by timestamp and combine text (chunks already contain line endings)
63
+ const sorted = [...chunks].sort((a, b) => a.timestamp - b.timestamp);
64
+ let combined = sorted.map((c) => c.text).join('');
65
+ // When not finished, only show complete lines (strip trailing incomplete line)
66
+ // unless the last chunk is old enough to be shown
67
+ if (!isFinished && !combined.endsWith('\n')) {
68
+ const lastChunk = sorted[sorted.length - 1];
69
+ if (!shouldShowIncompleteLine(lastChunk)) {
70
+ const lastNewline = combined.lastIndexOf('\n');
71
+ if (lastNewline >= 0) {
72
+ combined = combined.slice(0, lastNewline + 1);
73
+ }
74
+ else {
75
+ // No complete lines yet
76
+ return [];
77
+ }
78
+ }
79
+ }
80
+ // Convert to rows
81
+ const rows = textToRows(combined, maxWidth);
82
+ // Deduplicate adjacent identical lines
83
+ const deduplicated = rows.filter((row, index) => index === 0 || row !== rows[index - 1]);
84
+ return deduplicated.length <= maxLines
85
+ ? deduplicated
86
+ : deduplicated.slice(-maxLines);
18
87
  }
19
88
  /**
20
89
  * Compute display configuration for output rendering.
21
- * Encapsulates the logic for what to show and how to style it.
22
90
  */
23
- export function computeDisplayConfig(stdout, stderr, status, isFinished) {
24
- const hasStdout = stdout.trim().length > 0;
25
- const hasStderr = stderr.trim().length > 0;
26
- if (!hasStdout && !hasStderr)
91
+ export function computeDisplayConfig(chunks, status, isFinished) {
92
+ if (chunks.length === 0)
93
+ return null;
94
+ const rows = chunksToRows(chunks, MAX_LINES, MAX_WIDTH, isFinished);
95
+ if (rows.length === 0)
27
96
  return null;
28
- const stdoutLines = hasStdout ? getLastLines(stdout) : [];
29
- const stderrLines = hasStderr ? getLastLines(stderr) : [];
30
- // Show stdout if no stderr, or if stderr is minimal (provides context)
31
- const showStdout = hasStdout && (!hasStderr || stderrLines.length <= MINIMAL_INFO_THRESHOLD);
32
- // Use word wrapping for short outputs to show more detail
33
- const totalLines = stdoutLines.length + stderrLines.length;
34
- const wrapMode = totalLines <= SHORT_OUTPUT_THRESHOLD ? 'wrap' : 'truncate-end';
35
- // Darker colors for finished tasks
97
+ // Use yellow for failed status, otherwise gray (darker if finished)
36
98
  const baseColor = isFinished ? Palette.DarkGray : Palette.Gray;
37
- const stderrColor = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
38
- return {
39
- stdoutLines,
40
- stderrLines,
41
- showStdout,
42
- wrapMode,
43
- stdoutColor: baseColor,
44
- stderrColor,
45
- };
99
+ const color = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
100
+ return { rows, color };
46
101
  }
47
- export function Output({ stdout, stderr, status, isFinished }) {
48
- const config = computeDisplayConfig(stdout, stderr, status, isFinished ?? false);
102
+ export function Output({ chunks, status, isFinished }) {
103
+ const config = computeDisplayConfig(chunks, status, isFinished ?? false);
49
104
  if (!config)
50
105
  return null;
51
- const { stdoutLines, stderrLines, showStdout, wrapMode, stdoutColor, stderrColor, } = config;
52
- return (_jsxs(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: [showStdout &&
53
- stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap: wrapMode, children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: wrapMode, children: line }, `err-${index}`)))] }));
106
+ const { rows, color } = config;
107
+ return (_jsx(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: rows.map((row, index) => (_jsx(Text, { color: color, wrap: "truncate", children: row }, index))) }));
54
108
  }
@@ -1,25 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { loadDebugSetting } from '../../configuration/io.js';
4
+ import { DebugLevel } from '../../configuration/types.js';
3
5
  import { getStatusColors, Palette, STATUS_ICONS, } from '../../services/colors.js';
4
6
  import { ExecutionStatus } from '../../services/shell.js';
5
- import { formatDuration } from '../../services/utils.js';
7
+ import { formatDuration, formatMemory } from '../../services/utils.js';
6
8
  import { Spinner } from './Spinner.js';
7
9
  /**
8
10
  * Pure display component for a single subtask.
9
11
  * Shows label, command, status icon, and elapsed time.
10
12
  */
11
- export function SubtaskView({ label, command, status, elapsed, }) {
13
+ export function SubtaskView({ label, command, status, elapsed, currentMemory, }) {
12
14
  const colors = getStatusColors(status);
15
+ const debugLevel = loadDebugSetting();
16
+ const isVerbose = debugLevel === DebugLevel.Verbose;
13
17
  const isCancelled = status === ExecutionStatus.Cancelled;
14
18
  const isAborted = status === ExecutionStatus.Aborted;
19
+ const isRunning = status === ExecutionStatus.Running;
15
20
  const isFinished = status === ExecutionStatus.Success ||
16
21
  status === ExecutionStatus.Failed ||
17
22
  status === ExecutionStatus.Aborted;
18
23
  // Apply strikethrough for cancelled and aborted tasks
19
24
  const shouldStrikethrough = isCancelled || isAborted;
20
- const formatText = (text) => shouldStrikethrough ? text.split('').join('\u0336') + '\u0336' : text;
21
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[status] }), _jsx(Text, { color: colors.description, children: shouldStrikethrough
22
- ? formatText(label || command.description)
23
- : label || command.description }), (isFinished || status === ExecutionStatus.Running) &&
24
- elapsed !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsed), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), status === ExecutionStatus.Running && _jsx(Spinner, {})] })] })] }));
25
+ // Show memory in verbose mode while running
26
+ const showMemory = isVerbose && isRunning && currentMemory !== undefined;
27
+ // Build time/memory display
28
+ const showTimeInfo = (isFinished || isRunning) && elapsed !== undefined;
29
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[status] }), _jsx(Text, { color: colors.description, strikethrough: shouldStrikethrough, children: label || command.description }), showTimeInfo && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsed), ")"] })), showMemory && (_jsx(Text, { color: Palette.Yellow, children: formatMemory(currentMemory) }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), isRunning && _jsx(Spinner, {})] })] })] }));
25
30
  }
@@ -6,13 +6,12 @@ import { Output } from './Output.js';
6
6
  import { SubtaskView } from './Subtask.js';
7
7
  /**
8
8
  * Pure display component for a task.
9
- * Combines SubtaskView (label/command/status) with Output (stdout/stderr).
9
+ * Combines SubtaskView (label/command/status) with Output (chunks).
10
10
  * Output is shown during active execution, or in timeline only with debug mode.
11
11
  */
12
- export function TaskView({ label, command, status, elapsed, output, isFinished, isActive = false, }) {
13
- const stdout = output?.stdout ?? '';
14
- const stderr = output?.stderr ?? '';
12
+ export function TaskView({ label, command, status, elapsed, currentMemory, output, isFinished, isActive = false, }) {
13
+ const chunks = output?.chunks ?? [];
15
14
  // Show output during active execution, or in timeline only with debug enabled
16
15
  const showOutput = isActive || loadDebugSetting() !== DebugLevel.None;
17
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SubtaskView, { label: label, command: command, status: status, elapsed: elapsed }), showOutput && (_jsx(Output, { stdout: stdout, stderr: stderr, isFinished: isFinished, status: status }, `${stdout.length}-${stderr.length}`))] }));
16
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SubtaskView, { label: label, command: command, status: status, elapsed: elapsed, currentMemory: currentMemory }), showOutput && (_jsx(Output, { chunks: chunks, isFinished: isFinished, status: status }))] }));
18
17
  }
@@ -25,6 +25,6 @@ export const Upcoming = ({ items, status = ExecutionStatus.Pending, }) => {
25
25
  return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { color: Palette.Gray, children: STATUS_LABELS[status] }), items.map((name, index) => {
26
26
  const isLast = index === items.length - 1;
27
27
  const symbol = isLast ? BRANCH_LAST : BRANCH_MIDDLE;
28
- return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: Palette.DarkGray, strikethrough: strikethrough, children: [symbol, " ", name] }) }, index));
28
+ return (_jsxs(Box, { marginLeft: 1, gap: 1, children: [_jsx(Text, { color: Palette.DarkGray, children: symbol }), _jsx(Text, { color: Palette.DarkGray, strikethrough: strikethrough, children: name })] }, index));
29
29
  })] }));
30
30
  };
@@ -104,3 +104,13 @@ export function loadDebugSetting(fs = defaultFileSystem) {
104
104
  return DebugLevel.None;
105
105
  }
106
106
  }
107
+ const DEFAULT_MEMORY_LIMIT = 1024;
108
+ export function loadMemorySetting(fs = defaultFileSystem) {
109
+ try {
110
+ const config = loadConfig(fs);
111
+ return config.settings?.memory ?? DEFAULT_MEMORY_LIMIT;
112
+ }
113
+ catch {
114
+ return DEFAULT_MEMORY_LIMIT;
115
+ }
116
+ }
@@ -38,6 +38,12 @@ const coreConfigSchema = {
38
38
  default: DebugLevel.None,
39
39
  description: 'Debug mode',
40
40
  },
41
+ 'settings.memory': {
42
+ type: ConfigDefinitionType.Number,
43
+ required: false,
44
+ default: 1024,
45
+ description: 'Child process memory limit (MB)',
46
+ },
41
47
  };
42
48
  /**
43
49
  * Get complete configuration schema
@@ -37,6 +37,11 @@ export function validateConfig(parsed) {
37
37
  validatedConfig.settings.debug = settings.debug;
38
38
  }
39
39
  }
40
+ if ('memory' in settings) {
41
+ if (typeof settings.memory === 'number' && settings.memory > 0) {
42
+ validatedConfig.settings.memory = settings.memory;
43
+ }
44
+ }
40
45
  }
41
46
  return validatedConfig;
42
47
  }
@@ -1,3 +1,5 @@
1
+ import { stringify } from 'yaml';
2
+ import { loadMemorySetting } from '../configuration/io.js';
1
3
  import { loadUserConfig } from '../services/loader.js';
2
4
  import { replacePlaceholders } from '../services/resolver.js';
3
5
  import { validatePlaceholderResolution } from './validation.js';
@@ -10,6 +12,36 @@ export function fixEscapedQuotes(command) {
10
12
  // Replace ="value" with =\"value\"
11
13
  return command.replace(/="([^"]*)"/g, '=\\"$1\\"');
12
14
  }
15
+ /**
16
+ * Format a task as YAML with action line and metadata block
17
+ */
18
+ export function formatTaskAsYaml(action, metadata, indent = '') {
19
+ const normalizedAction = action.charAt(0).toLowerCase() + action.slice(1);
20
+ if (!metadata || Object.keys(metadata).length === 0) {
21
+ return normalizedAction;
22
+ }
23
+ const metadataYaml = stringify({ metadata })
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => `${indent}${line}`)
27
+ .join('\n');
28
+ return `${normalizedAction}\n\n${metadataYaml}`;
29
+ }
30
+ /**
31
+ * Build task descriptions for the LLM
32
+ * Single task: use as-is; multiple tasks: add header and bullet prefix
33
+ */
34
+ function buildTaskDescriptions(resolvedTasks) {
35
+ if (resolvedTasks.length === 1) {
36
+ const { action, params } = resolvedTasks[0];
37
+ return formatTaskAsYaml(action, params);
38
+ }
39
+ const header = `complete these ${resolvedTasks.length} tasks:`;
40
+ const bulletedTasks = resolvedTasks
41
+ .map(({ action, params }) => `- ${formatTaskAsYaml(action, params, ' ')}`)
42
+ .join('\n\n');
43
+ return `${header}\n\n${bulletedTasks}`;
44
+ }
13
45
  /**
14
46
  * Processes tasks through the AI service to generate executable commands.
15
47
  * Resolves placeholders in task descriptions and validates the results.
@@ -17,27 +49,26 @@ export function fixEscapedQuotes(command) {
17
49
  export async function processTasks(tasks, service) {
18
50
  // Load user config for placeholder resolution
19
51
  const userConfig = loadUserConfig();
20
- // Format tasks for the execute tool and resolve placeholders
21
- const taskList = tasks
22
- .map((task) => {
23
- const resolvedAction = replacePlaceholders(task.action, userConfig);
24
- const params = task.params
25
- ? ` (params: ${JSON.stringify(task.params)})`
26
- : '';
27
- return `- ${resolvedAction}${params}`;
28
- })
29
- .join('\n');
30
- // Build message with confirmed schedule header
31
- const taskDescriptions = `Confirmed schedule (${tasks.length} tasks):\n${taskList}`;
52
+ const memoryLimitMB = loadMemorySetting();
53
+ // Resolve placeholders in task actions
54
+ const resolvedTasks = tasks.map((task) => ({
55
+ action: replacePlaceholders(task.action, userConfig),
56
+ params: task.params,
57
+ }));
58
+ const taskDescriptions = buildTaskDescriptions(resolvedTasks);
32
59
  // Call execute tool to get commands
33
60
  const result = await service.processWithTool(taskDescriptions, 'execute');
34
- // Resolve placeholders in command strings
61
+ // Resolve placeholders in command strings and inject memory limit
35
62
  const resolvedCommands = (result.commands || []).map((cmd) => {
36
63
  // Fix escaped quotes lost in JSON parsing
37
64
  const fixed = fixEscapedQuotes(cmd.command);
38
65
  const resolved = replacePlaceholders(fixed, userConfig);
39
66
  validatePlaceholderResolution(resolved);
40
- return { ...cmd, command: resolved };
67
+ return {
68
+ ...cmd,
69
+ command: resolved,
70
+ memoryLimit: memoryLimitMB,
71
+ };
41
72
  });
42
73
  return {
43
74
  message: result.message,
@@ -1,37 +1,31 @@
1
- import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
1
+ import { ExecutionResult, ExecutionStatus, executeCommand, setMemoryCallback, setOutputCallback, } from '../services/shell.js';
2
2
  import { calculateElapsed } from '../services/utils.js';
3
- // Maximum number of output lines to keep in memory
4
- const MAX_OUTPUT_LINES = 128;
5
- /**
6
- * Limit output to last MAX_OUTPUT_LINES lines to prevent memory exhaustion
7
- */
8
- function limitLines(output) {
9
- const lines = output.split('\n');
10
- return lines.slice(-MAX_OUTPUT_LINES).join('\n');
11
- }
3
+ // Maximum number of output chunks to keep in memory
4
+ const MAX_OUTPUT_CHUNKS = 256;
12
5
  /**
13
6
  * Execute a single task and track its progress.
14
7
  * All execution logic is contained here, outside of React components.
15
8
  */
16
9
  export async function executeTask(command, index, callbacks) {
17
10
  const startTime = Date.now();
18
- let stdout = '';
19
- let stderr = '';
11
+ let chunks = [];
20
12
  let error = '';
21
13
  let workdir;
14
+ let currentMemory;
22
15
  // Helper to create current output snapshot
23
16
  const createOutput = () => ({
24
- stdout,
25
- stderr,
17
+ chunks,
26
18
  error,
27
19
  workdir,
20
+ currentMemory,
28
21
  });
29
- // Throttle updates to avoid excessive re-renders (100ms minimum interval)
22
+ // Throttle updates to avoid excessive re-renders (80ms minimum interval)
30
23
  let lastUpdateTime = 0;
31
24
  let pendingTimeout;
25
+ const THROTTLE_INTERVAL = 80;
32
26
  const throttledUpdate = () => {
33
27
  const now = Date.now();
34
- if (now - lastUpdateTime >= 100) {
28
+ if (now - lastUpdateTime >= THROTTLE_INTERVAL) {
35
29
  lastUpdateTime = now;
36
30
  callbacks.onUpdate(createOutput());
37
31
  }
@@ -40,29 +34,50 @@ export async function executeTask(command, index, callbacks) {
40
34
  pendingTimeout = undefined;
41
35
  lastUpdateTime = Date.now();
42
36
  callbacks.onUpdate(createOutput());
43
- }, 100 - (now - lastUpdateTime));
37
+ }, THROTTLE_INTERVAL - (now - lastUpdateTime));
44
38
  }
45
39
  };
46
- // Set up output streaming callback
40
+ // Set up output streaming callback - store chunks with timestamps
47
41
  setOutputCallback((data, stream) => {
48
- if (stream === 'stdout') {
49
- stdout = limitLines(stdout + data);
50
- }
51
- else {
52
- stderr = limitLines(stderr + data);
42
+ chunks.push({
43
+ text: data,
44
+ timestamp: Date.now(),
45
+ source: stream,
46
+ });
47
+ // Limit chunks to prevent memory exhaustion
48
+ if (chunks.length > MAX_OUTPUT_CHUNKS) {
49
+ chunks = chunks.slice(-MAX_OUTPUT_CHUNKS);
53
50
  }
54
51
  throttledUpdate();
55
52
  });
53
+ // Set up memory callback to track current memory
54
+ setMemoryCallback((memoryMB) => {
55
+ currentMemory = memoryMB;
56
+ throttledUpdate();
57
+ });
56
58
  try {
57
59
  const result = await executeCommand(command, undefined, index);
58
- // Clear callback and pending timeout
60
+ // Clear callbacks and pending timeout
59
61
  setOutputCallback(undefined);
62
+ setMemoryCallback(undefined);
60
63
  clearTimeout(pendingTimeout);
61
64
  const elapsed = calculateElapsed(startTime);
62
- // Update final output from result
63
- stdout = result.output;
64
- stderr = result.errors;
65
+ const now = Date.now();
66
+ // Update workdir from result
65
67
  workdir = result.workdir;
68
+ // Add final output/errors as chunks only if not already captured during streaming
69
+ const hasStreamedStdout = chunks.some((c) => c.source === 'stdout');
70
+ const hasStreamedStderr = chunks.some((c) => c.source === 'stderr');
71
+ if (result.output && result.output.trim() && !hasStreamedStdout) {
72
+ chunks.push({ text: result.output, timestamp: now, source: 'stdout' });
73
+ }
74
+ if (result.errors && result.errors.trim() && !hasStreamedStderr) {
75
+ chunks.push({
76
+ text: result.errors,
77
+ timestamp: now + 1,
78
+ source: 'stderr',
79
+ });
80
+ }
66
81
  if (result.result === ExecutionResult.Success) {
67
82
  const output = createOutput();
68
83
  callbacks.onUpdate(output);
@@ -70,7 +85,7 @@ export async function executeTask(command, index, callbacks) {
70
85
  return { status: ExecutionStatus.Success, elapsed, output };
71
86
  }
72
87
  else {
73
- const errorMsg = result.errors || result.error || 'Command failed';
88
+ const errorMsg = result.error || result.errors || 'Command failed';
74
89
  error = errorMsg;
75
90
  const output = createOutput();
76
91
  callbacks.onUpdate(output);
@@ -79,8 +94,9 @@ export async function executeTask(command, index, callbacks) {
79
94
  }
80
95
  }
81
96
  catch (err) {
82
- // Clear callback and pending timeout
97
+ // Clear callbacks and pending timeout
83
98
  setOutputCallback(undefined);
99
+ setMemoryCallback(undefined);
84
100
  clearTimeout(pendingTimeout);
85
101
  const elapsed = calculateElapsed(startTime);
86
102
  const errorMsg = err instanceof Error ? err.message : 'Unknown error';
@@ -95,5 +111,5 @@ export async function executeTask(command, index, callbacks) {
95
111
  * Create an empty execution output
96
112
  */
97
113
  export function createEmptyOutput() {
98
- return { stdout: '', stderr: '', error: '' };
114
+ return { chunks: [], error: '' };
99
115
  }
package/dist/index.js CHANGED
@@ -5,7 +5,9 @@ import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { render } from 'ink';
7
7
  import { DebugLevel } from './configuration/types.js';
8
+ import { preventPerformanceBufferOverflow } from './services/performance.js';
8
9
  import { Main } from './Main.js';
10
+ preventPerformanceBufferOverflow();
9
11
  const __filename = fileURLToPath(import.meta.url);
10
12
  const __dirname = dirname(__filename);
11
13
  // Get package info