ei-tui 0.1.3 โ 0.1.4
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/README.md +31 -34
- package/package.json +5 -1
- package/src/README.md +85 -1
- package/src/cli/README.md +29 -21
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +91 -158
- package/src/core/orchestrators/ceremony.ts +1 -1
- package/src/core/processor.ts +172 -45
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/queue.ts +1 -10
- package/src/core/state-manager.ts +1 -7
- package/src/core/types.ts +4 -5
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +117 -690
- package/src/prompts/heartbeat/ei.ts +61 -133
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/index.ts +2 -5
- package/tui/README.md +77 -3
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/PromptInput.tsx +111 -29
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/context/ei.tsx +10 -0
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +12 -4
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -1,51 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Ei's heartbeat is special - it considers not just engagement gaps but also
|
|
5
|
-
* inactive personas and cross-system health. Ei is the "system guide" and
|
|
6
|
-
* should prompt the user about neglected relationships.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { EiHeartbeatPromptData, PromptOutput } from "./types.js";
|
|
10
|
-
import type { Message, Topic, Person } from "../../core/types.js";
|
|
1
|
+
import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
|
|
2
|
+
import type { Message } from "../../core/types.js";
|
|
11
3
|
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
12
4
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
5
|
+
function formatItem(item: EiHeartbeatItem): string {
|
|
6
|
+
switch (item.type) {
|
|
7
|
+
case "Fact Check":
|
|
8
|
+
return [
|
|
9
|
+
`- **${item.id}** Fact Check: "${item.name}" โ ${item.description}`,
|
|
10
|
+
item.quote ? ` Quote: "${item.quote}"` : "",
|
|
11
|
+
].filter(Boolean).join("\n");
|
|
12
|
+
|
|
13
|
+
case "Low-Engagement Person":
|
|
14
|
+
return [
|
|
15
|
+
`- **${item.id}** Low-Engagement Person: ${item.name} (${item.relationship}, gap: ${item.engagement_delta})`,
|
|
16
|
+
` ${item.description}`,
|
|
17
|
+
item.quote ? ` Quote: "${item.quote}"` : "",
|
|
18
|
+
].filter(Boolean).join("\n");
|
|
19
|
+
|
|
20
|
+
case "Low-Engagement Topic":
|
|
21
|
+
return [
|
|
22
|
+
`- **${item.id}** Low-Engagement Topic: ${item.name} (gap: ${item.engagement_delta})`,
|
|
23
|
+
` ${item.description}`,
|
|
24
|
+
item.quote ? ` Quote: "${item.quote}"` : "",
|
|
25
|
+
].filter(Boolean).join("\n");
|
|
26
|
+
|
|
27
|
+
case "Inactive Persona": {
|
|
28
|
+
const desc = item.short_description ? ` โ ${item.short_description}` : "";
|
|
29
|
+
return `- **${item.id}** Inactive Persona: ${item.name}${desc} (${item.days_inactive} days inactive)`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
function countTrailingPersonaMessages(history: Message[]): number {
|
|
39
|
-
if (history.length === 0) return 0;
|
|
40
|
-
|
|
41
35
|
let count = 0;
|
|
42
36
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
count++;
|
|
46
|
-
} else {
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
37
|
+
if (history[i].role === "system") count++;
|
|
38
|
+
else break;
|
|
49
39
|
}
|
|
50
40
|
return count;
|
|
51
41
|
}
|
|
@@ -54,119 +44,57 @@ function getLastPersonaMessage(history: Message[]): Message | undefined {
|
|
|
54
44
|
return history.filter(m => m.role === "system").slice(-1)[0];
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
function formatInactivePersonas(personas: EiHeartbeatPromptData["inactive_personas"]): string {
|
|
58
|
-
if (personas.length === 0) return "(All personas have been active recently)";
|
|
59
|
-
|
|
60
|
-
return personas
|
|
61
|
-
.map(p => {
|
|
62
|
-
const desc = p.short_description ? ` - ${p.short_description}` : "";
|
|
63
|
-
return `- **${p.name}**${desc}: ${p.days_inactive} days inactive`;
|
|
64
|
-
})
|
|
65
|
-
.join('\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Build Ei heartbeat prompts.
|
|
70
|
-
*
|
|
71
|
-
* Ei sees ALL data and has special responsibilities:
|
|
72
|
-
* - System health monitoring
|
|
73
|
-
* - Gentle nudges about neglected relationships
|
|
74
|
-
* - Encouraging human-to-human connection
|
|
75
|
-
*/
|
|
76
47
|
export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutput {
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
const itemsSection = data.items.length === 0
|
|
49
|
+
? "(Nothing requires attention right now)"
|
|
50
|
+
: data.items.map(formatItem).join("\n\n");
|
|
51
|
+
|
|
52
|
+
const system = `You are Ei, the user's personal companion and system guide.
|
|
79
53
|
|
|
80
|
-
You are NOT having a conversation right now
|
|
54
|
+
You are NOT having a conversation right now โ you are deciding IF and WHAT to discuss with your human friend.
|
|
81
55
|
|
|
82
56
|
Your unique role:
|
|
83
57
|
- You see ALL of the human's data across all groups
|
|
84
58
|
- You help them reflect on their life and relationships
|
|
85
59
|
- You gently encourage human-to-human connection
|
|
86
|
-
- You care about their overall wellbeing, not just being helpful
|
|
87
|
-
|
|
88
|
-
const systemHealthFragment = `## System Health
|
|
89
|
-
|
|
90
|
-
### Pending Validations
|
|
91
|
-
${data.pending_validations > 0
|
|
92
|
-
? `There are **${data.pending_validations}** items from other personas that need your review.`
|
|
93
|
-
: "No pending validations."}
|
|
94
|
-
|
|
95
|
-
### Inactive Personas
|
|
96
|
-
${formatInactivePersonas(data.inactive_personas)}`;
|
|
60
|
+
- You care about their overall wellbeing, not just being helpful
|
|
97
61
|
|
|
98
|
-
|
|
62
|
+
## Items That May Need Attention
|
|
99
63
|
|
|
100
|
-
|
|
101
|
-
These are topics they want to talk about more:
|
|
64
|
+
Each item has an ID in brackets. Pick at most ONE to address.
|
|
102
65
|
|
|
103
|
-
${
|
|
66
|
+
${itemsSection}
|
|
104
67
|
|
|
105
|
-
|
|
106
|
-
These are relationships they might want to nurture:
|
|
68
|
+
## How to Respond to Each Type
|
|
107
69
|
|
|
108
|
-
|
|
70
|
+
- **Fact Check**: Do NOT write your own message. Set should_respond=true and provide the id. The system will generate an appropriate canned notification for the user. Leave my_response empty.
|
|
71
|
+
- **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
|
|
72
|
+
- **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
|
|
109
73
|
|
|
110
|
-
|
|
74
|
+
## When NOT to Reach Out
|
|
111
75
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
2. **Human connections** - Encourage real-world relationships over AI dependency
|
|
115
|
-
3. **Reflection** - Help them think, don't do their thinking for them
|
|
116
|
-
4. **System health** - Mention inactive personas or pending validations if relevant
|
|
117
|
-
|
|
118
|
-
### When to Reach Out
|
|
119
|
-
- A significant topic has been neglected and you can help them process it
|
|
120
|
-
- They might benefit from connecting with someone (real person or persona)
|
|
121
|
-
- You have a genuine observation or question
|
|
122
|
-
- Pending validations need attention
|
|
123
|
-
|
|
124
|
-
### When NOT to Reach Out
|
|
125
|
-
- Recent conversation ended with natural closure
|
|
126
|
-
- Nothing meaningful to add
|
|
76
|
+
- Nothing in the list feels meaningful right now
|
|
77
|
+
- You've already sent unanswered messages (see below)
|
|
127
78
|
- It would feel like nagging
|
|
128
|
-
- They seem to need space
|
|
129
|
-
|
|
130
|
-
### Tone
|
|
131
|
-
- Warm but not saccharine
|
|
132
|
-
- Curious but not intrusive
|
|
133
|
-
- Supportive but honest
|
|
134
|
-
- A good friend, not a therapist`;
|
|
135
79
|
|
|
136
|
-
|
|
80
|
+
## Response Format
|
|
137
81
|
|
|
138
|
-
|
|
82
|
+
Pick ONE item (or none):
|
|
139
83
|
|
|
140
84
|
\`\`\`json
|
|
141
85
|
{
|
|
142
86
|
"should_respond": true,
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
{ "type": "persona", "name": "Adventure Guide", "reason": "inactive for 5 days" },
|
|
146
|
-
{ "type": "person", "name": "Mom", "reason": "they mentioned wanting to call her" }
|
|
147
|
-
],
|
|
148
|
-
"message": "Hey! I noticed we haven't talked about work lately - how's that project going?"
|
|
87
|
+
"id": "the-item-id-you-chose",
|
|
88
|
+
"my_response": "Hey, how's your mom doing? You mentioned wanting to call her."
|
|
149
89
|
}
|
|
150
90
|
\`\`\`
|
|
151
91
|
|
|
152
|
-
|
|
92
|
+
Or if nothing warrants reaching out:
|
|
153
93
|
\`\`\`json
|
|
154
94
|
{
|
|
155
95
|
"should_respond": false
|
|
156
96
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Note: The "priorities" list helps you organize your thoughts. Your message should naturally address the top priority without feeling like a checklist.`;
|
|
160
|
-
|
|
161
|
-
const system = `${roleFragment}
|
|
162
|
-
|
|
163
|
-
${systemHealthFragment}
|
|
164
|
-
|
|
165
|
-
${humanDataFragment}
|
|
166
|
-
|
|
167
|
-
${guidelinesFragment}
|
|
168
|
-
|
|
169
|
-
${outputFragment}`;
|
|
97
|
+
\`\`\``;
|
|
170
98
|
|
|
171
99
|
const historySection = `## Recent Conversation History
|
|
172
100
|
|
|
@@ -174,20 +102,20 @@ ${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
|
|
|
174
102
|
|
|
175
103
|
const consecutiveMessages = countTrailingPersonaMessages(data.recent_history);
|
|
176
104
|
const lastEiMsg = getLastPersonaMessage(data.recent_history);
|
|
177
|
-
|
|
178
|
-
let unansweredWarning =
|
|
105
|
+
|
|
106
|
+
let unansweredWarning = "";
|
|
179
107
|
if (lastEiMsg && consecutiveMessages >= 1) {
|
|
180
|
-
const preview = lastEiMsg.content.length > 100
|
|
181
|
-
? lastEiMsg.content.substring(0, 100) + "..."
|
|
108
|
+
const preview = lastEiMsg.content.length > 100
|
|
109
|
+
? lastEiMsg.content.substring(0, 100) + "..."
|
|
182
110
|
: lastEiMsg.content;
|
|
183
|
-
|
|
111
|
+
|
|
184
112
|
unansweredWarning = `
|
|
185
113
|
### CRITICAL: You Already Reached Out
|
|
186
114
|
|
|
187
115
|
Your last message was: "${preview}"
|
|
188
116
|
|
|
189
117
|
The human has NOT responded. DO NOT repeat or rephrase this message.
|
|
190
|
-
If you reach out now, it MUST be about something COMPLETELY DIFFERENT
|
|
118
|
+
If you reach out now, it MUST be about something COMPLETELY DIFFERENT โ or say nothing.`;
|
|
191
119
|
|
|
192
120
|
if (consecutiveMessages >= 2) {
|
|
193
121
|
unansweredWarning += `
|
|
@@ -200,7 +128,7 @@ If you reach out now, it MUST be about something COMPLETELY DIFFERENT - or say n
|
|
|
200
128
|
${unansweredWarning}
|
|
201
129
|
---
|
|
202
130
|
|
|
203
|
-
Based on all the context above, decide: Should you reach out to your human friend right now? If so,
|
|
131
|
+
Based on all the context above, decide: Should you reach out to your human friend right now? If so, which item above is most worth addressing?
|
|
204
132
|
|
|
205
133
|
Remember: You're their thoughtful companion, not their productivity assistant.`;
|
|
206
134
|
|
|
@@ -39,32 +39,62 @@ export interface HeartbeatCheckResult {
|
|
|
39
39
|
message?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// EI HEARTBEAT TYPES
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A single item Ei can choose to address.
|
|
48
|
+
* One of: an unverified fact, an under-engaged person, an under-engaged topic,
|
|
49
|
+
* or an inactive persona.
|
|
50
|
+
*/
|
|
51
|
+
export type EiHeartbeatItem =
|
|
52
|
+
| {
|
|
53
|
+
id: string;
|
|
54
|
+
type: "Fact Check";
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
quote?: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
id: string;
|
|
61
|
+
type: "Low-Engagement Person";
|
|
62
|
+
engagement_delta: string; // e.g. "25%"
|
|
63
|
+
relationship: string;
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
quote?: string;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
id: string;
|
|
70
|
+
type: "Low-Engagement Topic";
|
|
71
|
+
engagement_delta: string; // e.g. "28%"
|
|
72
|
+
name: string;
|
|
73
|
+
description: string;
|
|
74
|
+
quote?: string;
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
id: string;
|
|
78
|
+
type: "Inactive Persona";
|
|
79
|
+
name: string;
|
|
80
|
+
short_description?: string;
|
|
81
|
+
days_inactive: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
42
84
|
/**
|
|
43
85
|
* Data contract for buildEiHeartbeatPrompt
|
|
44
86
|
*/
|
|
45
87
|
export interface EiHeartbeatPromptData {
|
|
46
|
-
|
|
47
|
-
topics: Topic[]; // All topics with gaps
|
|
48
|
-
people: Person[]; // All people with gaps
|
|
49
|
-
};
|
|
50
|
-
inactive_personas: Array<{
|
|
51
|
-
name: string;
|
|
52
|
-
short_description?: string;
|
|
53
|
-
days_inactive: number;
|
|
54
|
-
}>;
|
|
55
|
-
pending_validations: number; // Count of items needing Ei review
|
|
88
|
+
items: EiHeartbeatItem[];
|
|
56
89
|
recent_history: Message[];
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
/**
|
|
60
|
-
* Expected LLM response from Ei heartbeat
|
|
93
|
+
* Expected LLM response from Ei heartbeat.
|
|
94
|
+
* Ei picks exactly ONE item by id and optionally writes a message.
|
|
61
95
|
*/
|
|
62
96
|
export interface EiHeartbeatResult {
|
|
63
97
|
should_respond: boolean;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: string;
|
|
67
|
-
reason: string;
|
|
68
|
-
}>;
|
|
69
|
-
message?: string;
|
|
98
|
+
id?: string; // ID of the chosen item (required if should_respond is true)
|
|
99
|
+
my_response?: string; // Only used for Person/Topic/Persona items (not Fact Check)
|
|
70
100
|
}
|
package/src/prompts/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type {
|
|
|
10
10
|
HeartbeatCheckResult,
|
|
11
11
|
EiHeartbeatPromptData,
|
|
12
12
|
EiHeartbeatResult,
|
|
13
|
+
EiHeartbeatItem,
|
|
13
14
|
} from "./heartbeat/types.js";
|
|
14
15
|
|
|
15
16
|
export {
|
|
@@ -41,11 +42,7 @@ export type {
|
|
|
41
42
|
TraitResult,
|
|
42
43
|
} from "./persona/types.js";
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
export type {
|
|
46
|
-
EiValidationPromptData,
|
|
47
|
-
EiValidationResult,
|
|
48
|
-
} from "./validation/types.js";
|
|
45
|
+
|
|
49
46
|
|
|
50
47
|
export {
|
|
51
48
|
buildHumanFactScanPrompt,
|
package/tui/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Terminal User Interface (TUI)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ei TUI is built with OpenTUI and SolidJS.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
OpenCode integration: import via `/settings` (`opencode.integration: true`) ยท export via [CLI](../src/cli/README.md)
|
|
6
6
|
|
|
7
7
|
# Installation
|
|
8
8
|
|
|
@@ -12,7 +12,81 @@ npm install -g ei-tui
|
|
|
12
12
|
|
|
13
13
|
## TUI Commands
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
All commands start with `/`. Append `!` to any command as a shorthand for `--force` (e.g., `/quit!`).
|
|
16
|
+
|
|
17
|
+
### Navigation & App
|
|
18
|
+
|
|
19
|
+
| Command | Aliases | Description |
|
|
20
|
+
|---------|---------|-------------|
|
|
21
|
+
| `/help` | `/h` | Show the command list and keybindings |
|
|
22
|
+
| `/quit` | `/q` | Save, sync, and exit |
|
|
23
|
+
| `/quit!` | `/q!` | Force quit without syncing |
|
|
24
|
+
|
|
25
|
+
### Personas
|
|
26
|
+
|
|
27
|
+
| Command | Aliases | Description |
|
|
28
|
+
|---------|---------|-------------|
|
|
29
|
+
| `/persona` | `/p` | Open persona picker overlay |
|
|
30
|
+
| `/persona <name>` | `/p <name>` | Switch to a persona by name or alias |
|
|
31
|
+
| `/persona new <name>` | `/p new <name>` | Create a new persona (opens `$EDITOR`) |
|
|
32
|
+
| `/details` | `/d` | Edit the current persona in `$EDITOR` |
|
|
33
|
+
| `/details <name>` | `/d <name>` | Edit a specific persona in `$EDITOR` |
|
|
34
|
+
| `/archive` | | List archived personas (Enter to unarchive) |
|
|
35
|
+
| `/archive <name>` | | Archive a persona by name |
|
|
36
|
+
| `/unarchive <name>` | | Unarchive a persona and switch to it |
|
|
37
|
+
| `/delete` | `/del` | Pick a persona to permanently delete |
|
|
38
|
+
| `/delete <name>` | `/del <name>` | Permanently delete a persona by name (confirms) |
|
|
39
|
+
| `/pause` | | Pause current persona indefinitely |
|
|
40
|
+
| `/pause <duration>` | | Pause for a duration: `2h`, `1d`, `1w`, `30m` |
|
|
41
|
+
| `/resume` | `/unpause` | Resume the current paused persona |
|
|
42
|
+
| `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
|
|
43
|
+
|
|
44
|
+
### Providers & Models
|
|
45
|
+
|
|
46
|
+
| Command | Aliases | Description |
|
|
47
|
+
|---------|---------|-------------|
|
|
48
|
+
| `/provider` | `/providers` | Open provider picker (select, edit, or create) |
|
|
49
|
+
| `/provider <name>` | | Set a provider on the active persona by name |
|
|
50
|
+
| `/provider new` | | Create a new LLM provider (opens `$EDITOR`) |
|
|
51
|
+
| `/model <model>` | | Set model for active persona (e.g., `sonnet-latest`) |
|
|
52
|
+
| `/model <provider:model>` | | Set provider + model explicitly (e.g., `openai:gpt-4o`) |
|
|
53
|
+
|
|
54
|
+
### Messages & Context
|
|
55
|
+
|
|
56
|
+
| Command | Aliases | Description |
|
|
57
|
+
|---------|---------|-------------|
|
|
58
|
+
| `/new` | | Toggle context boundary (fresh conversation start) |
|
|
59
|
+
| `/context` | `/messages` | Edit message context status in `$EDITOR` |
|
|
60
|
+
| `/quotes` | `/quote` | Open all quotes in `$EDITOR` |
|
|
61
|
+
| `/quotes me` | | Open only your (human) quotes |
|
|
62
|
+
| `/quotes <N>` | | View/edit quotes attached to message number N |
|
|
63
|
+
| `/quotes search "term"` | | Search quotes by keyword |
|
|
64
|
+
| `/quotes <persona>` | | View/edit quotes attributed to a specific persona |
|
|
65
|
+
|
|
66
|
+
### Data & Settings
|
|
67
|
+
|
|
68
|
+
| Command | Aliases | Description |
|
|
69
|
+
|---------|---------|-------------|
|
|
70
|
+
| `/me` | | Edit all your data (facts, traits, topics, people) in `$EDITOR` |
|
|
71
|
+
| `/me <type>` | | Edit one type: `facts`, `traits`, `topics`, or `people` |
|
|
72
|
+
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
73
|
+
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
|
74
|
+
|
|
75
|
+
### Editor
|
|
76
|
+
|
|
77
|
+
| Command | Aliases | Description |
|
|
78
|
+
|---------|---------|-------------|
|
|
79
|
+
| `/editor` | `/e`, `/edit` | Open current input text in `$EDITOR`, update on save |
|
|
80
|
+
|
|
81
|
+
### Keybindings
|
|
82
|
+
|
|
83
|
+
| Key | Action |
|
|
84
|
+
|-----|--------|
|
|
85
|
+
| `Escape` | Abort current operation / resume queue |
|
|
86
|
+
| `Ctrl+C` | Clear input (second press exits) |
|
|
87
|
+
| `Ctrl+B` | Toggle sidebar |
|
|
88
|
+
| `Ctrl+E` | Open `$EDITOR` (preserves current input) |
|
|
89
|
+
| `PageUp / PageDown` | Scroll message history |
|
|
16
90
|
|
|
17
91
|
# Development
|
|
18
92
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createMemo, For } from "solid-js";
|
|
2
|
+
import { getAllCommands } from "../commands/registry";
|
|
3
|
+
|
|
4
|
+
interface CommandSuggestProps {
|
|
5
|
+
input: () => string;
|
|
6
|
+
highlightIndex: () => number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CommandSuggest(props: CommandSuggestProps) {
|
|
10
|
+
const matches = createMemo(() => {
|
|
11
|
+
const raw = props.input().trim();
|
|
12
|
+
if (!raw.startsWith("/")) return [];
|
|
13
|
+
const query = raw.slice(1).split(/\s/)[0].replace(/!$/, "").toLowerCase();
|
|
14
|
+
return getAllCommands().filter(
|
|
15
|
+
(cmd) =>
|
|
16
|
+
cmd.name.startsWith(query) ||
|
|
17
|
+
cmd.aliases.some((a) => a.startsWith(query))
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<box
|
|
23
|
+
flexDirection="column"
|
|
24
|
+
visible={matches().length > 0}
|
|
25
|
+
borderStyle="single"
|
|
26
|
+
border={true}
|
|
27
|
+
borderColor="#586e75"
|
|
28
|
+
backgroundColor="#1a1a2e"
|
|
29
|
+
flexShrink={0}
|
|
30
|
+
>
|
|
31
|
+
<For each={matches()}>
|
|
32
|
+
{(cmd, i) => {
|
|
33
|
+
const isHighlighted = () => i() === props.highlightIndex();
|
|
34
|
+
const aliases =
|
|
35
|
+
cmd.aliases.length > 0 ? ` (/${cmd.aliases.join(", /")})` : "";
|
|
36
|
+
return (
|
|
37
|
+
<box
|
|
38
|
+
paddingX={1}
|
|
39
|
+
backgroundColor={isHighlighted() ? "#2d3748" : "transparent"}
|
|
40
|
+
>
|
|
41
|
+
<text fg={isHighlighted() ? "#eee8d5" : "#839496"} truncate>
|
|
42
|
+
{`/${cmd.name}${aliases} ${cmd.description}`}
|
|
43
|
+
</text>
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
46
|
+
}}
|
|
47
|
+
</For>
|
|
48
|
+
</box>
|
|
49
|
+
);
|
|
50
|
+
}
|