This commit is contained in:
2026-04-25 06:45:36 +09:00
commit e77acee8ba
1903 changed files with 513282 additions and 0 deletions
@@ -0,0 +1,51 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text } from '../../ink.js';
import TextInput from '../TextInput.js';
type Props = {
value: string;
onChange: (value: string) => void;
historyFailedMatch: boolean;
};
function HistorySearchInput(t0) {
const $ = _c(9);
const {
value,
onChange,
historyFailedMatch
} = t0;
const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
let t2;
if ($[0] !== t1) {
t2 = <Text dimColor={true}>{t1}</Text>;
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
const t3 = stringWidth(value) + 1;
let t4;
if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />;
$[2] = onChange;
$[3] = t3;
$[4] = value;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== t2 || $[7] !== t4) {
t5 = <Box gap={1}>{t2}{t4}</Box>;
$[6] = t2;
$[7] = t4;
$[8] = t5;
} else {
t5 = $[8];
}
return t5;
}
function _temp() {}
export default HistorySearchInput;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInN0cmluZ1dpZHRoIiwiQm94IiwiVGV4dCIsIlRleHRJbnB1dCIsIlByb3BzIiwidmFsdWUiLCJvbkNoYW5nZSIsImhpc3RvcnlGYWlsZWRNYXRjaCIsIkhpc3RvcnlTZWFyY2hJbnB1dCIsInQwIiwiJCIsIl9jIiwidDEiLCJ0MiIsInQzIiwidDQiLCJsZW5ndGgiLCJfdGVtcCIsInQ1Il0sInNvdXJjZXMiOlsiSGlzdG9yeVNlYXJjaElucHV0LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHN0cmluZ1dpZHRoIH0gZnJvbSAnLi4vLi4vaW5rL3N0cmluZ1dpZHRoLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IFRleHRJbnB1dCBmcm9tICcuLi9UZXh0SW5wdXQuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIHZhbHVlOiBzdHJpbmdcbiAgb25DaGFuZ2U6ICh2YWx1ZTogc3RyaW5nKSA9PiB2b2lkXG4gIGhpc3RvcnlGYWlsZWRNYXRjaDogYm9vbGVhblxufVxuXG5mdW5jdGlvbiBIaXN0b3J5U2VhcmNoSW5wdXQoe1xuICB2YWx1ZSxcbiAgb25DaGFuZ2UsXG4gIGhpc3RvcnlGYWlsZWRNYXRjaCxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8Qm94IGdhcD17MX0+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAge2hpc3RvcnlGYWlsZWRNYXRjaCA/ICdubyBtYXRjaGluZyBwcm9tcHQ6JyA6ICdzZWFyY2ggcHJvbXB0czonfVxuICAgICAgPC9UZXh0PlxuICAgICAgPFRleHRJbnB1dFxuICAgICAgICB2YWx1ZT17dmFsdWV9XG4gICAgICAgIG9uQ2hhbmdlPXtvbkNoYW5nZX1cbiAgICAgICAgLy8gRm9yY2UgY3Vyc29yIHRvIGVuZCBvZiBzZWFyY2ggaW5wdXQgc2luY2UgbmF2aWdhdGlvbiBzaG91bGQgY2FuY2VsIHNlYXJjaFxuICAgICAgICBjdXJzb3JPZmZzZXQ9e3ZhbHVlLmxlbmd0aH1cbiAgICAgICAgb25DaGFuZ2VDdXJzb3JPZmZzZXQ9eygpID0+IHt9fVxuICAgICAgICBjb2x1bW5zPXtzdHJpbmdXaWR0aCh2YWx1ZSkgKyAxfVxuICAgICAgICBmb2N1cz17dHJ1ZX1cbiAgICAgICAgc2hvd0N1cnNvcj17dHJ1ZX1cbiAgICAgICAgbXVsdGlsaW5lPXtmYWxzZX1cbiAgICAgICAgZGltQ29sb3I9e3RydWV9XG4gICAgICAvPlxuICAgIDwvQm94PlxuICApXG59XG5cbmV4cG9ydCBkZWZhdWx0IEhpc3RvcnlTZWFyY2hJbnB1dFxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxXQUFXLFFBQVEsMEJBQTBCO0FBQ3RELFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsT0FBT0MsU0FBUyxNQUFNLGlCQUFpQjtBQUV2QyxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsS0FBSyxFQUFFLE1BQU07RUFDYkMsUUFBUSxFQUFFLENBQUNELEtBQUssRUFBRSxNQUFNLEVBQUUsR0FBRyxJQUFJO0VBQ2pDRSxrQkFBa0IsRUFBRSxPQUFPO0FBQzdCLENBQUM7QUFFRCxTQUFBQyxtQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUE0QjtJQUFBTixLQUFBO0lBQUFDLFFBQUE7SUFBQUM7RUFBQSxJQUFBRSxFQUlwQjtFQUlDLE1BQUFHLEVBQUEsR0FBQUwsa0JBQWtCLEdBQWxCLHFCQUE4RCxHQUE5RCxpQkFBOEQ7RUFBQSxJQUFBTSxFQUFBO0VBQUEsSUFBQUgsQ0FBQSxRQUFBRSxFQUFBO0lBRGpFQyxFQUFBLElBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FDWCxDQUFBRCxFQUE2RCxDQUNoRSxFQUZDLElBQUksQ0FFRTtJQUFBRixDQUFBLE1BQUFFLEVBQUE7SUFBQUYsQ0FBQSxNQUFBRyxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSCxDQUFBO0VBQUE7RUFPSSxNQUFBSSxFQUFBLEdBQUFkLFdBQVcsQ0FBQ0ssS0FBSyxDQUFDLEdBQUcsQ0FBQztFQUFBLElBQUFVLEVBQUE7RUFBQSxJQUFBTCxDQUFBLFFBQUFKLFFBQUEsSUFBQUksQ0FBQSxRQUFBSSxFQUFBLElBQUFKLENBQUEsUUFBQUwsS0FBQTtJQU5qQ1UsRUFBQSxJQUFDLFNBQVMsQ0FDRFYsS0FBSyxDQUFMQSxNQUFJLENBQUMsQ0FDRkMsUUFBUSxDQUFSQSxTQUFPLENBQUMsQ0FFSixZQUFZLENBQVosQ0FBQUQsS0FBSyxDQUFBVyxNQUFNLENBQUMsQ0FDSixvQkFBUSxDQUFSLENBQUFDLEtBQU8sQ0FBQyxDQUNyQixPQUFzQixDQUF0QixDQUFBSCxFQUFxQixDQUFDLENBQ3hCLEtBQUksQ0FBSixLQUFHLENBQUMsQ0FDQyxVQUFJLENBQUosS0FBRyxDQUFDLENBQ0wsU0FBSyxDQUFMLE1BQUksQ0FBQyxDQUNOLFFBQUksQ0FBSixLQUFHLENBQUMsR0FDZDtJQUFBSixDQUFBLE1BQUFKLFFBQUE7SUFBQUksQ0FBQSxNQUFBSSxFQUFBO0lBQUFKLENBQUEsTUFBQUwsS0FBQTtJQUFBSyxDQUFBLE1BQUFLLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFMLENBQUE7RUFBQTtFQUFBLElBQUFRLEVBQUE7RUFBQSxJQUFBUixDQUFBLFFBQUFHLEVBQUEsSUFBQUgsQ0FBQSxRQUFBSyxFQUFBO0lBZkpHLEVBQUEsSUFBQyxHQUFHLENBQU0sR0FBQyxDQUFELEdBQUMsQ0FDVCxDQUFBTCxFQUVNLENBQ04sQ0FBQUUsRUFXQyxDQUNILEVBaEJDLEdBQUcsQ0FnQkU7SUFBQUwsQ0FBQSxNQUFBRyxFQUFBO0lBQUFILENBQUEsTUFBQUssRUFBQTtJQUFBTCxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLE9BaEJOUSxFQWdCTTtBQUFBO0FBdEJWLFNBQUFELE1BQUE7QUEwQkEsZUFBZVQsa0JBQWtCIiwiaWdub3JlTGlzdCI6W119
@@ -0,0 +1,12 @@
import * as React from 'react';
import { FLAG_ICON } from '../../constants/figures.js';
import { Box, Text } from '../../ink.js';
/**
* ANT-ONLY: Banner shown in the transcript that prompts users to report
* issues via /issue. Appears when friction is detected in the conversation.
*/
export function IssueFlagBanner() {
return null;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkZMQUdfSUNPTiIsIkJveCIsIlRleHQiLCJJc3N1ZUZsYWdCYW5uZXIiXSwic291cmNlcyI6WyJJc3N1ZUZsYWdCYW5uZXIudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgRkxBR19JQ09OIH0gZnJvbSAnLi4vLi4vY29uc3RhbnRzL2ZpZ3VyZXMuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5cbi8qKlxuICogQU5ULU9OTFk6IEJhbm5lciBzaG93biBpbiB0aGUgdHJhbnNjcmlwdCB0aGF0IHByb21wdHMgdXNlcnMgdG8gcmVwb3J0XG4gKiBpc3N1ZXMgdmlhIC9pc3N1ZS4gQXBwZWFycyB3aGVuIGZyaWN0aW9uIGlzIGRldGVjdGVkIGluIHRoZSBjb252ZXJzYXRpb24uXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBJc3N1ZUZsYWdCYW5uZXIoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgaWYgKFwiZXh0ZXJuYWxcIiAhPT0gJ2FudCcpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIiBtYXJnaW5Ub3A9ezF9IHdpZHRoPVwiMTAwJVwiPlxuICAgICAgPEJveCBtaW5XaWR0aD17Mn0+XG4gICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiPntGTEFHX0lDT059PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgICA8VGV4dD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+W0FOVC1PTkxZXSA8L1RleHQ+XG4gICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiIGJvbGQ+XG4gICAgICAgICAgU29tZXRoaW5nIG9mZiB3aXRoIENsYXVkZT9cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj4gL2lzc3VlIHRvIHJlcG9ydCBpdDwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLFNBQVMsUUFBUSw0QkFBNEI7QUFDdEQsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYzs7QUFFeEM7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLFNBQUFDLGdCQUFBO0VBQUEsT0FFSSxJQUFJO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { Box, Text } from 'src/ink.js';
type Props = {
hasStash: boolean;
};
export function PromptInputStashNotice(t0) {
const $ = _c(1);
const {
hasStash
} = t0;
if (!hasStash) {
return null;
}
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJCb3giLCJUZXh0IiwiUHJvcHMiLCJoYXNTdGFzaCIsIlByb21wdElucHV0U3Rhc2hOb3RpY2UiLCJ0MCIsIiQiLCJfYyIsInQxIiwiU3ltYm9sIiwiZm9yIiwicG9pbnRlclNtYWxsIl0sInNvdXJjZXMiOlsiUHJvbXB0SW5wdXRTdGFzaE5vdGljZS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnc3JjL2luay5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgaGFzU3Rhc2g6IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFByb21wdElucHV0U3Rhc2hOb3RpY2UoeyBoYXNTdGFzaCB9OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGlmICghaGFzU3Rhc2gpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IHBhZGRpbmdMZWZ0PXsyfT5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICB7ZmlndXJlcy5wb2ludGVyU21hbGx9IFN0YXNoZWQgKGF1dG8tcmVzdG9yZXMgYWZ0ZXIgc3VibWl0KVxuICAgICAgPC9UZXh0PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPLEtBQUtDLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLFlBQVk7QUFFdEMsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLFFBQVEsRUFBRSxPQUFPO0FBQ25CLENBQUM7QUFFRCxPQUFPLFNBQUFDLHVCQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQWdDO0lBQUFKO0VBQUEsSUFBQUUsRUFBbUI7RUFDeEQsSUFBSSxDQUFDRixRQUFRO0lBQUEsT0FDSixJQUFJO0VBQUE7RUFDWixJQUFBSyxFQUFBO0VBQUEsSUFBQUYsQ0FBQSxRQUFBRyxNQUFBLENBQUFDLEdBQUE7SUFHQ0YsRUFBQSxJQUFDLEdBQUcsQ0FBYyxXQUFDLENBQUQsR0FBQyxDQUNqQixDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQ1gsQ0FBQVYsT0FBTyxDQUFBYSxZQUFZLENBQUUscUNBQ3hCLEVBRkMsSUFBSSxDQUdQLEVBSkMsR0FBRyxDQUlFO0lBQUFMLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQUEsT0FKTkUsRUFJTTtBQUFBIiwiaWdub3JlTGlzdCI6W119
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
import type { HistoryMode } from 'src/hooks/useArrowKeyHistory.js'
import type { PromptInputMode } from 'src/types/textInputTypes.js'
export function prependModeCharacterToInput(
input: string,
mode: PromptInputMode,
): string {
switch (mode) {
case 'bash':
return `!${input}`
default:
return input
}
}
export function getModeFromInput(input: string): HistoryMode {
if (input.startsWith('!')) {
return 'bash'
}
return 'prompt'
}
export function getValueFromInput(input: string): string {
const mode = getModeFromInput(input)
if (mode === 'prompt') {
return input
}
return input.slice(1)
}
export function isInputModeCharacter(input: string): boolean {
return input === '!'
}
+90
View File
@@ -0,0 +1,90 @@
import { getPastedTextRefNumLines } from 'src/history.js'
import type { PastedContent } from 'src/utils/config.js'
const TRUNCATION_THRESHOLD = 10000 // Characters before we truncate
const PREVIEW_LENGTH = 1000 // Characters to show at start and end
type TruncatedMessage = {
truncatedText: string
placeholderContent: string
}
/**
* Determines whether the input text should be truncated. If so, it adds a
* truncated text placeholder and neturns
*
* @param text The input text
* @param nextPasteId The reference id to use
* @returns The new text to display and separate placeholder content if applicable.
*/
export function maybeTruncateMessageForInput(
text: string,
nextPasteId: number,
): TruncatedMessage {
// If the text is short enough, return it as-is
if (text.length <= TRUNCATION_THRESHOLD) {
return {
truncatedText: text,
placeholderContent: '',
}
}
// Calculate how much text to keep from start and end
const startLength = Math.floor(PREVIEW_LENGTH / 2)
const endLength = Math.floor(PREVIEW_LENGTH / 2)
// Extract the portions we'll keep
const startText = text.slice(0, startLength)
const endText = text.slice(-endLength)
// Calculate the number of lines that will be truncated
const placeholderContent = text.slice(startLength, -endLength)
const truncatedLines = getPastedTextRefNumLines(placeholderContent)
// Create a placeholder reference similar to pasted text
const placeholderId = nextPasteId
const placeholderRef = formatTruncatedTextRef(placeholderId, truncatedLines)
// Combine the parts with the placeholder
const truncatedText = startText + placeholderRef + endText
return {
truncatedText,
placeholderContent,
}
}
function formatTruncatedTextRef(id: number, numLines: number): string {
return `[...Truncated text #${id} +${numLines} lines...]`
}
export function maybeTruncateInput(
input: string,
pastedContents: Record<number, PastedContent>,
): { newInput: string; newPastedContents: Record<number, PastedContent> } {
// Get the next available ID for the truncated content
const existingIds = Object.keys(pastedContents).map(Number)
const nextPasteId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1
// Apply truncation
const { truncatedText, placeholderContent } = maybeTruncateMessageForInput(
input,
nextPasteId,
)
if (!placeholderContent) {
return { newInput: input, newPastedContents: pastedContents }
}
return {
newInput: truncatedText,
newPastedContents: {
...pastedContents,
[nextPasteId]: {
id: nextPasteId,
type: 'text',
content: placeholderContent,
},
},
}
}
@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react'
import type { PastedContent } from 'src/utils/config.js'
import { maybeTruncateInput } from './inputPaste.js'
type Props = {
input: string
pastedContents: Record<number, PastedContent>
onInputChange: (input: string) => void
setCursorOffset: (offset: number) => void
setPastedContents: (contents: Record<number, PastedContent>) => void
}
export function useMaybeTruncateInput({
input,
pastedContents,
onInputChange,
setCursorOffset,
setPastedContents,
}: Props) {
// Track if we've initialized this specific input value
const [hasAppliedTruncationToInput, setHasAppliedTruncationToInput] =
useState(false)
// Process input for truncation and pasted images from MessageSelector.
useEffect(() => {
if (hasAppliedTruncationToInput) {
return
}
if (input.length <= 10_000) {
return
}
const { newInput, newPastedContents } = maybeTruncateInput(
input,
pastedContents,
)
onInputChange(newInput)
setCursorOffset(newInput.length)
setPastedContents(newPastedContents)
setHasAppliedTruncationToInput(true)
}, [
input,
hasAppliedTruncationToInput,
pastedContents,
onInputChange,
setPastedContents,
setCursorOffset,
])
// Reset hasInitializedInput when input is cleared (e.g., after submission)
useEffect(() => {
if (input === '') {
setHasAppliedTruncationToInput(false)
}
}, [input])
}
@@ -0,0 +1,76 @@
import { feature } from 'bun:bundle'
import { useMemo } from 'react'
import { useCommandQueue } from 'src/hooks/useCommandQueue.js'
import { useAppState } from 'src/state/AppState.js'
import { getGlobalConfig } from 'src/utils/config.js'
import { getExampleCommandFromCache } from 'src/utils/exampleCommands.js'
import { isQueuedCommandEditable } from 'src/utils/messageQueueManager.js'
// Dead code elimination: conditional import for proactive mode
/* eslint-disable @typescript-eslint/no-require-imports */
const proactiveModule =
feature('PROACTIVE') || feature('KAIROS')
? require('../../proactive/index.js')
: null
type Props = {
input: string
submitCount: number
viewingAgentName?: string
}
const NUM_TIMES_QUEUE_HINT_SHOWN = 3
const MAX_TEAMMATE_NAME_LENGTH = 20
export function usePromptInputPlaceholder({
input,
submitCount,
viewingAgentName,
}: Props): string | undefined {
const queuedCommands = useCommandQueue()
const promptSuggestionEnabled = useAppState(s => s.promptSuggestionEnabled)
const placeholder = useMemo(() => {
if (input !== '') {
return
}
// Show teammate hint when viewing teammate
if (viewingAgentName) {
const displayName =
viewingAgentName.length > MAX_TEAMMATE_NAME_LENGTH
? viewingAgentName.slice(0, MAX_TEAMMATE_NAME_LENGTH - 3) + '...'
: viewingAgentName
return `Message @${displayName}`
}
// Show queue hint if user has not seen it yet.
// Only count user-editable commands — task-notification and isMeta
// are hidden from the prompt area (see PromptInputQueuedCommands).
if (
queuedCommands.some(isQueuedCommandEditable) &&
(getGlobalConfig().queuedCommandUpHintCount || 0) <
NUM_TIMES_QUEUE_HINT_SHOWN
) {
return 'Press up to edit queued messages'
}
// Show example command if user has not submitted yet and suggestions are enabled.
// Skip in proactive mode — the model drives the conversation so onboarding
// examples are irrelevant and block prompt suggestions from showing.
if (
submitCount < 1 &&
promptSuggestionEnabled &&
!proactiveModule?.isProactiveActive()
) {
return getExampleCommandFromCache()
}
}, [
input,
queuedCommands,
submitCount,
promptSuggestionEnabled,
viewingAgentName,
])
return placeholder
}
@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react'
const HINT_DISPLAY_DURATION_MS = 5000
let hasShownThisSession = false
/**
* Hook to manage the /fast hint display next to the fast icon.
* Shows the hint for 5 seconds once per session.
*/
export function useShowFastIconHint(showFastIcon: boolean): boolean {
const [showHint, setShowHint] = useState(false)
useEffect(() => {
if (hasShownThisSession || !showFastIcon) {
return
}
hasShownThisSession = true
setShowHint(true)
const timer = setTimeout(setShowHint, HINT_DISPLAY_DURATION_MS, false)
return () => {
clearTimeout(timer)
setShowHint(false)
}
}, [showFastIcon])
return showHint
}
+155
View File
@@ -0,0 +1,155 @@
import * as React from 'react'
import { useAppState, useAppStateStore } from '../../state/AppState.js'
import {
getActiveAgentForInput,
getViewedTeammateTask,
} from '../../state/selectors.js'
import {
AGENT_COLOR_TO_THEME_COLOR,
AGENT_COLORS,
type AgentColorName,
getAgentColor,
} from '../../tools/AgentTool/agentColorManager.js'
import { getStandaloneAgentName } from '../../utils/standaloneAgent.js'
import { isInsideTmux } from '../../utils/swarm/backends/detection.js'
import {
getCachedDetectionResult,
isInProcessEnabled,
} from '../../utils/swarm/backends/registry.js'
import { getSwarmSocketName } from '../../utils/swarm/constants.js'
import {
getAgentName,
getTeammateColor,
getTeamName,
isTeammate,
} from '../../utils/teammate.js'
import { isInProcessTeammate } from '../../utils/teammateContext.js'
import type { Theme } from '../../utils/theme.js'
type SwarmBannerInfo = {
text: string
bgColor: keyof Theme
} | null
/**
* Hook that returns banner information for swarm, standalone agent, or --agent CLI context.
* - Leader (not in tmux): Returns "tmux -L ... attach" command with cyan background
* - Leader (in tmux / in-process): Falls through to standalone-agent check shows
* /rename name + /color background if set, else null
* - Teammate: Returns "teammate@team" format with their assigned color background
* - Viewing a background agent (CoordinatorTaskPanel): Returns agent name with its color
* - Standalone agent: Returns agent name with their color background (no @team)
* - --agent CLI flag: Returns "@agentName" with cyan background
*/
export function useSwarmBanner(): SwarmBannerInfo {
const teamContext = useAppState(s => s.teamContext)
const standaloneAgentContext = useAppState(s => s.standaloneAgentContext)
const agent = useAppState(s => s.agent)
// Subscribe so the banner updates on enter/exit teammate view even though
// getActiveAgentForInput reads it from store.getState().
useAppState(s => s.viewingAgentTaskId)
const store = useAppStateStore()
const [insideTmux, setInsideTmux] = React.useState<boolean | null>(null)
React.useEffect(() => {
void isInsideTmux().then(setInsideTmux)
}, [])
const state = store.getState()
// Teammate process: show @agentName with assigned color.
// In-process teammates run headless — their banner shows in the leader UI instead.
if (isTeammate() && !isInProcessTeammate()) {
const agentName = getAgentName()
if (agentName && getTeamName()) {
return {
text: `@${agentName}`,
bgColor: toThemeColor(
teamContext?.selfAgentColor ?? getTeammateColor(),
),
}
}
}
// Leader with spawned teammates: tmux-attach hint when external, else show
// the viewed teammate's name when inside tmux / native panes / in-process.
const hasTeammates =
teamContext?.teamName &&
teamContext.teammates &&
Object.keys(teamContext.teammates).length > 0
if (hasTeammates) {
const viewedTeammate = getViewedTeammateTask(state)
const viewedColor = toThemeColor(viewedTeammate?.identity.color)
const inProcessMode = isInProcessEnabled()
const nativePanes = getCachedDetectionResult()?.isNative ?? false
if (insideTmux === false && !inProcessMode && !nativePanes) {
return {
text: `View teammates: \`tmux -L ${getSwarmSocketName()} a\``,
bgColor: viewedColor,
}
}
if (
(insideTmux === true || inProcessMode || nativePanes) &&
viewedTeammate
) {
return {
text: `@${viewedTeammate.identity.agentName}`,
bgColor: viewedColor,
}
}
// insideTmux === null: still loading — fall through.
// Not viewing a teammate: fall through so /rename and /color are honored.
}
// Viewing a background agent (CoordinatorTaskPanel): local_agent tasks aren't
// InProcessTeammates, so getViewedTeammateTask misses them. Reverse-lookup the
// name from agentNameRegistry the same way CoordinatorAgentStatus does.
const active = getActiveAgentForInput(state)
if (active.type === 'named_agent') {
const task = active.task
let name: string | undefined
for (const [n, id] of state.agentNameRegistry) {
if (id === task.id) {
name = n
break
}
}
return {
text: name ? `@${name}` : task.description,
bgColor: getAgentColor(task.agentType) ?? 'cyan_FOR_SUBAGENTS_ONLY',
}
}
// Standalone agent (/rename, /color): name and/or custom color, no @team.
const standaloneName = getStandaloneAgentName(state)
const standaloneColor = standaloneAgentContext?.color
if (standaloneName || standaloneColor) {
return {
text: standaloneName ?? '',
bgColor: toThemeColor(standaloneColor),
}
}
// --agent CLI flag (when not handled above).
if (agent) {
const agentDef = state.agentDefinitions.activeAgents.find(
a => a.agentType === agent,
)
return {
text: agent,
bgColor: toThemeColor(agentDef?.color, 'promptBorder'),
}
}
return null
}
function toThemeColor(
colorName: string | undefined,
fallback: keyof Theme = 'cyan_FOR_SUBAGENTS_ONLY',
): keyof Theme {
return colorName && AGENT_COLORS.includes(colorName as AgentColorName)
? AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
: fallback
}
+60
View File
@@ -0,0 +1,60 @@
import {
hasUsedBackslashReturn,
isShiftEnterKeyBindingInstalled,
} from '../../commands/terminalSetup/terminalSetup.js'
import type { Key } from '../../ink.js'
import { getGlobalConfig } from '../../utils/config.js'
import { env } from '../../utils/env.js'
/**
* Helper function to check if vim mode is currently enabled
* @returns boolean indicating if vim mode is active
*/
export function isVimModeEnabled(): boolean {
const config = getGlobalConfig()
return config.editorMode === 'vim'
}
export function getNewlineInstructions(): string {
// Apple Terminal on macOS uses native modifier key detection for Shift+Enter
if (env.terminal === 'Apple_Terminal' && process.platform === 'darwin') {
return 'shift + ⏎ for newline'
}
// For iTerm2 and VSCode, show Shift+Enter instructions if installed
if (isShiftEnterKeyBindingInstalled()) {
return 'shift + ⏎ for newline'
}
// Otherwise show backslash+return instructions
return hasUsedBackslashReturn()
? '\\⏎ for newline'
: 'backslash (\\) + return (⏎) for newline'
}
/**
* True when the keystroke is a printable character that does not begin
* with whitespace i.e., a normal letter/digit/symbol the user typed.
* Used to gate the lazy space inserted after an image pill.
*/
export function isNonSpacePrintable(input: string, key: Key): boolean {
if (
key.ctrl ||
key.meta ||
key.escape ||
key.return ||
key.tab ||
key.backspace ||
key.delete ||
key.upArrow ||
key.downArrow ||
key.leftArrow ||
key.rightArrow ||
key.pageUp ||
key.pageDown ||
key.home ||
key.end
) {
return false
}
return input.length > 0 && !/^\s/.test(input) && !input.startsWith('\x1b')
}