@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.
- package/LICENSE +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side Bar Component
|
|
3
|
+
*
|
|
4
|
+
* Right sidebar: Logo, session title, working directory, model info,
|
|
5
|
+
* context usage, cost estimate, MCP servers, and task list.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createTextAttributes } from "@opentui/core";
|
|
9
|
+
import { getMcpConnections, getMcpStatusSummary } from "../../services/mcp/index.js";
|
|
10
|
+
|
|
11
|
+
const KOI_LOGO = [
|
|
12
|
+
"██ ███ ███████ ██████",
|
|
13
|
+
"██ ██ ██ ███ ██ ",
|
|
14
|
+
"████ ██ █ ██ ██ ",
|
|
15
|
+
"██ ██ ███ ██ ██ ",
|
|
16
|
+
"██ ███ ███████ ██████",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const VERSION = "v0.1.0";
|
|
20
|
+
|
|
21
|
+
// 水墨风格渐变色:从淡蓝墨到浓墨
|
|
22
|
+
const GRADIENT_STOPS = [
|
|
23
|
+
"#778899", // 淡蓝灰(偏向蓝色)
|
|
24
|
+
"#708090", // 石板灰(主色调)
|
|
25
|
+
"#5a6a7a", // 中墨色
|
|
26
|
+
"#4a5a6a", // 浓墨
|
|
27
|
+
"#3a4a5a", // 最深墨色
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function abbreviatePath(path: string, maxLen: number = 24): string {
|
|
31
|
+
if (path.length <= maxLen) return path;
|
|
32
|
+
if (path === "/" || path === "~") return path;
|
|
33
|
+
|
|
34
|
+
const prefix = path.startsWith("~") ? "~" : "";
|
|
35
|
+
const cleanPath = path.startsWith("~") ? path.slice(1) : path;
|
|
36
|
+
const parts = cleanPath.split("/").filter(Boolean);
|
|
37
|
+
|
|
38
|
+
if (parts.length <= 1) {
|
|
39
|
+
return path.length > maxLen ? path.slice(0, maxLen - 1) + "…" : path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try keeping tail segments intact, drop leading ones
|
|
43
|
+
for (let i = 0; i < parts.length; i++) {
|
|
44
|
+
const tail = parts.slice(i).join("/");
|
|
45
|
+
const candidate = prefix ? `${prefix}/${tail}` : `/${tail}`;
|
|
46
|
+
if (candidate.length <= maxLen) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Even the last segment is too long — truncate it
|
|
52
|
+
const last = parts[parts.length - 1]!;
|
|
53
|
+
const abbreviatedLast =
|
|
54
|
+
last.length > maxLen - 4 ? last.slice(0, maxLen - 4) + "…" : last;
|
|
55
|
+
return prefix ? `${prefix}/…/${abbreviatedLast}` : `/…/${abbreviatedLast}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function FixedWidthText({
|
|
59
|
+
text,
|
|
60
|
+
width,
|
|
61
|
+
fg,
|
|
62
|
+
}: {
|
|
63
|
+
text: string;
|
|
64
|
+
width: number;
|
|
65
|
+
fg?: string;
|
|
66
|
+
}) {
|
|
67
|
+
const display = text.length <= width ? text : text.slice(0, Math.max(0, width - 1)) + "…";
|
|
68
|
+
return (
|
|
69
|
+
<box width={width}>
|
|
70
|
+
<text fg={fg}>{display}</text>
|
|
71
|
+
</box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TASK_STATUS_COLORS: Record<string, string> = {
|
|
76
|
+
pending: "#fbbf24",
|
|
77
|
+
in_progress: "#00d9ff",
|
|
78
|
+
completed: "#00ff99",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const SUBAGENT_STATUS_COLORS: Record<string, string> = {
|
|
82
|
+
running: "#00d9ff",
|
|
83
|
+
completed: "#00ff99",
|
|
84
|
+
failed: "#ff6b6b",
|
|
85
|
+
killed: "#fbbf24",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MONITOR_STATUS_COLORS: Record<string, string> = {
|
|
89
|
+
running: "#00d9ff",
|
|
90
|
+
completed: "#00ff99",
|
|
91
|
+
error: "#ff6b6b",
|
|
92
|
+
killed: "#fbbf24",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const MCP_STATUS_COLORS: Record<string, string> = {
|
|
96
|
+
connected: "#00ff99",
|
|
97
|
+
failed: "#ff6b6b",
|
|
98
|
+
disabled: "#fbbf24",
|
|
99
|
+
disconnected: "#6c6c7c",
|
|
100
|
+
pending: "#00d9ff",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function Divider({
|
|
104
|
+
width,
|
|
105
|
+
char = "─",
|
|
106
|
+
fg: color = "#9aabb8",
|
|
107
|
+
}: {
|
|
108
|
+
width: number;
|
|
109
|
+
char?: string;
|
|
110
|
+
fg?: string;
|
|
111
|
+
}) {
|
|
112
|
+
const pattern = char.repeat(width + 1);
|
|
113
|
+
return (
|
|
114
|
+
<text fg={color} wrapMode="none" truncate={true}>
|
|
115
|
+
{pattern.slice(0, width)}
|
|
116
|
+
</text>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface TaskItem {
|
|
121
|
+
id: string;
|
|
122
|
+
content: string;
|
|
123
|
+
status: "pending" | "in_progress" | "completed";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface SubagentItem {
|
|
127
|
+
id: string;
|
|
128
|
+
description: string;
|
|
129
|
+
status: "running" | "completed" | "failed" | "killed";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface MonitorItem {
|
|
133
|
+
id: string;
|
|
134
|
+
description: string;
|
|
135
|
+
status: "running" | "completed" | "killed" | "error";
|
|
136
|
+
lastOutput?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface SideBarProps {
|
|
140
|
+
width?: number;
|
|
141
|
+
workingDir?: string;
|
|
142
|
+
sessionTitle?: string;
|
|
143
|
+
modelName?: string;
|
|
144
|
+
provider?: string;
|
|
145
|
+
contextUsage?: string;
|
|
146
|
+
tokenCount?: string;
|
|
147
|
+
cost?: string;
|
|
148
|
+
tasks?: TaskItem[];
|
|
149
|
+
subagents?: SubagentItem[];
|
|
150
|
+
monitors?: MonitorItem[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function SideBar({
|
|
154
|
+
width = 28,
|
|
155
|
+
workingDir = "/",
|
|
156
|
+
sessionTitle = "New Session",
|
|
157
|
+
modelName = "Not configured",
|
|
158
|
+
provider = "Use /model to select",
|
|
159
|
+
contextUsage = "0%",
|
|
160
|
+
tokenCount = "(0)",
|
|
161
|
+
cost = "$0.00",
|
|
162
|
+
tasks = [],
|
|
163
|
+
subagents = [],
|
|
164
|
+
monitors = [],
|
|
165
|
+
}: SideBarProps) {
|
|
166
|
+
const usableWidth = Math.max(1, width - 1);
|
|
167
|
+
|
|
168
|
+
const visibleTasks = tasks.slice(0, 12);
|
|
169
|
+
const hasMoreTasks = tasks.length > visibleTasks.length;
|
|
170
|
+
|
|
171
|
+
const visibleSubagents = subagents.slice(0, 8);
|
|
172
|
+
const hasMoreSubagents = subagents.length > visibleSubagents.length;
|
|
173
|
+
|
|
174
|
+
const visibleMonitors = monitors.slice(0, 8);
|
|
175
|
+
const hasMoreMonitors = monitors.length > visibleMonitors.length;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<box width={width} flexDirection="column" paddingLeft={1}>
|
|
179
|
+
{/* Top spacer */}
|
|
180
|
+
<text> </text>
|
|
181
|
+
|
|
182
|
+
{/* Row 0: Meowdream (left) + version (right) */}
|
|
183
|
+
<box width={usableWidth} flexDirection="row" justifyContent="space-between">
|
|
184
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">Meowdream</text>
|
|
185
|
+
<text fg="#7a8a9a">{VERSION}</text>
|
|
186
|
+
</box>
|
|
187
|
+
|
|
188
|
+
{/* Spacer between header and logo */}
|
|
189
|
+
<text> </text>
|
|
190
|
+
|
|
191
|
+
{/* Divider above logo */}
|
|
192
|
+
<Divider width={usableWidth} />
|
|
193
|
+
<Divider width={usableWidth} char="·" fg="#c5cdd5" />
|
|
194
|
+
|
|
195
|
+
{/* Rows 1-5: KOI ASCII logo with gradient */}
|
|
196
|
+
{KOI_LOGO.map((line, i) => {
|
|
197
|
+
const color = GRADIENT_STOPS[Math.min(i, GRADIENT_STOPS.length - 1)];
|
|
198
|
+
return (
|
|
199
|
+
<text key={i} fg={color} wrapMode="none" truncate={true}>
|
|
200
|
+
{line.slice(0, usableWidth)}
|
|
201
|
+
</text>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
|
|
205
|
+
{/* Divider below logo */}
|
|
206
|
+
<Divider width={usableWidth} char="·" fg="#c5cdd5" />
|
|
207
|
+
<Divider width={usableWidth} />
|
|
208
|
+
|
|
209
|
+
{/* Spacer */}
|
|
210
|
+
<text> </text>
|
|
211
|
+
|
|
212
|
+
{/* Session title */}
|
|
213
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">{sessionTitle}</text>
|
|
214
|
+
|
|
215
|
+
{/* Spacer between session title and directory */}
|
|
216
|
+
<text> </text>
|
|
217
|
+
|
|
218
|
+
{/* Working directory */}
|
|
219
|
+
<text fg="#8a9aaa">{abbreviatePath(workingDir, usableWidth)}</text>
|
|
220
|
+
|
|
221
|
+
{/* Empty row */}
|
|
222
|
+
<text> </text>
|
|
223
|
+
|
|
224
|
+
{/* Model name */}
|
|
225
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">{modelName}</text>
|
|
226
|
+
|
|
227
|
+
{/* Provider */}
|
|
228
|
+
<text fg="#8a9aaa">{provider}</text>
|
|
229
|
+
|
|
230
|
+
{/* Context usage + cost */}
|
|
231
|
+
<text fg="#8a9aaa">{`${contextUsage} ${tokenCount} ${cost}`}</text>
|
|
232
|
+
|
|
233
|
+
{/* MCP Servers section - get live data */}
|
|
234
|
+
{(() => {
|
|
235
|
+
const mcpSummary = getMcpStatusSummary();
|
|
236
|
+
const mcpConnections = getMcpConnections();
|
|
237
|
+
if (mcpSummary.total === 0) return null;
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
<text> </text>
|
|
241
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">
|
|
242
|
+
MCP ({mcpSummary.connected}/{mcpSummary.total})
|
|
243
|
+
</text>
|
|
244
|
+
{Array.from(mcpConnections.entries()).map(([name, connection]) => {
|
|
245
|
+
const color = MCP_STATUS_COLORS[connection.status] ?? "#6c6c7c";
|
|
246
|
+
return (
|
|
247
|
+
<box key={name} flexDirection="row" gap={1}>
|
|
248
|
+
<text fg={color}>●</text>
|
|
249
|
+
<FixedWidthText
|
|
250
|
+
text={name}
|
|
251
|
+
width={Math.max(1, usableWidth - 4)}
|
|
252
|
+
fg="#8a9aaa"
|
|
253
|
+
/>
|
|
254
|
+
</box>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</>
|
|
258
|
+
);
|
|
259
|
+
})()}
|
|
260
|
+
|
|
261
|
+
{/* Subagents section */}
|
|
262
|
+
{visibleSubagents.length > 0 && (
|
|
263
|
+
<>
|
|
264
|
+
<text> </text>
|
|
265
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">
|
|
266
|
+
Subagents ({subagents.length})
|
|
267
|
+
</text>
|
|
268
|
+
{visibleSubagents.map((sa) => {
|
|
269
|
+
const color = SUBAGENT_STATUS_COLORS[sa.status] ?? "#fbbf24";
|
|
270
|
+
return (
|
|
271
|
+
<box key={sa.id} flexDirection="row" gap={1}>
|
|
272
|
+
<text fg={color}>●</text>
|
|
273
|
+
<FixedWidthText
|
|
274
|
+
text={sa.description}
|
|
275
|
+
width={Math.max(1, usableWidth - 4)}
|
|
276
|
+
fg="#8a9aaa"
|
|
277
|
+
/>
|
|
278
|
+
</box>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
{hasMoreSubagents && (
|
|
282
|
+
<text fg="#9aa5b0">
|
|
283
|
+
{`… and ${subagents.length - visibleSubagents.length} more`}
|
|
284
|
+
</text>
|
|
285
|
+
)}
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Tasks section */}
|
|
290
|
+
{visibleTasks.length > 0 && (
|
|
291
|
+
<>
|
|
292
|
+
<text> </text>
|
|
293
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">
|
|
294
|
+
Tasks ({tasks.length})
|
|
295
|
+
</text>
|
|
296
|
+
{visibleTasks.map((task) => {
|
|
297
|
+
const color = TASK_STATUS_COLORS[task.status] ?? "#fbbf24";
|
|
298
|
+
return (
|
|
299
|
+
<box key={task.id} flexDirection="row" gap={1}>
|
|
300
|
+
<text fg={color}>●</text>
|
|
301
|
+
<FixedWidthText
|
|
302
|
+
text={task.content}
|
|
303
|
+
width={Math.max(1, usableWidth - 4)}
|
|
304
|
+
fg="#8a9aaa"
|
|
305
|
+
/>
|
|
306
|
+
</box>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
{hasMoreTasks && (
|
|
310
|
+
<text fg="#9aa5b0">
|
|
311
|
+
{`… and ${tasks.length - visibleTasks.length} more`}
|
|
312
|
+
</text>
|
|
313
|
+
)}
|
|
314
|
+
</>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* Monitors section */}
|
|
318
|
+
{visibleMonitors.length > 0 && (
|
|
319
|
+
<>
|
|
320
|
+
<text> </text>
|
|
321
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#5a6a7a">
|
|
322
|
+
Monitors ({monitors.length})
|
|
323
|
+
</text>
|
|
324
|
+
{visibleMonitors.map((mon) => {
|
|
325
|
+
const color = MONITOR_STATUS_COLORS[mon.status] ?? "#fbbf24";
|
|
326
|
+
const displayText = mon.lastOutput
|
|
327
|
+
? `${mon.description}: ${mon.lastOutput.slice(0, 20)}`
|
|
328
|
+
: mon.description;
|
|
329
|
+
return (
|
|
330
|
+
<box key={mon.id} flexDirection="row" gap={1}>
|
|
331
|
+
<text fg={color}>●</text>
|
|
332
|
+
<FixedWidthText
|
|
333
|
+
text={displayText}
|
|
334
|
+
width={Math.max(1, usableWidth - 4)}
|
|
335
|
+
fg="#8a9aaa"
|
|
336
|
+
/>
|
|
337
|
+
</box>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
{hasMoreMonitors && (
|
|
341
|
+
<text fg="#9aa5b0">
|
|
342
|
+
{`… and ${monitors.length - visibleMonitors.length} more`}
|
|
343
|
+
</text>
|
|
344
|
+
)}
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
</box>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Prompt History Hook
|
|
3
|
+
*
|
|
4
|
+
* Global, session-independent history for user-sent messages.
|
|
5
|
+
* Filters out internal notifications (Monitor, background agent messages).
|
|
6
|
+
* Maximum 100 entries. Persisted to ~/.config/koi/prompt-history.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
|
|
13
|
+
const MAX_HISTORY_SIZE = 100;
|
|
14
|
+
const HISTORY_FILE = path.join(os.homedir(), ".config", "koi", "prompt-history.json");
|
|
15
|
+
|
|
16
|
+
// Global shared history - persists across all sessions
|
|
17
|
+
let userPromptHistory: string[] = [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the directory for storing history file.
|
|
21
|
+
*/
|
|
22
|
+
function getHistoryDir(): string {
|
|
23
|
+
return path.dirname(HISTORY_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load history from disk.
|
|
28
|
+
*/
|
|
29
|
+
function loadHistory(): string[] {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(HISTORY_FILE)) {
|
|
32
|
+
const data = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
33
|
+
const parsed = JSON.parse(data) as unknown;
|
|
34
|
+
if (Array.isArray(parsed)) {
|
|
35
|
+
return parsed.slice(0, MAX_HISTORY_SIZE) as string[];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
// Ignore errors, return empty array
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Save history to disk.
|
|
46
|
+
*/
|
|
47
|
+
function saveHistory(history: string[]): void {
|
|
48
|
+
try {
|
|
49
|
+
const dir = getHistoryDir();
|
|
50
|
+
if (!fs.existsSync(dir)) {
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), "utf-8");
|
|
54
|
+
} catch (_err) {
|
|
55
|
+
// Ignore write errors
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load history on module initialization
|
|
60
|
+
userPromptHistory = loadHistory();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a message is an internal notification from Monitor or background agent.
|
|
64
|
+
* These should not be added to user prompt history.
|
|
65
|
+
*/
|
|
66
|
+
export function isInternalUserMessage(text: string): boolean {
|
|
67
|
+
const trimmed = text.trimStart();
|
|
68
|
+
return (
|
|
69
|
+
trimmed.startsWith("<task-notification>") ||
|
|
70
|
+
trimmed.startsWith("<monitor-notification>")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add a message to the user prompt history.
|
|
76
|
+
* Only adds if not an internal notification.
|
|
77
|
+
* Maintains maximum size of 100 entries.
|
|
78
|
+
*/
|
|
79
|
+
export function addToUserHistory(text: string): void {
|
|
80
|
+
if (isInternalUserMessage(text)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Avoid duplicates: if the same message is at the top, don't add it again
|
|
85
|
+
if (userPromptHistory.length > 0 && userPromptHistory[0] === text) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add to the beginning (most recent first)
|
|
90
|
+
userPromptHistory.unshift(text);
|
|
91
|
+
|
|
92
|
+
// Trim to max size
|
|
93
|
+
if (userPromptHistory.length > MAX_HISTORY_SIZE) {
|
|
94
|
+
userPromptHistory = userPromptHistory.slice(0, MAX_HISTORY_SIZE);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Persist to disk
|
|
98
|
+
saveHistory(userPromptHistory);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the entire user prompt history.
|
|
103
|
+
*/
|
|
104
|
+
export function getUserHistory(): readonly string[] {
|
|
105
|
+
return userPromptHistory;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear all user prompt history.
|
|
110
|
+
*/
|
|
111
|
+
export function clearUserHistory(): void {
|
|
112
|
+
userPromptHistory = [];
|
|
113
|
+
saveHistory([]);
|
|
114
|
+
}
|
package/src/tui/theme.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Theme Configuration
|
|
3
|
+
*
|
|
4
|
+
* Color tokens, border styles, and semantic mappings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
// ─── Base Colors ───
|
|
10
|
+
|
|
11
|
+
export const borderColor = chalk.hex("#4a4a5a");
|
|
12
|
+
export const dimText = chalk.hex("#6c6c7c");
|
|
13
|
+
export const highlightText = chalk.hex("#ff79c6");
|
|
14
|
+
export const agentPrefixColor = chalk.hex("#ff79c6").bold;
|
|
15
|
+
|
|
16
|
+
// ─── 水墨风渐变 ───
|
|
17
|
+
// 从淡墨到浓墨的渐变色
|
|
18
|
+
|
|
19
|
+
const gradientStops = [
|
|
20
|
+
"#8fbc8f", // 淡石绿(远山)
|
|
21
|
+
"#708090", // 石板灰(主色调)
|
|
22
|
+
"#5a6a7a", // 中墨色
|
|
23
|
+
"#4a5a6a", // 浓墨
|
|
24
|
+
"#3a4a5a", // 最深墨色
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function gradientPinkPurple(text: string, rowIndex: number, totalRows: number): string {
|
|
28
|
+
if (totalRows <= 1) return chalk.hex(gradientStops[0]!)(text);
|
|
29
|
+
const t = rowIndex / (totalRows - 1);
|
|
30
|
+
const idx = Math.min(Math.floor(t * (gradientStops.length - 1)), gradientStops.length - 2);
|
|
31
|
+
const localT = t * (gradientStops.length - 1) - idx;
|
|
32
|
+
const stop1 = gradientStops[idx]!;
|
|
33
|
+
const stop2 = gradientStops[idx + 1]!;
|
|
34
|
+
const c1 = hexToRgb(stop1);
|
|
35
|
+
const c2 = hexToRgb(stop2);
|
|
36
|
+
const r = Math.round(c1.r + (c2.r - c1.r) * localT);
|
|
37
|
+
const g = Math.round(c1.g + (c2.g - c1.g) * localT);
|
|
38
|
+
const b = Math.round(c1.b + (c2.b - c1.b) * localT);
|
|
39
|
+
return chalk.rgb(r, g, b)(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
43
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
44
|
+
return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Sidebar 水墨风配色 ───
|
|
48
|
+
|
|
49
|
+
export function sidebarTitle(text: string): string {
|
|
50
|
+
return chalk.hex("#5a6a7a").bold(text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sidebarVersion(text: string): string {
|
|
54
|
+
return chalk.hex("#7a8a9a")(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sidebarModelName(text: string): string {
|
|
58
|
+
return chalk.hex("#5a6a7a")(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function sidebarDim(text: string): string {
|
|
62
|
+
return chalk.hex("#8a9aaa")(text);
|
|
63
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command System Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the command interface for Koi's command palette.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Command context passed to command actions
|
|
9
|
+
*/
|
|
10
|
+
export interface CommandContext {
|
|
11
|
+
cwd: string;
|
|
12
|
+
session: unknown;
|
|
13
|
+
onOpenSkillsModal?: () => void;
|
|
14
|
+
onOpenMCPSettings?: () => void;
|
|
15
|
+
onSwitchSession?: () => void;
|
|
16
|
+
onNewSession?: () => void;
|
|
17
|
+
onOpenModelModal?: () => void;
|
|
18
|
+
onOpenConnectModal?: () => void;
|
|
19
|
+
onFork?: () => void;
|
|
20
|
+
onCompact?: () => void;
|
|
21
|
+
onRename?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Command action result
|
|
26
|
+
*/
|
|
27
|
+
export interface CommandResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
message?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Command definition
|
|
35
|
+
*/
|
|
36
|
+
export interface Command {
|
|
37
|
+
/** Unique command identifier */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Display name */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Description shown in command palette */
|
|
42
|
+
description: string;
|
|
43
|
+
/** Search keywords */
|
|
44
|
+
keywords?: string[];
|
|
45
|
+
/** Command action */
|
|
46
|
+
action: (context: CommandContext) => Promise<CommandResult> | CommandResult;
|
|
47
|
+
/** Whether the command requires an active session */
|
|
48
|
+
requiresSession?: boolean;
|
|
49
|
+
/** Whether to show in command palette */
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Command group for organization
|
|
55
|
+
*/
|
|
56
|
+
export interface CommandGroup {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
commands: Command[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Built-in command IDs
|
|
64
|
+
*/
|
|
65
|
+
export const BUILTIN_COMMANDS = {
|
|
66
|
+
SKILLS: "skills",
|
|
67
|
+
NEW_SESSION: "new-session",
|
|
68
|
+
FORK: "fork",
|
|
69
|
+
SESSIONS: "sessions",
|
|
70
|
+
COMPACT: "compact",
|
|
71
|
+
RENAME: "rename",
|
|
72
|
+
CONNECT: "connect",
|
|
73
|
+
MODEL: "model",
|
|
74
|
+
MCP: "mcp",
|
|
75
|
+
YOLO: "yolo",
|
|
76
|
+
MODE: "mode",
|
|
77
|
+
PLAN: "plan",
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
export type BuiltinCommandId = (typeof BUILTIN_COMMANDS)[keyof typeof BUILTIN_COMMANDS];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare module "cross-spawn" {
|
|
2
|
+
import type { ChildProcess } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
interface SpawnOptions {
|
|
5
|
+
cwd?: string | URL;
|
|
6
|
+
env?: Record<string, unknown>;
|
|
7
|
+
argv0?: string;
|
|
8
|
+
stdio?: "pipe" | "ignore" | "inherit" | Array<"pipe" | "ignore" | "inherit" | "ipc" | number>;
|
|
9
|
+
shell?: boolean | string;
|
|
10
|
+
windowsHide?: boolean;
|
|
11
|
+
windowsVerbatimArguments?: boolean;
|
|
12
|
+
detached?: boolean;
|
|
13
|
+
uid?: number;
|
|
14
|
+
gid?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SpawnReturns extends ChildProcess {
|
|
18
|
+
pid: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function spawn(command: string, args?: string[], options?: SpawnOptions): SpawnReturns;
|
|
22
|
+
|
|
23
|
+
export = spawn;
|
|
24
|
+
}
|