plasalid 0.2.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.
- package/LICENSE +213 -0
- package/README.md +176 -0
- package/dist/accounts/taxonomy.d.ts +31 -0
- package/dist/accounts/taxonomy.js +189 -0
- package/dist/ai/agent.d.ts +43 -0
- package/dist/ai/agent.js +155 -0
- package/dist/ai/context.d.ts +4 -0
- package/dist/ai/context.js +33 -0
- package/dist/ai/memory.d.ts +14 -0
- package/dist/ai/memory.js +12 -0
- package/dist/ai/provider.d.ts +67 -0
- package/dist/ai/provider.js +5 -0
- package/dist/ai/providers/anthropic.d.ts +5 -0
- package/dist/ai/providers/anthropic.js +49 -0
- package/dist/ai/providers/index.d.ts +2 -0
- package/dist/ai/providers/index.js +12 -0
- package/dist/ai/providers/openai-compat.d.ts +5 -0
- package/dist/ai/providers/openai-compat.js +147 -0
- package/dist/ai/providers/openai.d.ts +5 -0
- package/dist/ai/providers/openai.js +147 -0
- package/dist/ai/redactor.d.ts +2 -0
- package/dist/ai/redactor.js +91 -0
- package/dist/ai/sanitize.d.ts +14 -0
- package/dist/ai/sanitize.js +25 -0
- package/dist/ai/system-prompt.d.ts +13 -0
- package/dist/ai/system-prompt.js +174 -0
- package/dist/ai/thai-taxonomy-hint.d.ts +8 -0
- package/dist/ai/thai-taxonomy-hint.js +22 -0
- package/dist/ai/thinking-phrases.d.ts +7 -0
- package/dist/ai/thinking-phrases.js +15 -0
- package/dist/ai/thinking.d.ts +7 -0
- package/dist/ai/thinking.js +15 -0
- package/dist/ai/tools/common.d.ts +2 -0
- package/dist/ai/tools/common.js +83 -0
- package/dist/ai/tools/index.d.ts +8 -0
- package/dist/ai/tools/index.js +34 -0
- package/dist/ai/tools/ingest.d.ts +2 -0
- package/dist/ai/tools/ingest.js +202 -0
- package/dist/ai/tools/read.d.ts +2 -0
- package/dist/ai/tools/read.js +123 -0
- package/dist/ai/tools/reconcile.d.ts +2 -0
- package/dist/ai/tools/reconcile.js +227 -0
- package/dist/ai/tools/scan.d.ts +2 -0
- package/dist/ai/tools/scan.js +24 -0
- package/dist/ai/tools/types.d.ts +26 -0
- package/dist/ai/tools/types.js +1 -0
- package/dist/ai/tools.d.ts +18 -0
- package/dist/ai/tools.js +402 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +28 -0
- package/dist/cli/commands/accounts.d.ts +1 -0
- package/dist/cli/commands/accounts.js +86 -0
- package/dist/cli/commands/data.d.ts +1 -0
- package/dist/cli/commands/data.js +28 -0
- package/dist/cli/commands/reconcile.d.ts +2 -0
- package/dist/cli/commands/reconcile.js +15 -0
- package/dist/cli/commands/revert.d.ts +1 -0
- package/dist/cli/commands/revert.js +68 -0
- package/dist/cli/commands/scan.d.ts +4 -0
- package/dist/cli/commands/scan.js +45 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +22 -0
- package/dist/cli/commands/transactions.d.ts +8 -0
- package/dist/cli/commands/transactions.js +92 -0
- package/dist/cli/commands/undo.d.ts +1 -0
- package/dist/cli/commands/undo.js +38 -0
- package/dist/cli/commands.d.ts +14 -0
- package/dist/cli/commands.js +196 -0
- package/dist/cli/format.d.ts +8 -0
- package/dist/cli/format.js +109 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +126 -0
- package/dist/cli/ink/ChatApp.d.ts +8 -0
- package/dist/cli/ink/ChatApp.js +94 -0
- package/dist/cli/ink/PromptFrame.d.ts +10 -0
- package/dist/cli/ink/PromptFrame.js +11 -0
- package/dist/cli/ink/TextInput.d.ts +13 -0
- package/dist/cli/ink/TextInput.js +24 -0
- package/dist/cli/ink/hooks/useAgent.d.ts +27 -0
- package/dist/cli/ink/hooks/useAgent.js +65 -0
- package/dist/cli/ink/hooks/useCtrlCExit.d.ts +16 -0
- package/dist/cli/ink/hooks/useCtrlCExit.js +43 -0
- package/dist/cli/ink/hooks/useFooterText.d.ts +2 -0
- package/dist/cli/ink/hooks/useFooterText.js +43 -0
- package/dist/cli/ink/hooks/useTextInput.d.ts +32 -0
- package/dist/cli/ink/hooks/useTextInput.js +356 -0
- package/dist/cli/ink/messages/AssistantMessage.d.ts +3 -0
- package/dist/cli/ink/messages/AssistantMessage.js +6 -0
- package/dist/cli/ink/messages/ErrorMessage.d.ts +4 -0
- package/dist/cli/ink/messages/ErrorMessage.js +6 -0
- package/dist/cli/ink/messages/InterruptedMessage.d.ts +1 -0
- package/dist/cli/ink/messages/InterruptedMessage.js +6 -0
- package/dist/cli/ink/messages/ThinkingLine.d.ts +12 -0
- package/dist/cli/ink/messages/ThinkingLine.js +23 -0
- package/dist/cli/ink/messages/UserMessage.d.ts +4 -0
- package/dist/cli/ink/messages/UserMessage.js +15 -0
- package/dist/cli/ink/mount.d.ts +6 -0
- package/dist/cli/ink/mount.js +12 -0
- package/dist/cli/logo.d.ts +1 -0
- package/dist/cli/logo.js +20 -0
- package/dist/cli/setup.d.ts +2 -0
- package/dist/cli/setup.js +210 -0
- package/dist/cli/ux.d.ts +38 -0
- package/dist/cli/ux.js +104 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +66 -0
- package/dist/currency.d.ts +6 -0
- package/dist/currency.js +19 -0
- package/dist/db/connection.d.ts +5 -0
- package/dist/db/connection.js +45 -0
- package/dist/db/encryption.d.ts +11 -0
- package/dist/db/encryption.js +45 -0
- package/dist/db/helpers.d.ts +16 -0
- package/dist/db/helpers.js +45 -0
- package/dist/db/queries/account_balance.d.ts +61 -0
- package/dist/db/queries/account_balance.js +146 -0
- package/dist/db/queries/journal.d.ts +95 -0
- package/dist/db/queries/journal.js +204 -0
- package/dist/db/queries/search.d.ts +7 -0
- package/dist/db/queries/search.js +19 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +95 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parser/pdf.d.ts +14 -0
- package/dist/parser/pdf.js +40 -0
- package/dist/parser/pipeline.d.ts +44 -0
- package/dist/parser/pipeline.js +160 -0
- package/dist/parser/prompts.d.ts +8 -0
- package/dist/parser/prompts.js +20 -0
- package/dist/parser/walker.d.ts +8 -0
- package/dist/parser/walker.js +42 -0
- package/dist/reconciler/pipeline.d.ts +17 -0
- package/dist/reconciler/pipeline.js +45 -0
- package/dist/reconciler/prompts.d.ts +12 -0
- package/dist/reconciler/prompts.js +22 -0
- package/dist/scanner/password-store.d.ts +34 -0
- package/dist/scanner/password-store.js +83 -0
- package/dist/scanner/pdf-unlock.d.ts +17 -0
- package/dist/scanner/pdf-unlock.js +48 -0
- package/dist/scanner/pdf.d.ts +17 -0
- package/dist/scanner/pdf.js +36 -0
- package/dist/scanner/pipeline.d.ts +32 -0
- package/dist/scanner/pipeline.js +137 -0
- package/dist/scanner/prompts.d.ts +8 -0
- package/dist/scanner/prompts.js +20 -0
- package/dist/scanner/state-machine.d.ts +60 -0
- package/dist/scanner/state-machine.js +64 -0
- package/dist/scanner/unlock.d.ts +24 -0
- package/dist/scanner/unlock.js +122 -0
- package/dist/scanner/walker.d.ts +8 -0
- package/dist/scanner/walker.js +42 -0
- package/package.json +65 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
interface Props {
|
|
3
|
+
db: Database.Database;
|
|
4
|
+
/** Auto-kick-off message to send silently on mount (onboarding). */
|
|
5
|
+
onboardingPrompt?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ChatApp({ db, onboardingPrompt }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Static, Text, useApp } from "ink";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { PromptFrame } from "./PromptFrame.js";
|
|
6
|
+
import { ThinkingLine } from "./messages/ThinkingLine.js";
|
|
7
|
+
import { UserMessage } from "./messages/UserMessage.js";
|
|
8
|
+
import { AssistantMessage } from "./messages/AssistantMessage.js";
|
|
9
|
+
import { ErrorMessage } from "./messages/ErrorMessage.js";
|
|
10
|
+
import { InterruptedMessage } from "./messages/InterruptedMessage.js";
|
|
11
|
+
import { useTextInput } from "./hooks/useTextInput.js";
|
|
12
|
+
import { useAgent } from "./hooks/useAgent.js";
|
|
13
|
+
import { useCtrlCExit } from "./hooks/useCtrlCExit.js";
|
|
14
|
+
import { useFooterText } from "./hooks/useFooterText.js";
|
|
15
|
+
let turnSeq = 0;
|
|
16
|
+
const nextId = () => `t${++turnSeq}`;
|
|
17
|
+
export function ChatApp({ db, onboardingPrompt }) {
|
|
18
|
+
const { exit } = useApp();
|
|
19
|
+
const [turns, setTurns] = useState([]);
|
|
20
|
+
const footerText = useFooterText(db);
|
|
21
|
+
const ctrlC = useCtrlCExit();
|
|
22
|
+
const pushTurn = useCallback((t) => {
|
|
23
|
+
setTurns(prev => [...prev, t]);
|
|
24
|
+
}, []);
|
|
25
|
+
const { thinking, submit: runAgent, cancel, isBusy } = useAgent({
|
|
26
|
+
db,
|
|
27
|
+
onEvent: (e) => {
|
|
28
|
+
if (e.type === "response") {
|
|
29
|
+
pushTurn({ id: nextId(), kind: "assistant", text: e.text });
|
|
30
|
+
}
|
|
31
|
+
else if (e.type === "error") {
|
|
32
|
+
pushTurn({ id: nextId(), kind: "error", error: e.error });
|
|
33
|
+
}
|
|
34
|
+
else if (e.type === "interrupted") {
|
|
35
|
+
pushTurn({ id: nextId(), kind: "interrupted" });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const handleSubmit = useCallback((raw) => {
|
|
40
|
+
const trimmed = raw.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return;
|
|
43
|
+
if (trimmed === "/quit" || trimmed === "/exit" || trimmed === "/q") {
|
|
44
|
+
exit();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
pushTurn({ id: nextId(), kind: "user", text: trimmed });
|
|
48
|
+
runAgent(trimmed);
|
|
49
|
+
}, [exit, pushTurn, runAgent]);
|
|
50
|
+
const onCtrlCFromInput = useCallback((bufferEmpty) => {
|
|
51
|
+
const action = ctrlC.trigger({ bufferEmpty, busy: isBusy });
|
|
52
|
+
if (action === "clear-input") {
|
|
53
|
+
textInput.reset();
|
|
54
|
+
}
|
|
55
|
+
else if (action === "abort") {
|
|
56
|
+
cancel();
|
|
57
|
+
}
|
|
58
|
+
else if (action === "exit") {
|
|
59
|
+
exit();
|
|
60
|
+
}
|
|
61
|
+
// "arm" → do nothing; hint renders via ctrlC.pending
|
|
62
|
+
}, [ctrlC, isBusy, cancel, exit]);
|
|
63
|
+
const textInput = useTextInput({
|
|
64
|
+
onSubmit: handleSubmit,
|
|
65
|
+
onCtrlC: onCtrlCFromInput,
|
|
66
|
+
onChange: () => {
|
|
67
|
+
if (ctrlC.pending)
|
|
68
|
+
ctrlC.clear();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Auto-onboarding: fire exactly once on mount
|
|
72
|
+
const onboardedRef = useRef(false);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (onboardedRef.current)
|
|
75
|
+
return;
|
|
76
|
+
onboardedRef.current = true;
|
|
77
|
+
if (onboardingPrompt)
|
|
78
|
+
runAgent(onboardingPrompt);
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, []);
|
|
81
|
+
const exitHint = ctrlC.pending
|
|
82
|
+
? chalk.yellow(" press ctrl+c again to exit")
|
|
83
|
+
: undefined;
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: turns, children: (t) => _jsx(TurnView, { turn: t }, t.id) }), thinking ? _jsx(ThinkingLine, { state: thinking }) : null, !isBusy ? (_jsx(PromptFrame, { buffer: textInput.buffer, footerText: footerText, showCaret: !isBusy, banner: exitHint })) : null] }));
|
|
85
|
+
}
|
|
86
|
+
function TurnView({ turn }) {
|
|
87
|
+
switch (turn.kind) {
|
|
88
|
+
case "user": return _jsx(UserMessage, { text: turn.text });
|
|
89
|
+
case "assistant": return _jsx(AssistantMessage, { text: turn.text });
|
|
90
|
+
case "error": return _jsx(ErrorMessage, { error: turn.error, context: turn.context });
|
|
91
|
+
case "interrupted": return _jsx(InterruptedMessage, {});
|
|
92
|
+
default: return _jsx(Text, {});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TextBuffer } from "./hooks/useTextInput.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
buffer: TextBuffer;
|
|
4
|
+
footerText: string;
|
|
5
|
+
showCaret: boolean;
|
|
6
|
+
banner?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Framed prompt: top rule, input area, bottom rule, footer, optional banner below. */
|
|
9
|
+
export declare function PromptFrame({ buffer, footerText, showCaret, banner }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useStdout } from "ink";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { TextInput } from "./TextInput.js";
|
|
5
|
+
/** Framed prompt: top rule, input area, bottom rule, footer, optional banner below. */
|
|
6
|
+
export function PromptFrame({ buffer, footerText, showCaret, banner }) {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const cols = stdout?.columns || 80;
|
|
9
|
+
const rule = chalk.dim("─".repeat(cols));
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: rule }), _jsx(TextInput, { buffer: buffer, prompt: chalk.dim("❯ "), showCaret: showCaret }), _jsx(Text, { children: rule }), _jsx(Text, { children: chalk.dim(` ${footerText}`) }), banner ? _jsx(Text, { children: banner }) : null] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TextBuffer } from "./hooks/useTextInput.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
buffer: TextBuffer;
|
|
4
|
+
prompt: string;
|
|
5
|
+
/** Render caret inverted on the current character. */
|
|
6
|
+
showCaret: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders a multiline text buffer with an inverted caret at the cursor position.
|
|
10
|
+
* Prefixes the first line with `prompt`; continuation lines are indented to match.
|
|
11
|
+
*/
|
|
12
|
+
export declare function TextInput({ buffer, prompt, showCaret }: Props): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* Renders a multiline text buffer with an inverted caret at the cursor position.
|
|
5
|
+
* Prefixes the first line with `prompt`; continuation lines are indented to match.
|
|
6
|
+
*/
|
|
7
|
+
export function TextInput({ buffer, prompt, showCaret }) {
|
|
8
|
+
const plainPromptLen = stripAnsi(prompt).length;
|
|
9
|
+
const indent = " ".repeat(plainPromptLen);
|
|
10
|
+
return (_jsx(_Fragment, { children: buffer.lines.map((line, row) => {
|
|
11
|
+
const isCursorRow = showCaret && row === buffer.row;
|
|
12
|
+
const prefix = row === 0 ? prompt : indent;
|
|
13
|
+
if (!isCursorRow) {
|
|
14
|
+
return (_jsxs(Text, { children: [prefix, line || " "] }, row));
|
|
15
|
+
}
|
|
16
|
+
const before = line.slice(0, buffer.col);
|
|
17
|
+
const caretChar = buffer.col < line.length ? line[buffer.col] : " ";
|
|
18
|
+
const after = buffer.col < line.length ? line.slice(buffer.col + 1) : "";
|
|
19
|
+
return (_jsxs(Text, { children: [prefix, before, _jsx(Text, { inverse: true, children: caretChar }), after] }, row));
|
|
20
|
+
}) }));
|
|
21
|
+
}
|
|
22
|
+
function stripAnsi(str) {
|
|
23
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
import type { ThinkingState } from "../messages/ThinkingLine.js";
|
|
3
|
+
export type AgentEvent = {
|
|
4
|
+
type: "response";
|
|
5
|
+
text: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: "error";
|
|
8
|
+
error: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
type: "interrupted";
|
|
11
|
+
};
|
|
12
|
+
interface UseAgentOpts {
|
|
13
|
+
db: Database.Database;
|
|
14
|
+
onEvent: (event: AgentEvent) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bridges handleMessage with Ink state. submit() kicks off a run and owns the
|
|
18
|
+
* AbortController; cancel() aborts whatever's in flight. state.thinking is null
|
|
19
|
+
* when idle, a ThinkingState otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useAgent({ db, onEvent }: UseAgentOpts): {
|
|
22
|
+
thinking: ThinkingState | null;
|
|
23
|
+
submit: (text: string) => void;
|
|
24
|
+
cancel: () => void;
|
|
25
|
+
isBusy: boolean;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { handleChatMessage, AbortedError } from "../../../ai/agent.js";
|
|
3
|
+
import { pickThinking } from "../../../ai/thinking.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bridges handleMessage with Ink state. submit() kicks off a run and owns the
|
|
6
|
+
* AbortController; cancel() aborts whatever's in flight. state.thinking is null
|
|
7
|
+
* when idle, a ThinkingState otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export function useAgent({ db, onEvent }) {
|
|
10
|
+
const [thinking, setThinking] = useState(null);
|
|
11
|
+
const controllerRef = useRef(null);
|
|
12
|
+
const onEventRef = useRef(onEvent);
|
|
13
|
+
onEventRef.current = onEvent;
|
|
14
|
+
const inflightRef = useRef(false);
|
|
15
|
+
const cancel = useCallback(() => {
|
|
16
|
+
const c = controllerRef.current;
|
|
17
|
+
if (c && !c.signal.aborted)
|
|
18
|
+
c.abort();
|
|
19
|
+
}, []);
|
|
20
|
+
const submit = useCallback((text) => {
|
|
21
|
+
if (inflightRef.current)
|
|
22
|
+
return; // ignore overlapping submits
|
|
23
|
+
inflightRef.current = true;
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
controllerRef.current = controller;
|
|
26
|
+
setThinking({ phrase: pickThinking() });
|
|
27
|
+
const onProgress = ({ phase, toolName, toolCount, elapsedMs }) => {
|
|
28
|
+
setThinking(prev => prev
|
|
29
|
+
? { ...prev, progress: { phase, toolName, toolCount, elapsedMs } }
|
|
30
|
+
: prev);
|
|
31
|
+
};
|
|
32
|
+
(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await handleChatMessage(db, text, onProgress, controller.signal);
|
|
35
|
+
if (controller.signal.aborted) {
|
|
36
|
+
onEventRef.current({ type: "interrupted" });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
onEventRef.current({ type: "response", text: response });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof AbortedError || controller.signal.aborted) {
|
|
44
|
+
onEventRef.current({ type: "interrupted" });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
onEventRef.current({ type: "error", error: err });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
inflightRef.current = false;
|
|
52
|
+
setThinking(null);
|
|
53
|
+
if (controllerRef.current === controller) {
|
|
54
|
+
controllerRef.current = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
}, [db]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
return () => {
|
|
61
|
+
controllerRef.current?.abort();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
return { thinking, submit, cancel, isBusy: thinking !== null };
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-step exit state machine, Claude-style.
|
|
3
|
+
*
|
|
4
|
+
* - trigger({ bufferEmpty: false }) → caller should clear the input; returns "cleared"
|
|
5
|
+
* - trigger({ bufferEmpty: true, busy: true }) → caller should abort; returns "abort"
|
|
6
|
+
* - trigger({ bufferEmpty: true, busy: false }): first call sets `pending=true`
|
|
7
|
+
* and returns "arm"; second call within 2s returns "exit".
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCtrlCExit(): {
|
|
10
|
+
pending: boolean;
|
|
11
|
+
trigger: (opts: {
|
|
12
|
+
bufferEmpty: boolean;
|
|
13
|
+
busy: boolean;
|
|
14
|
+
}) => "clear-input" | "abort" | "arm" | "exit";
|
|
15
|
+
clear: () => void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
const EXIT_WINDOW_MS = 2000;
|
|
3
|
+
/**
|
|
4
|
+
* Two-step exit state machine, Claude-style.
|
|
5
|
+
*
|
|
6
|
+
* - trigger({ bufferEmpty: false }) → caller should clear the input; returns "cleared"
|
|
7
|
+
* - trigger({ bufferEmpty: true, busy: true }) → caller should abort; returns "abort"
|
|
8
|
+
* - trigger({ bufferEmpty: true, busy: false }): first call sets `pending=true`
|
|
9
|
+
* and returns "arm"; second call within 2s returns "exit".
|
|
10
|
+
*/
|
|
11
|
+
export function useCtrlCExit() {
|
|
12
|
+
const [pending, setPending] = useState(false);
|
|
13
|
+
const timerRef = useRef(null);
|
|
14
|
+
const clear = useCallback(() => {
|
|
15
|
+
if (timerRef.current)
|
|
16
|
+
clearTimeout(timerRef.current);
|
|
17
|
+
timerRef.current = null;
|
|
18
|
+
setPending(false);
|
|
19
|
+
}, []);
|
|
20
|
+
const trigger = useCallback((opts) => {
|
|
21
|
+
if (opts.busy) {
|
|
22
|
+
clear();
|
|
23
|
+
return "abort";
|
|
24
|
+
}
|
|
25
|
+
if (!opts.bufferEmpty) {
|
|
26
|
+
clear();
|
|
27
|
+
return "clear-input";
|
|
28
|
+
}
|
|
29
|
+
if (pending) {
|
|
30
|
+
clear();
|
|
31
|
+
return "exit";
|
|
32
|
+
}
|
|
33
|
+
if (timerRef.current)
|
|
34
|
+
clearTimeout(timerRef.current);
|
|
35
|
+
setPending(true);
|
|
36
|
+
timerRef.current = setTimeout(() => {
|
|
37
|
+
setPending(false);
|
|
38
|
+
timerRef.current = null;
|
|
39
|
+
}, EXIT_WINDOW_MS);
|
|
40
|
+
return "arm";
|
|
41
|
+
}, [pending, clear]);
|
|
42
|
+
return { pending, trigger, clear };
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
const HINTS = [
|
|
3
|
+
"try: what is my net worth?",
|
|
4
|
+
"try: how much did I spend on food this month?",
|
|
5
|
+
"try: when is my credit card due?",
|
|
6
|
+
"try: show me transactions over 5000 baht",
|
|
7
|
+
"try: which credit card has the highest balance?",
|
|
8
|
+
"try: how much income did I receive last month?",
|
|
9
|
+
"try: list all accounts",
|
|
10
|
+
];
|
|
11
|
+
export function useFooterText(db) {
|
|
12
|
+
const [tick, setTick] = useState(0);
|
|
13
|
+
const [hintIdx] = useState(() => Math.floor(Math.random() * HINTS.length));
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const id = setInterval(() => setTick((t) => t + 1), 60_000);
|
|
16
|
+
return () => clearInterval(id);
|
|
17
|
+
}, []);
|
|
18
|
+
return useMemo(() => {
|
|
19
|
+
const lastScan = db
|
|
20
|
+
.prepare(`SELECT MAX(scanned_at) AS ts FROM scanned_files WHERE status = 'scanned'`)
|
|
21
|
+
.get();
|
|
22
|
+
let scanStr = "";
|
|
23
|
+
if (lastScan?.ts) {
|
|
24
|
+
const diffMs = Date.now() - new Date(lastScan.ts + "Z").getTime();
|
|
25
|
+
const mins = Math.floor(diffMs / 60000);
|
|
26
|
+
if (mins < 1)
|
|
27
|
+
scanStr = "scanned just now";
|
|
28
|
+
else if (mins < 60)
|
|
29
|
+
scanStr = `scanned ${mins}m ago`;
|
|
30
|
+
else if (mins < 1440)
|
|
31
|
+
scanStr = `scanned ${Math.floor(mins / 60)}h ago`;
|
|
32
|
+
else
|
|
33
|
+
scanStr = `scanned ${Math.floor(mins / 1440)}d ago`;
|
|
34
|
+
}
|
|
35
|
+
const idx = (hintIdx + tick) % HINTS.length;
|
|
36
|
+
const parts = ["Plasalid"];
|
|
37
|
+
if (scanStr)
|
|
38
|
+
parts.push(scanStr);
|
|
39
|
+
parts.push(HINTS[idx]);
|
|
40
|
+
parts.push("ctrl+c to exit");
|
|
41
|
+
return parts.join(" | ");
|
|
42
|
+
}, [db, tick, hintIdx]);
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A multiline text buffer with a 2D cursor. */
|
|
2
|
+
export interface TextBuffer {
|
|
3
|
+
lines: string[];
|
|
4
|
+
row: number;
|
|
5
|
+
col: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UseTextInputOpts {
|
|
8
|
+
onSubmit: (text: string) => void;
|
|
9
|
+
onCtrlC: (bufferEmpty: boolean) => void;
|
|
10
|
+
/** Called every time the buffer changes — can be used for hints. */
|
|
11
|
+
onChange?: (buf: TextBuffer) => void;
|
|
12
|
+
/** Return true to signal the key was handled and default behavior should be skipped. */
|
|
13
|
+
onKey?: (key: {
|
|
14
|
+
code: number;
|
|
15
|
+
raw: string;
|
|
16
|
+
}) => boolean | void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Raw-stdin driven keystroke state machine that owns a multiline buffer and
|
|
20
|
+
* exposes its current state plus reset/insert helpers. Purely stateful — Ink
|
|
21
|
+
* re-renders whenever the buffer changes via setBuffer.
|
|
22
|
+
*
|
|
23
|
+
* Handles: Enter/submit, Backspace, Ctrl+A/E/K/U/W, arrow keys, Option+←/→,
|
|
24
|
+
* Option+B/F, Option+Backspace, Kitty Cmd+Backspace, Shift+Enter (newline),
|
|
25
|
+
* bracketed paste.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useTextInput(opts: UseTextInputOpts): {
|
|
28
|
+
buffer: TextBuffer;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
insert: (text: string) => void;
|
|
31
|
+
isEmpty: boolean;
|
|
32
|
+
};
|