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.
This commit is contained in:
commit
5b8503fcd9
93 files changed
+12775
No files matched your search
+116
@@ -0,0 +1,116 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user