|
|
@@ -814,208 +814,198 @@ export function Prompt(props: PromptProps) {
|
|
|
agentStyleId={agentStyleId}
|
|
|
promptPartTypeId={() => promptPartTypeId}
|
|
|
/>
|
|
|
- <box ref={(r) => (anchor = r)} visible={props.visible !== false}>
|
|
|
- <box
|
|
|
- border={["left"]}
|
|
|
- borderColor={highlight()}
|
|
|
- customBorderChars={{
|
|
|
- ...EmptyBorder,
|
|
|
- vertical: "┃",
|
|
|
- bottomLeft: "╹",
|
|
|
- }}
|
|
|
- >
|
|
|
- <box
|
|
|
- paddingLeft={2}
|
|
|
- paddingRight={2}
|
|
|
- paddingTop={1}
|
|
|
- flexShrink={0}
|
|
|
- backgroundColor={theme.backgroundElement}
|
|
|
- flexGrow={1}
|
|
|
- >
|
|
|
- <textarea
|
|
|
- placeholder={placeholderText()}
|
|
|
- textColor={keybind.leader ? theme.textMuted : theme.text}
|
|
|
- focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
|
|
- minHeight={1}
|
|
|
- maxHeight={6}
|
|
|
- onContentChange={() => {
|
|
|
- const value = input.plainText
|
|
|
- setStore("prompt", "input", value)
|
|
|
- autocomplete.onInput(value)
|
|
|
- syncExtmarksWithPromptParts()
|
|
|
- }}
|
|
|
- keyBindings={textareaKeybindings()}
|
|
|
- onKeyDown={async (e) => {
|
|
|
- if (props.disabled) {
|
|
|
+ <box
|
|
|
+ ref={(r) => (anchor = r)}
|
|
|
+ visible={props.visible !== false}
|
|
|
+ backgroundColor={theme.backgroundElement}
|
|
|
+ paddingBottom={1}
|
|
|
+ paddingLeft={2}
|
|
|
+ paddingRight={2}
|
|
|
+ >
|
|
|
+ <box paddingTop={1} flexShrink={0} flexGrow={1}>
|
|
|
+ <textarea
|
|
|
+ placeholder={placeholderText()}
|
|
|
+ textColor={keybind.leader ? theme.textMuted : theme.text}
|
|
|
+ focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
|
|
+ minHeight={1}
|
|
|
+ maxHeight={6}
|
|
|
+ onContentChange={() => {
|
|
|
+ const value = input.plainText
|
|
|
+ setStore("prompt", "input", value)
|
|
|
+ autocomplete.onInput(value)
|
|
|
+ syncExtmarksWithPromptParts()
|
|
|
+ }}
|
|
|
+ keyBindings={textareaKeybindings()}
|
|
|
+ onKeyDown={async (e) => {
|
|
|
+ if (props.disabled) {
|
|
|
+ e.preventDefault()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Handle clipboard paste (Ctrl+V) - check for images first on Windows
|
|
|
+ // This is needed because Windows terminal doesn't properly send image data
|
|
|
+ // through bracketed paste, so we need to intercept the keypress and
|
|
|
+ // directly read from clipboard before the terminal handles it
|
|
|
+ if (keybind.match("input_paste", e)) {
|
|
|
+ const content = await Clipboard.read()
|
|
|
+ if (content?.mime.startsWith("image/")) {
|
|
|
e.preventDefault()
|
|
|
- return
|
|
|
- }
|
|
|
- // Handle clipboard paste (Ctrl+V) - check for images first on Windows
|
|
|
- // This is needed because Windows terminal doesn't properly send image data
|
|
|
- // through bracketed paste, so we need to intercept the keypress and
|
|
|
- // directly read from clipboard before the terminal handles it
|
|
|
- if (keybind.match("input_paste", e)) {
|
|
|
- const content = await Clipboard.read()
|
|
|
- if (content?.mime.startsWith("image/")) {
|
|
|
- e.preventDefault()
|
|
|
- await pasteImage({
|
|
|
- filename: "clipboard",
|
|
|
- mime: content.mime,
|
|
|
- content: content.data,
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
- // If no image, let the default paste behavior continue
|
|
|
- }
|
|
|
- if (keybind.match("input_clear", e) && store.prompt.input !== "") {
|
|
|
- input.clear()
|
|
|
- input.extmarks.clear()
|
|
|
- setStore("prompt", {
|
|
|
- input: "",
|
|
|
- parts: [],
|
|
|
+ await pasteImage({
|
|
|
+ filename: "clipboard",
|
|
|
+ mime: content.mime,
|
|
|
+ content: content.data,
|
|
|
})
|
|
|
- setStore("extmarkToPartIndex", new Map())
|
|
|
return
|
|
|
}
|
|
|
- if (keybind.match("app_exit", e)) {
|
|
|
- if (store.prompt.input === "") {
|
|
|
- await exit()
|
|
|
- // Don't preventDefault - let textarea potentially handle the event
|
|
|
- e.preventDefault()
|
|
|
- return
|
|
|
- }
|
|
|
+ // If no image, let the default paste behavior continue
|
|
|
+ }
|
|
|
+ if (keybind.match("input_clear", e) && store.prompt.input !== "") {
|
|
|
+ input.clear()
|
|
|
+ input.extmarks.clear()
|
|
|
+ setStore("prompt", {
|
|
|
+ input: "",
|
|
|
+ parts: [],
|
|
|
+ })
|
|
|
+ setStore("extmarkToPartIndex", new Map())
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (keybind.match("app_exit", e)) {
|
|
|
+ if (store.prompt.input === "") {
|
|
|
+ await exit()
|
|
|
+ // Don't preventDefault - let textarea potentially handle the event
|
|
|
+ e.preventDefault()
|
|
|
+ return
|
|
|
}
|
|
|
- if (e.name === "!" && input.visualCursor.offset === 0) {
|
|
|
- setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length))
|
|
|
- setStore("mode", "shell")
|
|
|
+ }
|
|
|
+ if (e.name === "!" && input.visualCursor.offset === 0) {
|
|
|
+ setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length))
|
|
|
+ setStore("mode", "shell")
|
|
|
+ e.preventDefault()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (store.mode === "shell") {
|
|
|
+ if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
|
|
+ setStore("mode", "normal")
|
|
|
e.preventDefault()
|
|
|
return
|
|
|
}
|
|
|
- if (store.mode === "shell") {
|
|
|
- if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
|
|
- setStore("mode", "normal")
|
|
|
+ }
|
|
|
+ if (store.mode === "normal") autocomplete.onKeyDown(e)
|
|
|
+ if (!autocomplete.visible) {
|
|
|
+ if (
|
|
|
+ (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
|
|
|
+ (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
|
|
|
+ ) {
|
|
|
+ const direction = keybind.match("history_previous", e) ? -1 : 1
|
|
|
+ const item = history.move(direction, input.plainText)
|
|
|
+
|
|
|
+ if (item) {
|
|
|
+ input.setText(item.input)
|
|
|
+ setStore("prompt", item)
|
|
|
+ setStore("mode", item.mode ?? "normal")
|
|
|
+ restoreExtmarksFromParts(item.parts)
|
|
|
e.preventDefault()
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- if (store.mode === "normal") autocomplete.onKeyDown(e)
|
|
|
- if (!autocomplete.visible) {
|
|
|
- if (
|
|
|
- (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
|
|
|
- (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
|
|
|
- ) {
|
|
|
- const direction = keybind.match("history_previous", e) ? -1 : 1
|
|
|
- const item = history.move(direction, input.plainText)
|
|
|
-
|
|
|
- if (item) {
|
|
|
- input.setText(item.input)
|
|
|
- setStore("prompt", item)
|
|
|
- setStore("mode", item.mode ?? "normal")
|
|
|
- restoreExtmarksFromParts(item.parts)
|
|
|
- e.preventDefault()
|
|
|
- if (direction === -1) input.cursorOffset = 0
|
|
|
- if (direction === 1) input.cursorOffset = input.plainText.length
|
|
|
- }
|
|
|
- return
|
|
|
+ if (direction === -1) input.cursorOffset = 0
|
|
|
+ if (direction === 1) input.cursorOffset = input.plainText.length
|
|
|
}
|
|
|
-
|
|
|
- if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
|
|
|
- if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
|
|
|
- input.cursorOffset = input.plainText.length
|
|
|
- }
|
|
|
- }}
|
|
|
- onSubmit={submit}
|
|
|
- onPaste={async (event: PasteEvent) => {
|
|
|
- if (props.disabled) {
|
|
|
- event.preventDefault()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Normalize line endings at the boundary
|
|
|
- // Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
|
|
|
- // Replace CRLF first, then any remaining CR
|
|
|
- const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
|
|
- const pastedContent = normalizedText.trim()
|
|
|
- if (!pastedContent) {
|
|
|
- command.trigger("prompt.paste")
|
|
|
- return
|
|
|
- }
|
|
|
+ if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
|
|
|
+ if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
|
|
|
+ input.cursorOffset = input.plainText.length
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onSubmit={submit}
|
|
|
+ onPaste={async (event: PasteEvent) => {
|
|
|
+ if (props.disabled) {
|
|
|
+ event.preventDefault()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // trim ' from the beginning and end of the pasted content. just
|
|
|
- // ' and nothing else
|
|
|
- const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
|
|
|
- const isUrl = /^(https?):\/\//.test(filepath)
|
|
|
- if (!isUrl) {
|
|
|
- try {
|
|
|
- const mime = Filesystem.mimeType(filepath)
|
|
|
- const filename = path.basename(filepath)
|
|
|
- // Handle SVG as raw text content, not as base64 image
|
|
|
- if (mime === "image/svg+xml") {
|
|
|
- event.preventDefault()
|
|
|
- const content = await Filesystem.readText(filepath).catch(() => {})
|
|
|
- if (content) {
|
|
|
- pasteText(content, `[SVG: ${filename ?? "image"}]`)
|
|
|
- return
|
|
|
- }
|
|
|
+ // Normalize line endings at the boundary
|
|
|
+ // Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
|
|
|
+ // Replace CRLF first, then any remaining CR
|
|
|
+ const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
|
|
+ const pastedContent = normalizedText.trim()
|
|
|
+ if (!pastedContent) {
|
|
|
+ command.trigger("prompt.paste")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // trim ' from the beginning and end of the pasted content. just
|
|
|
+ // ' and nothing else
|
|
|
+ const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
|
|
|
+ const isUrl = /^(https?):\/\//.test(filepath)
|
|
|
+ if (!isUrl) {
|
|
|
+ try {
|
|
|
+ const mime = Filesystem.mimeType(filepath)
|
|
|
+ const filename = path.basename(filepath)
|
|
|
+ // Handle SVG as raw text content, not as base64 image
|
|
|
+ if (mime === "image/svg+xml") {
|
|
|
+ event.preventDefault()
|
|
|
+ const content = await Filesystem.readText(filepath).catch(() => {})
|
|
|
+ if (content) {
|
|
|
+ pasteText(content, `[SVG: ${filename ?? "image"}]`)
|
|
|
+ return
|
|
|
}
|
|
|
- if (mime.startsWith("image/")) {
|
|
|
- event.preventDefault()
|
|
|
- const content = await Filesystem.readArrayBuffer(filepath)
|
|
|
- .then((buffer) => Buffer.from(buffer).toString("base64"))
|
|
|
- .catch(() => {})
|
|
|
- if (content) {
|
|
|
- await pasteImage({
|
|
|
- filename,
|
|
|
- mime,
|
|
|
- content,
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
+ }
|
|
|
+ if (mime.startsWith("image/")) {
|
|
|
+ event.preventDefault()
|
|
|
+ const content = await Filesystem.readArrayBuffer(filepath)
|
|
|
+ .then((buffer) => Buffer.from(buffer).toString("base64"))
|
|
|
+ .catch(() => {})
|
|
|
+ if (content) {
|
|
|
+ await pasteImage({
|
|
|
+ filename,
|
|
|
+ mime,
|
|
|
+ content,
|
|
|
+ })
|
|
|
+ return
|
|
|
}
|
|
|
- } catch {}
|
|
|
- }
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
+ }
|
|
|
|
|
|
- const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
|
|
- if (
|
|
|
- (lineCount >= 3 || pastedContent.length > 150) &&
|
|
|
- !sync.data.config.experimental?.disable_paste_summary
|
|
|
- ) {
|
|
|
- event.preventDefault()
|
|
|
- pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
|
|
- return
|
|
|
- }
|
|
|
+ const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
|
|
+ if (
|
|
|
+ (lineCount >= 3 || pastedContent.length > 150) &&
|
|
|
+ !sync.data.config.experimental?.disable_paste_summary
|
|
|
+ ) {
|
|
|
+ event.preventDefault()
|
|
|
+ pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // Force layout update and render for the pasted content
|
|
|
- setTimeout(() => {
|
|
|
- // setTimeout is a workaround and needs to be addressed properly
|
|
|
- if (!input || input.isDestroyed) return
|
|
|
- input.getLayoutNode().markDirty()
|
|
|
- renderer.requestRender()
|
|
|
- }, 0)
|
|
|
- }}
|
|
|
- ref={(r: TextareaRenderable) => {
|
|
|
- input = r
|
|
|
- if (promptPartTypeId === 0) {
|
|
|
- promptPartTypeId = input.extmarks.registerType("prompt-part")
|
|
|
- }
|
|
|
- props.ref?.(ref)
|
|
|
- setTimeout(() => {
|
|
|
- // setTimeout is a workaround and needs to be addressed properly
|
|
|
- if (!input || input.isDestroyed) return
|
|
|
- input.cursorColor = theme.text
|
|
|
- }, 0)
|
|
|
- }}
|
|
|
- onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
|
|
- focusedBackgroundColor={theme.backgroundElement}
|
|
|
- cursorColor={theme.text}
|
|
|
- syntaxStyle={syntax()}
|
|
|
- />
|
|
|
- <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
|
|
- <text fg={highlight()}>
|
|
|
- {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
|
|
- </text>
|
|
|
- <Show when={store.mode === "normal"}>
|
|
|
+ // Force layout update and render for the pasted content
|
|
|
+ setTimeout(() => {
|
|
|
+ // setTimeout is a workaround and needs to be addressed properly
|
|
|
+ if (!input || input.isDestroyed) return
|
|
|
+ input.getLayoutNode().markDirty()
|
|
|
+ renderer.requestRender()
|
|
|
+ }, 0)
|
|
|
+ }}
|
|
|
+ ref={(r: TextareaRenderable) => {
|
|
|
+ input = r
|
|
|
+ if (promptPartTypeId === 0) {
|
|
|
+ promptPartTypeId = input.extmarks.registerType("prompt-part")
|
|
|
+ }
|
|
|
+ props.ref?.(ref)
|
|
|
+ setTimeout(() => {
|
|
|
+ // setTimeout is a workaround and needs to be addressed properly
|
|
|
+ if (!input || input.isDestroyed) return
|
|
|
+ input.cursorColor = theme.text
|
|
|
+ }, 0)
|
|
|
+ }}
|
|
|
+ onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
|
|
+ focusedBackgroundColor={theme.backgroundElement}
|
|
|
+ cursorColor={theme.text}
|
|
|
+ syntaxStyle={syntax()}
|
|
|
+ />
|
|
|
+ <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
|
|
+ <Switch>
|
|
|
+ <Match when={store.mode === "normal"}>
|
|
|
<box flexDirection="row" gap={1}>
|
|
|
+ <text fg={highlight()}>{Locale.titlecase(local.agent.current().name)} </text>
|
|
|
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
|
|
{local.model.parsed().model}
|
|
|
</text>
|
|
|
@@ -1027,141 +1017,29 @@ export function Prompt(props: PromptProps) {
|
|
|
</text>
|
|
|
</Show>
|
|
|
</box>
|
|
|
- </Show>
|
|
|
- </box>
|
|
|
- </box>
|
|
|
- </box>
|
|
|
- <box
|
|
|
- height={1}
|
|
|
- border={["left"]}
|
|
|
- borderColor={highlight()}
|
|
|
- customBorderChars={{
|
|
|
- ...EmptyBorder,
|
|
|
- vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
|
|
- }}
|
|
|
- >
|
|
|
- <box
|
|
|
- height={1}
|
|
|
- border={["bottom"]}
|
|
|
- borderColor={theme.backgroundElement}
|
|
|
- customBorderChars={
|
|
|
- theme.backgroundElement.a !== 0
|
|
|
- ? {
|
|
|
- ...EmptyBorder,
|
|
|
- horizontal: "▀",
|
|
|
- }
|
|
|
- : {
|
|
|
- ...EmptyBorder,
|
|
|
- horizontal: " ",
|
|
|
- }
|
|
|
- }
|
|
|
- />
|
|
|
- </box>
|
|
|
- <box flexDirection="row" justifyContent="space-between">
|
|
|
- <Show when={status().type !== "idle"} fallback={<text />}>
|
|
|
- <box
|
|
|
- flexDirection="row"
|
|
|
- gap={1}
|
|
|
- flexGrow={1}
|
|
|
- justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
|
|
- >
|
|
|
- <box flexShrink={0} flexDirection="row" gap={1}>
|
|
|
- <box marginLeft={1}>
|
|
|
- <Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
|
|
- <spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
|
|
- </Show>
|
|
|
- </box>
|
|
|
- <box flexDirection="row" gap={1} flexShrink={0}>
|
|
|
- {(() => {
|
|
|
- const retry = createMemo(() => {
|
|
|
- const s = status()
|
|
|
- if (s.type !== "retry") return
|
|
|
- return s
|
|
|
- })
|
|
|
- const message = createMemo(() => {
|
|
|
- const r = retry()
|
|
|
- if (!r) return
|
|
|
- if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
|
|
- return "gemini is way too hot right now"
|
|
|
- if (r.message.length > 80) return r.message.slice(0, 80) + "..."
|
|
|
- return r.message
|
|
|
- })
|
|
|
- const isTruncated = createMemo(() => {
|
|
|
- const r = retry()
|
|
|
- if (!r) return false
|
|
|
- return r.message.length > 120
|
|
|
- })
|
|
|
- const [seconds, setSeconds] = createSignal(0)
|
|
|
- onMount(() => {
|
|
|
- const timer = setInterval(() => {
|
|
|
- const next = retry()?.next
|
|
|
- if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
|
|
- }, 1000)
|
|
|
-
|
|
|
- onCleanup(() => {
|
|
|
- clearInterval(timer)
|
|
|
- })
|
|
|
- })
|
|
|
- const handleMessageClick = () => {
|
|
|
- const r = retry()
|
|
|
- if (!r) return
|
|
|
- if (isTruncated()) {
|
|
|
- DialogAlert.show(dialog, "Retry Error", r.message)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const retryText = () => {
|
|
|
- const r = retry()
|
|
|
- if (!r) return ""
|
|
|
- const baseMessage = message()
|
|
|
- const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
|
|
- const duration = formatDuration(seconds())
|
|
|
- const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
|
|
- return baseMessage + truncatedHint + retryInfo
|
|
|
- }
|
|
|
-
|
|
|
- return (
|
|
|
- <Show when={retry()}>
|
|
|
- <box onMouseUp={handleMessageClick}>
|
|
|
- <text fg={theme.error}>{retryText()}</text>
|
|
|
- </box>
|
|
|
- </Show>
|
|
|
- )
|
|
|
- })()}
|
|
|
- </box>
|
|
|
- </box>
|
|
|
- <text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
|
|
- esc{" "}
|
|
|
- <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
|
|
- {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
|
|
- </span>
|
|
|
- </text>
|
|
|
- </box>
|
|
|
- </Show>
|
|
|
- <Show when={status().type !== "retry"}>
|
|
|
- <box gap={2} flexDirection="row">
|
|
|
- <Switch>
|
|
|
- <Match when={store.mode === "normal"}>
|
|
|
- <Show when={local.model.variant.list().length > 0}>
|
|
|
+ <Show when={status().type !== "retry"}>
|
|
|
+ <box gap={2} flexDirection="row">
|
|
|
+ <Show when={local.model.variant.list().length > 0}>
|
|
|
+ <text fg={theme.text}>
|
|
|
+ {keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
|
|
+ </text>
|
|
|
+ </Show>
|
|
|
<text fg={theme.text}>
|
|
|
- {keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
|
|
+ {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
|
</text>
|
|
|
- </Show>
|
|
|
- <text fg={theme.text}>
|
|
|
- {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
|
- </text>
|
|
|
- <text fg={theme.text}>
|
|
|
- {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
|
- </text>
|
|
|
- </Match>
|
|
|
- <Match when={store.mode === "shell"}>
|
|
|
- <text fg={theme.text}>
|
|
|
- esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
|
|
- </text>
|
|
|
- </Match>
|
|
|
- </Switch>
|
|
|
- </box>
|
|
|
- </Show>
|
|
|
+ <text fg={theme.text}>
|
|
|
+ {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
|
+ </text>
|
|
|
+ </box>
|
|
|
+ </Show>
|
|
|
+ </Match>
|
|
|
+ <Match when={store.mode === "shell"}>
|
|
|
+ <text fg={highlight()}>
|
|
|
+ {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
|
|
+ </text>
|
|
|
+ </Match>
|
|
|
+ </Switch>
|
|
|
+ </box>
|
|
|
</box>
|
|
|
</box>
|
|
|
</>
|