Files
shiro-neko/src/ui/Ask.tsx
T
Muhammad Zakir Ramadhan 5b8503fcd9 Initial commit: shiro-neko 0.1.0-beta.1
Agentic coding CLI on Bun, Ink, and the AI SDK.

Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff.

Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming.

Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model.

Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents.

State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies.

Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems.

404 tests, typecheck clean.
2026-09-02 17:30:18 +07:00

117 lines
3.8 KiB
TypeScript

import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import React, { useState } from 'react';
import type { AskRequest } from '../ask';
import { InlineMarkdown } from './Markdown';
import { PromptInput } from './PromptInput';
export type AskPending = { req: AskRequest; resolve: (answers: string[] | undefined) => void };
/** Bridges the ask tool's promise into React state, the same shape as the approval bridge. */
export type AskBridge = {
bind: (fn: (p: AskPending | undefined) => void) => void;
ask: (req: AskRequest) => Promise<string[] | undefined>;
};
export function createAskBridge(): AskBridge {
let setter: ((p: AskPending | undefined) => void) | undefined;
return {
bind(fn) {
setter = fn;
},
ask(req) {
return new Promise((resolve) => {
// No UI mounted means no one can answer; resolving undefined lets the tool
// tell the model to decide for itself rather than hanging forever.
if (!setter) return resolve(undefined);
setter({
req,
resolve: (answers) => {
setter?.(undefined);
resolve(answers);
},
});
});
},
};
}
const TYPE_YOUR_OWN = '__own__';
/**
* The question popup.
*
* Options become a picker; no options, or "type your own", falls back to free text.
* Escape resolves undefined rather than leaving the tool waiting.
*/
export function AskPanel({ pending }: { pending: AskPending }) {
const { question, options, multiple } = pending.req;
const [chosen, setChosen] = useState<string[]>([]);
const [typing, setTyping] = useState(!options || options.length === 0);
const [draft, setDraft] = useState('');
useInput(
(_input, key) => {
if (key.escape) pending.resolve(undefined);
},
{ isActive: !typing },
);
const items = [
...(options ?? []).map((o) => ({
key: o.label,
label: chosen.includes(o.label) ? `[x] ${o.label}` : multiple ? `[ ] ${o.label}` : o.label,
value: o.label,
})),
...(multiple && chosen.length > 0 ? [{ key: '__done__', label: `-- submit ${chosen.length} --`, value: '__done__' }] : []),
{ key: TYPE_YOUR_OWN, label: 'type your own answer...', value: TYPE_YOUR_OWN },
];
const detailOf = (label: string) => (options ?? []).find((o) => o.label === label)?.detail;
return (
<Box flexDirection="column" borderStyle="double" borderColor="yellow" paddingX={1}>
<Text color="yellow" bold>
shiro is asking
</Text>
<Box marginBottom={1}>
<InlineMarkdown text={question} />
</Box>
{typing ? (
<Box>
<Text color="yellow">{'> '}</Text>
<PromptInput
value={draft}
onChange={setDraft}
onSubmit={(v) => pending.resolve(v.trim() ? [v.trim()] : undefined)}
placeholder="type your answer, enter to send"
/>
</Box>
) : (
<Box flexDirection="column">
<SelectInput
items={items}
limit={10}
onSelect={(item) => {
if (item.value === TYPE_YOUR_OWN) return setTyping(true);
if (item.value === '__done__') return pending.resolve(chosen);
if (!multiple) return pending.resolve([item.value]);
setChosen((c) => (c.includes(item.value) ? c.filter((x) => x !== item.value) : [...c, item.value]));
}}
onHighlight={(item) => {
const detail = detailOf(item.value);
if (detail) setDraft(detail);
else setDraft('');
}}
/>
{draft.length > 0 && <Text dimColor>{draft}</Text>}
<Text dimColor>
{multiple ? 'space/enter toggles, pick submit when done' : 'enter to choose'} | esc to skip
</Text>
</Box>
)}
</Box>
);
}