ei-tui 0.1.3 → 0.1.5
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 +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -29,17 +29,29 @@ ${description}`;
|
|
|
29
29
|
// =============================================================================
|
|
30
30
|
|
|
31
31
|
export function buildGuidelinesSection(personaName: string): string {
|
|
32
|
-
const
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
32
|
+
const silenceGuidance = [
|
|
33
|
+
'- **Silence is a valid response** - real friends don\'t always have something to say. Stay silent when:',
|
|
34
|
+
' - You sent the last message and the human is just acknowledging it ("good night", "thanks", "lol", "ok")',
|
|
35
|
+
' - The human is venting or in the middle of a thought - sometimes a nod is all that\'s needed',
|
|
36
|
+
' - The question or statement is rhetorical - not everything deserves an answer',
|
|
37
|
+
' - Responding would feel forced, performative, or hollow',
|
|
38
|
+
].join('\n');
|
|
39
|
+
|
|
40
|
+
const universal = [
|
|
41
|
+
'## Guidelines',
|
|
42
|
+
'- Be genuine, not sycophantic - a good friend disagrees when appropriate',
|
|
43
|
+
'- Match conversational energy - brief replies for brief messages',
|
|
44
|
+
'- You\'re a friend with your own interests, not just a helper',
|
|
45
|
+
silenceGuidance,
|
|
46
|
+
].join('\n');
|
|
47
|
+
|
|
48
|
+
if (personaName.toLowerCase() === 'ei') {
|
|
49
|
+
return [
|
|
50
|
+
universal,
|
|
51
|
+
'- Encourage human-to-human connection when appropriate',
|
|
52
|
+
'- Be transparent about being an AI when relevant',
|
|
53
|
+
'- Gently challenge self-limiting beliefs - growth over comfort',
|
|
54
|
+
].join('\n');
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
return universal;
|
|
@@ -280,7 +292,7 @@ export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["h
|
|
|
280
292
|
|
|
281
293
|
return `## Memorable Moments
|
|
282
294
|
|
|
283
|
-
These are quotes the human found worth preserving:
|
|
295
|
+
These are quotes the human or the system found worth preserving:
|
|
284
296
|
|
|
285
297
|
${formatted}`;
|
|
286
298
|
}
|
|
@@ -353,3 +365,44 @@ The human can view and edit all of this by ${seeHumanDataAction}.
|
|
|
353
365
|
- If they want you to remember something specific, tell them about the quote capture feature (${viewQuotesAction})
|
|
354
366
|
- Pausing the system (Escape) immediately stops AI processing but preserves messages`;
|
|
355
367
|
}
|
|
368
|
+
|
|
369
|
+
// =============================================================================
|
|
370
|
+
// RESPONSE FORMAT SECTION
|
|
371
|
+
// =============================================================================
|
|
372
|
+
|
|
373
|
+
export function buildResponseFormatSection(): string {
|
|
374
|
+
const jsonResponding = [
|
|
375
|
+
'{',
|
|
376
|
+
' "should_respond": true,',
|
|
377
|
+
' "verbal_response": "What you would say out loud",',
|
|
378
|
+
' "action_response": "What you would do (rendered in italics, like stage directions)"',
|
|
379
|
+
'}'
|
|
380
|
+
].join('\n');
|
|
381
|
+
|
|
382
|
+
const jsonSilent = [
|
|
383
|
+
'{',
|
|
384
|
+
' "should_respond": false,',
|
|
385
|
+
' "reason": "Brief explanation of why silence is the right choice here"',
|
|
386
|
+
'}'
|
|
387
|
+
].join('\n');
|
|
388
|
+
|
|
389
|
+
return `## Response Format
|
|
390
|
+
|
|
391
|
+
Always respond with JSON in this exact format:
|
|
392
|
+
|
|
393
|
+
\`\`\`json
|
|
394
|
+
${jsonResponding}
|
|
395
|
+
\`\`\`
|
|
396
|
+
|
|
397
|
+
Or, if staying silent:
|
|
398
|
+
|
|
399
|
+
\`\`\`json
|
|
400
|
+
${jsonSilent}
|
|
401
|
+
\`\`\`
|
|
402
|
+
|
|
403
|
+
Rules:
|
|
404
|
+
- \`verbal_response\` and \`action_response\` are both optional - include whichever applies
|
|
405
|
+
- \`reason\` is only used when \`should_respond\` is false
|
|
406
|
+
- Do NOT include \`<thinking>\` blocks or analysis outside the JSON
|
|
407
|
+
- The JSON must be valid - use double quotes, no trailing commas`;
|
|
408
|
+
}
|
|
@@ -29,6 +29,16 @@ export interface ResponsePromptData {
|
|
|
29
29
|
isTUI: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Structured response from LLM (new JSON schema)
|
|
34
|
+
*/
|
|
35
|
+
export interface PersonaResponseResult {
|
|
36
|
+
should_respond: boolean;
|
|
37
|
+
verbal_response?: string;
|
|
38
|
+
action_response?: string;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Prompt output structure (all prompts return this)
|
|
34
44
|
*/
|
package/tui/README.md
CHANGED
|
@@ -1,18 +1,103 @@
|
|
|
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
|
|
|
9
|
-
```
|
|
9
|
+
```bash
|
|
10
|
+
# Install Bun (if you don't have it)
|
|
11
|
+
curl -fsSL https://bun.sh/install | bash
|
|
12
|
+
|
|
13
|
+
# Install Ei
|
|
10
14
|
npm install -g ei-tui
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
## TUI Commands
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
All commands start with `/`. Append `!` to any command as a shorthand for `--force` (e.g., `/quit!`).
|
|
20
|
+
|
|
21
|
+
### Navigation & App
|
|
22
|
+
|
|
23
|
+
| Command | Aliases | Description |
|
|
24
|
+
|---------|---------|-------------|
|
|
25
|
+
| `/help` | `/h` | Show the command list and keybindings |
|
|
26
|
+
| `/quit` | `/q` | Save, sync, and exit |
|
|
27
|
+
| `/quit!` | `/q!` | Force quit without syncing |
|
|
28
|
+
|
|
29
|
+
### Personas
|
|
30
|
+
|
|
31
|
+
| Command | Aliases | Description |
|
|
32
|
+
|---------|---------|-------------|
|
|
33
|
+
| `/persona` | `/p` | Open persona picker overlay |
|
|
34
|
+
| `/persona <name>` | `/p <name>` | Switch to a persona by name or alias |
|
|
35
|
+
| `/persona new <name>` | `/p new <name>` | Create a new persona (opens `$EDITOR`) |
|
|
36
|
+
| `/details` | `/d` | Edit the current persona in `$EDITOR` |
|
|
37
|
+
| `/details <name>` | `/d <name>` | Edit a specific persona in `$EDITOR` |
|
|
38
|
+
| `/archive` | | List archived personas (Enter to unarchive) |
|
|
39
|
+
| `/archive <name>` | | Archive a persona by name |
|
|
40
|
+
| `/unarchive <name>` | | Unarchive a persona and switch to it |
|
|
41
|
+
| `/delete` | `/del` | Pick a persona to permanently delete |
|
|
42
|
+
| `/delete <name>` | `/del <name>` | Permanently delete a persona by name (confirms) |
|
|
43
|
+
| `/pause` | | Pause current persona indefinitely |
|
|
44
|
+
| `/pause <duration>` | | Pause for a duration: `2h`, `1d`, `1w`, `30m` |
|
|
45
|
+
| `/resume` | `/unpause` | Resume the current paused persona |
|
|
46
|
+
| `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
|
|
47
|
+
|
|
48
|
+
### Providers & Models
|
|
49
|
+
|
|
50
|
+
| Command | Aliases | Description |
|
|
51
|
+
|---------|---------|-------------|
|
|
52
|
+
| `/provider` | `/providers` | Open provider picker (select, edit, or create) |
|
|
53
|
+
| `/provider <name>` | | Set a provider on the active persona by name |
|
|
54
|
+
| `/provider new` | | Create a new LLM provider (opens `$EDITOR`) |
|
|
55
|
+
| `/model <model>` | | Set model for active persona (e.g., `sonnet-latest`) |
|
|
56
|
+
| `/model <provider:model>` | | Set provider + model explicitly (e.g., `openai:gpt-4o`) |
|
|
57
|
+
|
|
58
|
+
### Messages & Context
|
|
59
|
+
|
|
60
|
+
| Command | Aliases | Description |
|
|
61
|
+
|---------|---------|-------------|
|
|
62
|
+
| `/new` | | Toggle context boundary (fresh conversation start) |
|
|
63
|
+
| `/context` | `/messages` | Edit message context status in `$EDITOR` |
|
|
64
|
+
| `/quotes` | `/quote` | Open all quotes in `$EDITOR` |
|
|
65
|
+
| `/quotes me` | | Open only your (human) quotes |
|
|
66
|
+
| `/quotes <N>` | | View/edit quotes attached to message number N |
|
|
67
|
+
| `/quotes search "term"` | | Search quotes by keyword |
|
|
68
|
+
| `/quotes <persona>` | | View/edit quotes attributed to a specific persona |
|
|
69
|
+
|
|
70
|
+
### Data & Settings
|
|
71
|
+
|
|
72
|
+
| Command | Aliases | Description |
|
|
73
|
+
|---------|---------|-------------|
|
|
74
|
+
| `/me` | | Edit all your data (facts, traits, topics, people) in `$EDITOR` |
|
|
75
|
+
| `/me <type>` | | Edit one type: `facts`, `traits`, `topics`, or `people` |
|
|
76
|
+
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
77
|
+
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
|
78
|
+
|
|
79
|
+
### Editor
|
|
80
|
+
|
|
81
|
+
| Command | Aliases | Description |
|
|
82
|
+
|---------|---------|-------------|
|
|
83
|
+
| `/editor` | `/e`, `/edit` | Open current input text in `$EDITOR`, update on save |
|
|
84
|
+
|
|
85
|
+
### Queue & Debugging
|
|
86
|
+
|
|
87
|
+
| Command | Aliases | Description |
|
|
88
|
+
|---------|---------|-------------|
|
|
89
|
+
| `/queue` | | Pause queue and inspect/edit active items in `$EDITOR` |
|
|
90
|
+
| `/dlq` | | Inspect and recover failed (dead-letter) queue items in `$EDITOR` |
|
|
91
|
+
|
|
92
|
+
### Keybindings
|
|
93
|
+
|
|
94
|
+
| Key | Action |
|
|
95
|
+
|-----|--------|
|
|
96
|
+
| `Escape` | Abort current operation / resume queue |
|
|
97
|
+
| `Ctrl+C` | Clear input (second press exits) |
|
|
98
|
+
| `Ctrl+B` | Toggle sidebar |
|
|
99
|
+
| `Ctrl+E` | Open `$EDITOR` (preserves current input) |
|
|
100
|
+
| `PageUp / PageDown` | Scroll message history |
|
|
16
101
|
|
|
17
102
|
# Development
|
|
18
103
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
import { queueItemsToYAML, queueItemsFromYAML } from "../util/yaml-serializers.js";
|
|
4
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
5
|
+
|
|
6
|
+
export const dlqCommand: Command = {
|
|
7
|
+
name: "dlq",
|
|
8
|
+
aliases: [],
|
|
9
|
+
description: "Open dead-letter queue $EDITOR",
|
|
10
|
+
usage: "/dlq - Inspect and recover failed queue items",
|
|
11
|
+
|
|
12
|
+
async execute(_args, ctx) {
|
|
13
|
+
const items = ctx.ei.getDLQItems();
|
|
14
|
+
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
ctx.showNotification("DLQ is empty", "info");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let yamlContent = queueItemsToYAML(items);
|
|
21
|
+
|
|
22
|
+
while (true) {
|
|
23
|
+
const result = await spawnEditor({
|
|
24
|
+
initialContent: yamlContent,
|
|
25
|
+
filename: "ei-dlq.yaml",
|
|
26
|
+
renderer: ctx.renderer,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result.aborted || !result.success) {
|
|
30
|
+
ctx.showNotification("DLQ edit cancelled", "info");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.content === null) {
|
|
35
|
+
ctx.showNotification("No changes made", "info");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const updates = queueItemsFromYAML(result.content);
|
|
41
|
+
let recovered = 0;
|
|
42
|
+
for (const update of updates) {
|
|
43
|
+
await ctx.ei.updateQueueItem(update.id, update);
|
|
44
|
+
if (update.state === "pending") recovered++;
|
|
45
|
+
}
|
|
46
|
+
const msg = recovered > 0
|
|
47
|
+
? `DLQ updated — ${recovered} item(s) requeued`
|
|
48
|
+
: `DLQ updated (no items requeued)`;
|
|
49
|
+
ctx.showNotification(msg, "info");
|
|
50
|
+
return;
|
|
51
|
+
} catch (parseError) {
|
|
52
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
53
|
+
|
|
54
|
+
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
55
|
+
ctx.showOverlay((hideOverlay) =>
|
|
56
|
+
ConfirmOverlay({
|
|
57
|
+
message: `YAML error:\n${errorMsg}\n\nRe-edit?`,
|
|
58
|
+
onConfirm: () => { hideOverlay(); resolve(true); },
|
|
59
|
+
onCancel: () => { hideOverlay(); resolve(false); },
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (shouldReEdit) {
|
|
65
|
+
yamlContent = result.content;
|
|
66
|
+
await new Promise(r => setTimeout(r, 50));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ctx.showNotification("Changes discarded", "warn");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
import { queueItemsToYAML, queueItemsFromYAML } from "../util/yaml-serializers.js";
|
|
4
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
5
|
+
|
|
6
|
+
export const queueCommand: Command = {
|
|
7
|
+
name: "queue",
|
|
8
|
+
aliases: [],
|
|
9
|
+
description: "Pause & open queue items in $EDITOR",
|
|
10
|
+
usage: "/queue - Inspect and edit active queue items",
|
|
11
|
+
|
|
12
|
+
async execute(_args, ctx) {
|
|
13
|
+
const items = ctx.ei.getQueueActiveItems();
|
|
14
|
+
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
ctx.showNotification("Queue is empty", "info");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ctx.ei.pauseQueue();
|
|
21
|
+
ctx.showNotification(`Queue paused (${items.length} items)`, "info");
|
|
22
|
+
|
|
23
|
+
let yamlContent = queueItemsToYAML(items);
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
const result = await spawnEditor({
|
|
27
|
+
initialContent: yamlContent,
|
|
28
|
+
filename: "ei-queue.yaml",
|
|
29
|
+
renderer: ctx.renderer,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (result.aborted || !result.success) {
|
|
33
|
+
ctx.ei.resumeQueue();
|
|
34
|
+
ctx.showNotification("Queue resumed (no changes)", "info");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (result.content === null) {
|
|
39
|
+
ctx.ei.resumeQueue();
|
|
40
|
+
ctx.showNotification("No changes — queue resumed", "info");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const updates = queueItemsFromYAML(result.content);
|
|
46
|
+
for (const update of updates) {
|
|
47
|
+
await ctx.ei.updateQueueItem(update.id, update);
|
|
48
|
+
}
|
|
49
|
+
ctx.ei.resumeQueue();
|
|
50
|
+
ctx.showNotification(`Queue updated (${updates.length} items) — resumed`, "info");
|
|
51
|
+
return;
|
|
52
|
+
} catch (parseError) {
|
|
53
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
54
|
+
|
|
55
|
+
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
56
|
+
ctx.showOverlay((hideOverlay) =>
|
|
57
|
+
ConfirmOverlay({
|
|
58
|
+
message: `YAML error:\n${errorMsg}\n\nRe-edit?`,
|
|
59
|
+
onConfirm: () => { hideOverlay(); resolve(true); },
|
|
60
|
+
onCancel: () => { hideOverlay(); resolve(false); },
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (shouldReEdit) {
|
|
66
|
+
yamlContent = result.content;
|
|
67
|
+
await new Promise(r => setTimeout(r, 50));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ctx.ei.resumeQueue();
|
|
72
|
+
ctx.showNotification("Changes discarded — queue resumed", "warn");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -18,6 +18,16 @@ function formatTime(timestamp: string): string {
|
|
|
18
18
|
return `${hours}:${minutes}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function buildMessageText(message: Message): string {
|
|
22
|
+
if (message.silence_reason !== undefined) {
|
|
23
|
+
return `[chose not to respond: ${message.silence_reason}]`;
|
|
24
|
+
}
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
27
|
+
if (message.verbal_response) parts.push(message.verbal_response);
|
|
28
|
+
return parts.join('\n\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
function insertQuoteMarkers(content: string, quotes: Quote[]): string {
|
|
22
32
|
const validQuotes = quotes
|
|
23
33
|
.filter(q => q.end !== null && q.end !== undefined)
|
|
@@ -30,7 +40,7 @@ function insertQuoteMarkers(content: string, quotes: Quote[]): string {
|
|
|
30
40
|
while (insertPos > 0 && (result[insertPos - 1] === '\n' || result[insertPos - 1] === ' ')) {
|
|
31
41
|
insertPos--;
|
|
32
42
|
}
|
|
33
|
-
result = result.slice(0, insertPos) + "
|
|
43
|
+
result = result.slice(0, insertPos) + "\u207a" + result.slice(insertPos);
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
return result;
|
|
@@ -118,7 +128,7 @@ export function MessageList() {
|
|
|
118
128
|
|
|
119
129
|
const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`;
|
|
120
130
|
|
|
121
|
-
const displayContent = insertQuoteMarkers(message
|
|
131
|
+
const displayContent = insertQuoteMarkers(buildMessageText(message), message._quotes);
|
|
122
132
|
|
|
123
133
|
const showDivider = () => {
|
|
124
134
|
const boundary = activeContextBoundary();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createEffect, createSignal } from "solid-js";
|
|
2
|
+
import { getAllCommands } from "../commands/registry";
|
|
1
3
|
import type { TextareaRenderable, KeyBinding } from "@opentui/core";
|
|
2
4
|
import { useEi } from "../context/ei";
|
|
3
5
|
import { useKeyboardNav } from "../context/keyboard";
|
|
@@ -19,7 +21,12 @@ import { deleteCommand } from "../commands/delete";
|
|
|
19
21
|
import { quotesCommand } from "../commands/quotes";
|
|
20
22
|
import { providerCommand } from "../commands/provider";
|
|
21
23
|
import { setSyncCommand } from "../commands/setsync";
|
|
24
|
+
import { queueCommand } from "../commands/queue";
|
|
25
|
+
import { dlqCommand } from "../commands/dlq";
|
|
22
26
|
import { useOverlay } from "../context/overlay";
|
|
27
|
+
import { CommandSuggest } from "./CommandSuggest";
|
|
28
|
+
import { useKeyboard } from "@opentui/solid";
|
|
29
|
+
import type { KeyEvent } from "@opentui/core";
|
|
23
30
|
|
|
24
31
|
const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
25
32
|
{ name: "return", action: "submit" },
|
|
@@ -30,7 +37,7 @@ const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
|
30
37
|
export function PromptInput() {
|
|
31
38
|
const ei = useEi();
|
|
32
39
|
const { sendMessage, activePersonaId, stopProcessor, showNotification } = ei;
|
|
33
|
-
const { registerTextarea, registerEditorHandler, exitApp, renderer } = useKeyboardNav();
|
|
40
|
+
const { registerTextarea, registerEditorHandler, exitApp, renderer, resetHistoryIndex } = useKeyboardNav();
|
|
34
41
|
const { showOverlay, hideOverlay, overlayRenderer } = useOverlay();
|
|
35
42
|
|
|
36
43
|
registerCommand(helpCommand);
|
|
@@ -51,9 +58,70 @@ export function PromptInput() {
|
|
|
51
58
|
registerCommand(setSyncCommand);
|
|
52
59
|
registerCommand(contextCommand);
|
|
53
60
|
registerCommand(deleteCommand);
|
|
61
|
+
registerCommand(queueCommand);
|
|
62
|
+
registerCommand(dlqCommand);
|
|
54
63
|
|
|
55
64
|
let textareaRef: TextareaRenderable | undefined;
|
|
56
65
|
|
|
66
|
+
const [inputText, setInputText] = createSignal("");
|
|
67
|
+
const [suggestIndex, setSuggestIndex] = createSignal(0);
|
|
68
|
+
|
|
69
|
+
const suggestMatches = () => {
|
|
70
|
+
const raw = inputText().trim();
|
|
71
|
+
if (!raw.startsWith("/")) return [];
|
|
72
|
+
const query = raw.slice(1).split(/\s/)[0].replace(/!$/, "").toLowerCase();
|
|
73
|
+
return getAllCommands().filter(
|
|
74
|
+
(cmd) =>
|
|
75
|
+
cmd.name.startsWith(query) ||
|
|
76
|
+
cmd.aliases.some((a) => a.startsWith(query))
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const suggestVisible = () => suggestMatches().length > 0 && !overlayRenderer();
|
|
81
|
+
|
|
82
|
+
createEffect(() => {
|
|
83
|
+
inputText();
|
|
84
|
+
setSuggestIndex(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
createEffect(() => {
|
|
88
|
+
activePersonaId();
|
|
89
|
+
resetHistoryIndex();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
useKeyboard((event: KeyEvent) => {
|
|
93
|
+
if (!suggestVisible()) return;
|
|
94
|
+
|
|
95
|
+
if (event.name === "up") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
setSuggestIndex(i => Math.max(0, i - 1));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (event.name === "down") {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
setSuggestIndex(i => Math.min(suggestMatches().length - 1, i + 1));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.name === "tab" || event.name === "right") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
const match = suggestMatches()[suggestIndex()];
|
|
108
|
+
if (match) {
|
|
109
|
+
textareaRef?.setText(`/${match.name} `);
|
|
110
|
+
setInputText(`/${match.name} `);
|
|
111
|
+
textareaRef?.gotoBufferEnd();
|
|
112
|
+
setSuggestIndex(0);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (event.name === "escape") {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
textareaRef?.clear();
|
|
119
|
+
setInputText("");
|
|
120
|
+
setSuggestIndex(0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
57
125
|
const getCommandContext = (): CommandContext => ({
|
|
58
126
|
showOverlay,
|
|
59
127
|
hideOverlay,
|
|
@@ -62,21 +130,24 @@ export function PromptInput() {
|
|
|
62
130
|
stopProcessor,
|
|
63
131
|
ei,
|
|
64
132
|
renderer,
|
|
65
|
-
setInputText: (text: string) =>
|
|
133
|
+
setInputText: (text: string) => {
|
|
134
|
+
textareaRef?.setText(text);
|
|
135
|
+
setInputText(text);
|
|
136
|
+
},
|
|
66
137
|
getInputText: () => textareaRef?.plainText || "",
|
|
67
138
|
});
|
|
68
139
|
|
|
69
140
|
const handleSubmit = async () => {
|
|
70
141
|
const text = textareaRef?.plainText?.trim();
|
|
71
142
|
if (!text) return;
|
|
72
|
-
|
|
143
|
+
|
|
73
144
|
if (text.startsWith("/")) {
|
|
74
145
|
const isEditorCmd = text.startsWith("/editor") ||
|
|
75
146
|
text.startsWith("/edit") ||
|
|
76
147
|
text.startsWith("/e ") ||
|
|
77
148
|
text === "/e";
|
|
78
|
-
const opensEditorForData = text.startsWith("/me") ||
|
|
79
|
-
text.startsWith("/details") ||
|
|
149
|
+
const opensEditorForData = text.startsWith("/me") ||
|
|
150
|
+
text.startsWith("/details") ||
|
|
80
151
|
text.startsWith("/d ") ||
|
|
81
152
|
text === "/d" ||
|
|
82
153
|
text.startsWith("/settings") ||
|
|
@@ -86,19 +157,27 @@ export function PromptInput() {
|
|
|
86
157
|
text.startsWith("/quotes") ||
|
|
87
158
|
text.startsWith("/q ") ||
|
|
88
159
|
text.startsWith("/context") ||
|
|
89
|
-
text.startsWith("/messages")
|
|
90
|
-
|
|
160
|
+
text.startsWith("/messages") ||
|
|
161
|
+
text === "/queue" ||
|
|
162
|
+
text === "/dlq";
|
|
163
|
+
|
|
91
164
|
if (!isEditorCmd && !opensEditorForData) {
|
|
92
165
|
textareaRef?.clear();
|
|
166
|
+
setInputText("");
|
|
93
167
|
}
|
|
94
168
|
await parseAndExecute(text, getCommandContext());
|
|
95
169
|
if (opensEditorForData) {
|
|
96
170
|
textareaRef?.clear();
|
|
171
|
+
setInputText("");
|
|
97
172
|
}
|
|
173
|
+
setSuggestIndex(0);
|
|
98
174
|
return;
|
|
99
175
|
}
|
|
100
|
-
|
|
176
|
+
|
|
101
177
|
textareaRef?.clear();
|
|
178
|
+
setInputText("");
|
|
179
|
+
resetHistoryIndex();
|
|
180
|
+
setSuggestIndex(0);
|
|
102
181
|
if (!activePersonaId()) return;
|
|
103
182
|
await sendMessage(text);
|
|
104
183
|
};
|
|
@@ -115,31 +194,40 @@ export function PromptInput() {
|
|
|
115
194
|
};
|
|
116
195
|
|
|
117
196
|
return (
|
|
118
|
-
<box
|
|
197
|
+
<box
|
|
198
|
+
flexDirection="column"
|
|
119
199
|
flexShrink={0}
|
|
120
|
-
border={["top"]}
|
|
121
|
-
borderStyle="single"
|
|
122
|
-
backgroundColor="#0f3460"
|
|
123
|
-
paddingLeft={1}
|
|
124
|
-
paddingRight={1}
|
|
125
|
-
paddingTop={0.5}
|
|
126
|
-
paddingBottom={0.5}
|
|
127
200
|
>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
registerTextarea(r);
|
|
132
|
-
}}
|
|
133
|
-
focused={!overlayRenderer()}
|
|
134
|
-
onSubmit={() => void handleSubmit()}
|
|
135
|
-
placeholder={getPlaceholder()}
|
|
136
|
-
textColor="#eee8d5"
|
|
137
|
-
backgroundColor="#0f3460"
|
|
138
|
-
cursorColor="#eee8d5"
|
|
139
|
-
minHeight={1}
|
|
140
|
-
maxHeight={6}
|
|
141
|
-
keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
|
|
201
|
+
<CommandSuggest
|
|
202
|
+
input={inputText}
|
|
203
|
+
highlightIndex={suggestIndex}
|
|
142
204
|
/>
|
|
205
|
+
<box
|
|
206
|
+
border={["top"]}
|
|
207
|
+
borderStyle="single"
|
|
208
|
+
backgroundColor="#0f3460"
|
|
209
|
+
paddingLeft={1}
|
|
210
|
+
paddingRight={1}
|
|
211
|
+
paddingTop={0.5}
|
|
212
|
+
paddingBottom={0.5}
|
|
213
|
+
>
|
|
214
|
+
<textarea
|
|
215
|
+
ref={(r: TextareaRenderable) => {
|
|
216
|
+
textareaRef = r;
|
|
217
|
+
registerTextarea(r);
|
|
218
|
+
}}
|
|
219
|
+
focused={!overlayRenderer()}
|
|
220
|
+
onSubmit={() => void handleSubmit()}
|
|
221
|
+
onContentChange={() => setInputText(textareaRef?.plainText ?? "")}
|
|
222
|
+
placeholder={getPlaceholder()}
|
|
223
|
+
textColor="#eee8d5"
|
|
224
|
+
backgroundColor="#0f3460"
|
|
225
|
+
cursorColor="#eee8d5"
|
|
226
|
+
minHeight={1}
|
|
227
|
+
maxHeight={6}
|
|
228
|
+
keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
|
|
229
|
+
/>
|
|
230
|
+
</box>
|
|
143
231
|
</box>
|
|
144
232
|
);
|
|
145
233
|
}
|