@meowlynxsea/koi 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. package/src/types/cross-spawn.d.ts +24 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Command Panel Component
3
+ *
4
+ * A modal command palette with a single-line filter input and a scrollable
5
+ * list of sectioned commands. Opened with Ctrl+P or "/" in empty prompt.
6
+ * Closed with Ctrl+P, Esc, or clearing the input.
7
+ */
8
+
9
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
10
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
11
+ import { createTextAttributes, type KeyBinding } from "@opentui/core";
12
+ import type { TextareaRenderable } from "@opentui/core";
13
+
14
+ export interface CommandDef {
15
+ id: string;
16
+ label: string;
17
+ section: string;
18
+ action: () => void;
19
+ }
20
+
21
+ interface CommandPanelProps {
22
+ isActive: boolean;
23
+ onClose: () => void;
24
+ commands: CommandDef[];
25
+ }
26
+
27
+ interface ListItem {
28
+ type: "header" | "command";
29
+ section?: string;
30
+ cmd?: CommandDef;
31
+ cmdIndex?: number;
32
+ }
33
+
34
+ export function CommandPanel({ isActive, onClose, commands }: CommandPanelProps) {
35
+ const { width, height } = useTerminalDimensions();
36
+ const inputRef = useRef<TextareaRenderable>(null);
37
+ const [filterText, setFilterText] = useState("");
38
+ const [selectedCmdIndex, setSelectedCmdIndex] = useState(0);
39
+ const scrollOffsetRef = useRef(0);
40
+
41
+ const panelWidth = Math.min(70, Math.max(40, Math.floor(width * 0.7)));
42
+ const listHeight = Math.min(12, Math.floor(height * 0.4));
43
+
44
+ // Reset when opened
45
+ useLayoutEffect(() => {
46
+ if (isActive) {
47
+ setFilterText("");
48
+ setSelectedCmdIndex(0);
49
+ scrollOffsetRef.current = 0;
50
+ const ta = inputRef.current;
51
+ if (ta) {
52
+ ta.editBuffer.replaceText("");
53
+ ta.focus();
54
+ }
55
+ }
56
+ }, [isActive]);
57
+
58
+ // Build filtered flat list
59
+ const query = filterText;
60
+ const { flatItems, cmdCount } = useMemo(() => {
61
+ let filtered = commands;
62
+ if (query) {
63
+ const q = query.toLowerCase();
64
+ // Filter and sort by match priority: id > label > section
65
+ filtered = commands
66
+ .map((cmd) => {
67
+ const idMatch = cmd.id.toLowerCase().includes(q);
68
+ const labelMatch = cmd.label.toLowerCase().includes(q);
69
+ const sectionMatch = cmd.section.toLowerCase().includes(q);
70
+ // Priority: 0 = id match (highest), 1 = label match, 2 = section match, 3 = no match
71
+ const priority = !idMatch && !labelMatch && !sectionMatch ? 3
72
+ : idMatch ? 0
73
+ : labelMatch ? 1
74
+ : 2;
75
+ return { cmd, priority };
76
+ })
77
+ .filter((item) => item.priority < 3)
78
+ .sort((a, b) => a.priority - b.priority)
79
+ .map((item) => item.cmd);
80
+ }
81
+
82
+ const grouped = new Map<string, CommandDef[]>();
83
+ for (const cmd of filtered) {
84
+ if (!grouped.has(cmd.section)) grouped.set(cmd.section, []);
85
+ grouped.get(cmd.section)!.push(cmd);
86
+ }
87
+
88
+ const items: ListItem[] = [];
89
+ let cmdIdx = 0;
90
+ for (const [section, cmds] of grouped) {
91
+ items.push({ type: "header", section });
92
+ for (const cmd of cmds) {
93
+ items.push({ type: "command", cmd, cmdIndex: cmdIdx });
94
+ cmdIdx++;
95
+ }
96
+ }
97
+ return { flatItems: items, cmdCount: cmdIdx };
98
+ }, [commands, query]);
99
+
100
+ // Effective scroll offset (synced from ref)
101
+ const effectiveScrollOffset = scrollOffsetRef.current;
102
+
103
+ // Global keyboard for navigation and close shortcuts
104
+ useKeyboard((key) => {
105
+ if (!isActive) return;
106
+ if (key.ctrl && key.name === "p") {
107
+ key.preventDefault();
108
+ key.stopPropagation();
109
+ onClose();
110
+ return;
111
+ }
112
+ if (key.name === "escape") {
113
+ key.preventDefault();
114
+ key.stopPropagation();
115
+ onClose();
116
+ return;
117
+ }
118
+
119
+ // Navigation with direct scroll calculation
120
+ if (key.name === "up" || key.name === "down") {
121
+ key.preventDefault();
122
+ key.stopPropagation();
123
+
124
+ const newIndex = key.name === "up"
125
+ ? Math.max(0, selectedCmdIndex - 1)
126
+ : Math.min(cmdCount - 1, selectedCmdIndex + 1);
127
+
128
+ // Calculate scroll based on new index
129
+ const newFlatIndex = flatItems.findIndex(
130
+ (i) => i.type === "command" && i.cmdIndex === newIndex
131
+ );
132
+
133
+ const currentScroll = scrollOffsetRef.current;
134
+ let newScrollOffset = currentScroll;
135
+ if (newFlatIndex !== -1) {
136
+ if (newFlatIndex < currentScroll) {
137
+ newScrollOffset = newFlatIndex;
138
+ } else if (newFlatIndex > currentScroll + listHeight - 1) {
139
+ newScrollOffset = newFlatIndex - listHeight + 1;
140
+ }
141
+ }
142
+
143
+ console.log('[CommandPanel] Key press:', {
144
+ key: key.name,
145
+ newIndex,
146
+ newFlatIndex,
147
+ currentScroll,
148
+ listHeight,
149
+ newScrollOffset
150
+ });
151
+
152
+ scrollOffsetRef.current = newScrollOffset;
153
+ setSelectedCmdIndex(newIndex);
154
+ return;
155
+ }
156
+ });
157
+
158
+ const handleContentChange = () => {
159
+ const text = inputRef.current?.editBuffer.getText() ?? "";
160
+ setFilterText(text);
161
+ setSelectedCmdIndex(0);
162
+ scrollOffsetRef.current = 0;
163
+ };
164
+
165
+ const handleSubmit = () => {
166
+ const selectedItem = flatItems.find(
167
+ (i) => i.type === "command" && i.cmdIndex === selectedCmdIndex
168
+ );
169
+ if (selectedItem?.cmd) {
170
+ onClose();
171
+ selectedItem.cmd.action();
172
+ }
173
+ };
174
+
175
+ const keyBindings = useMemo<KeyBinding[]>(
176
+ () => [{ name: "return", action: "submit" }],
177
+ []
178
+ );
179
+
180
+ if (!isActive) return null;
181
+
182
+ const visibleItems = flatItems.slice(effectiveScrollOffset, effectiveScrollOffset + listHeight);
183
+
184
+ return (
185
+ <box
186
+ position="absolute"
187
+ top={0}
188
+ left={0}
189
+ width="100%"
190
+ height="100%"
191
+ backgroundColor="#00000080"
192
+ alignItems="center"
193
+ justifyContent="center"
194
+ >
195
+ <box
196
+ width={panelWidth}
197
+ flexDirection="column"
198
+ borderStyle="rounded"
199
+ borderColor="#4a4a5a"
200
+ backgroundColor="#1a1a2e"
201
+ paddingX={1}
202
+ paddingY={1}
203
+ >
204
+ {/* Filter input */}
205
+ <textarea
206
+ ref={inputRef}
207
+ initialValue=""
208
+ focused={isActive}
209
+ showCursor
210
+ height={1}
211
+ wrapMode="none"
212
+ marginX={1}
213
+ textColor="#f8f8f2"
214
+ backgroundColor="#16213e"
215
+ onContentChange={handleContentChange}
216
+ onSubmit={handleSubmit}
217
+ keyBindings={keyBindings}
218
+ />
219
+
220
+ {/* Separator */}
221
+ <box height={1} marginTop={1}>
222
+ <text fg="#4a4a5a">
223
+ {"─".repeat(panelWidth - 2)}
224
+ </text>
225
+ </box>
226
+
227
+ {/* Command list */}
228
+ <box height={listHeight} flexDirection="column" overflow="hidden">
229
+ {visibleItems.map((item, idx) => {
230
+ const flatIndex = effectiveScrollOffset + idx;
231
+ if (item.type === "header") {
232
+ return (
233
+ <box key={`h-${item.section}-${flatIndex}`} height={1}>
234
+ <text
235
+ fg="#ff79c6"
236
+ attributes={createTextAttributes({ bold: true })}
237
+ >
238
+ {item.section}
239
+ </text>
240
+ </box>
241
+ );
242
+ }
243
+ const isSelected = item.cmdIndex === selectedCmdIndex;
244
+ return (
245
+ <box key={`c-${item.cmd!.id}-${flatIndex}`} height={1} backgroundColor={isSelected ? "#44475a" : undefined} paddingLeft={2}>
246
+ <text fg={isSelected ? "#ff79c6" : "#f8f8f2"}>
247
+ {`${item.cmd!.id} ${item.cmd!.label}`}
248
+ </text>
249
+ </box>
250
+ );
251
+ })}
252
+ {flatItems.length === 0 && (
253
+ <box height={1}>
254
+ <text fg="#6c6c7c">No commands found</text>
255
+ </box>
256
+ )}
257
+ </box>
258
+ </box>
259
+ </box>
260
+ );
261
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Confirm Modal
3
+ *
4
+ * Generic confirmation dialog with customizable title, message, and buttons.
5
+ */
6
+
7
+ import { useState } from "react";
8
+ import { useKeyboard } from "@opentui/react";
9
+ import { createTextAttributes } from "@opentui/core";
10
+ import type { MouseEvent } from "@opentui/core";
11
+
12
+ interface ConfirmModalProps {
13
+ isActive: boolean;
14
+ title: string;
15
+ message: string;
16
+ confirmLabel?: string;
17
+ cancelLabel?: string;
18
+ onConfirm: () => void;
19
+ onCancel: () => void;
20
+ }
21
+
22
+ function Button({
23
+ label,
24
+ fgColor,
25
+ bgColor,
26
+ hoverBgColor,
27
+ isActive,
28
+ onClick,
29
+ }: {
30
+ label: string;
31
+ fgColor: string;
32
+ bgColor: string;
33
+ hoverBgColor: string;
34
+ isActive: boolean;
35
+ onClick: () => void;
36
+ }) {
37
+ const [hover, setHover] = useState(false);
38
+ const currentBg = hover ? hoverBgColor : bgColor;
39
+
40
+ const handleMouseUp = (e: MouseEvent) => {
41
+ if (isActive) {
42
+ e.stopPropagation();
43
+ onClick();
44
+ }
45
+ };
46
+
47
+ const handleMouseOver = () => {
48
+ if (isActive) setHover(true);
49
+ };
50
+
51
+ const handleMouseOut = () => {
52
+ if (isActive) setHover(false);
53
+ };
54
+
55
+ return (
56
+ <box
57
+ paddingX={1}
58
+ backgroundColor={currentBg}
59
+ onMouseUp={handleMouseUp}
60
+ onMouseOver={handleMouseOver}
61
+ onMouseOut={handleMouseOut}
62
+ >
63
+ <text fg={fgColor} attributes={createTextAttributes({ bold: true })}>
64
+ {label}
65
+ </text>
66
+ </box>
67
+ );
68
+ }
69
+
70
+ export function ConfirmModal({
71
+ isActive,
72
+ title,
73
+ message,
74
+ confirmLabel = "Yes",
75
+ cancelLabel = "No",
76
+ onConfirm,
77
+ onCancel,
78
+ }: ConfirmModalProps) {
79
+ useKeyboard((key) => {
80
+ if (!isActive) return;
81
+ if (key.name === "y" || key.name === "return") {
82
+ onConfirm();
83
+ } else if (key.name === "n" || key.name === "escape") {
84
+ onCancel();
85
+ }
86
+ });
87
+
88
+ if (!isActive) return null;
89
+
90
+ return (
91
+ <box
92
+ position="absolute"
93
+ top={0}
94
+ left={0}
95
+ width="100%"
96
+ height="100%"
97
+ backgroundColor="#00000080"
98
+ alignItems="center"
99
+ justifyContent="center"
100
+ >
101
+ <box
102
+ borderStyle="rounded"
103
+ borderColor="#4a4a5a"
104
+ backgroundColor="#1a1a2e"
105
+ paddingX={2}
106
+ paddingY={1}
107
+ flexDirection="column"
108
+ alignItems="center"
109
+ >
110
+ <text attributes={createTextAttributes({ bold: true })} fg="#fb7185">
111
+ {title}
112
+ </text>
113
+ <text>{message}</text>
114
+ <box marginTop={1} flexDirection="row" gap={2}>
115
+ <Button
116
+ label={` ${confirmLabel} `}
117
+ fgColor="white"
118
+ bgColor="#f43f5e"
119
+ hoverBgColor="#fb7185"
120
+ isActive={isActive}
121
+ onClick={onConfirm}
122
+ />
123
+ <Button
124
+ label={` ${cancelLabel} `}
125
+ fgColor="white"
126
+ bgColor="#2dd4bf"
127
+ hoverBgColor="#5eead4"
128
+ isActive={isActive}
129
+ onClick={onCancel}
130
+ />
131
+ </box>
132
+ </box>
133
+ </box>
134
+ );
135
+ }