ei-tui 0.1.3

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/package.json +63 -0
  4. package/src/README.md +96 -0
  5. package/src/cli/README.md +47 -0
  6. package/src/cli/commands/facts.ts +25 -0
  7. package/src/cli/commands/people.ts +25 -0
  8. package/src/cli/commands/quotes.ts +19 -0
  9. package/src/cli/commands/topics.ts +25 -0
  10. package/src/cli/commands/traits.ts +25 -0
  11. package/src/cli/retrieval.ts +269 -0
  12. package/src/cli.ts +176 -0
  13. package/src/core/AGENTS.md +104 -0
  14. package/src/core/embedding-service.ts +241 -0
  15. package/src/core/handlers/index.ts +1057 -0
  16. package/src/core/index.ts +4 -0
  17. package/src/core/llm-client.ts +265 -0
  18. package/src/core/model-context-windows.ts +49 -0
  19. package/src/core/orchestrators/ceremony.ts +500 -0
  20. package/src/core/orchestrators/extraction-chunker.ts +138 -0
  21. package/src/core/orchestrators/human-extraction.ts +457 -0
  22. package/src/core/orchestrators/index.ts +28 -0
  23. package/src/core/orchestrators/persona-generation.ts +76 -0
  24. package/src/core/orchestrators/persona-topics.ts +117 -0
  25. package/src/core/personas/index.ts +5 -0
  26. package/src/core/personas/opencode-agent.ts +81 -0
  27. package/src/core/processor.ts +1413 -0
  28. package/src/core/queue-processor.ts +197 -0
  29. package/src/core/state/checkpoints.ts +68 -0
  30. package/src/core/state/human.ts +176 -0
  31. package/src/core/state/index.ts +5 -0
  32. package/src/core/state/personas.ts +217 -0
  33. package/src/core/state/queue.ts +144 -0
  34. package/src/core/state-manager.ts +347 -0
  35. package/src/core/types.ts +421 -0
  36. package/src/core/utils/decay.ts +33 -0
  37. package/src/index.ts +1 -0
  38. package/src/integrations/opencode/importer.ts +896 -0
  39. package/src/integrations/opencode/index.ts +16 -0
  40. package/src/integrations/opencode/json-reader.ts +304 -0
  41. package/src/integrations/opencode/reader-factory.ts +35 -0
  42. package/src/integrations/opencode/sqlite-reader.ts +189 -0
  43. package/src/integrations/opencode/types.ts +244 -0
  44. package/src/prompts/AGENTS.md +62 -0
  45. package/src/prompts/ceremony/description-check.ts +47 -0
  46. package/src/prompts/ceremony/expire.ts +30 -0
  47. package/src/prompts/ceremony/explore.ts +60 -0
  48. package/src/prompts/ceremony/index.ts +11 -0
  49. package/src/prompts/ceremony/types.ts +42 -0
  50. package/src/prompts/generation/descriptions.ts +91 -0
  51. package/src/prompts/generation/index.ts +15 -0
  52. package/src/prompts/generation/persona.ts +155 -0
  53. package/src/prompts/generation/seeds.ts +31 -0
  54. package/src/prompts/generation/types.ts +47 -0
  55. package/src/prompts/heartbeat/check.ts +179 -0
  56. package/src/prompts/heartbeat/ei.ts +208 -0
  57. package/src/prompts/heartbeat/index.ts +15 -0
  58. package/src/prompts/heartbeat/types.ts +70 -0
  59. package/src/prompts/human/fact-scan.ts +152 -0
  60. package/src/prompts/human/index.ts +32 -0
  61. package/src/prompts/human/item-match.ts +74 -0
  62. package/src/prompts/human/item-update.ts +322 -0
  63. package/src/prompts/human/person-scan.ts +115 -0
  64. package/src/prompts/human/topic-scan.ts +135 -0
  65. package/src/prompts/human/trait-scan.ts +115 -0
  66. package/src/prompts/human/types.ts +127 -0
  67. package/src/prompts/index.ts +90 -0
  68. package/src/prompts/message-utils.ts +39 -0
  69. package/src/prompts/persona/index.ts +16 -0
  70. package/src/prompts/persona/topics-match.ts +69 -0
  71. package/src/prompts/persona/topics-scan.ts +98 -0
  72. package/src/prompts/persona/topics-update.ts +157 -0
  73. package/src/prompts/persona/traits.ts +117 -0
  74. package/src/prompts/persona/types.ts +74 -0
  75. package/src/prompts/response/index.ts +147 -0
  76. package/src/prompts/response/sections.ts +355 -0
  77. package/src/prompts/response/types.ts +38 -0
  78. package/src/prompts/validation/ei.ts +93 -0
  79. package/src/prompts/validation/index.ts +6 -0
  80. package/src/prompts/validation/types.ts +22 -0
  81. package/src/storage/crypto.ts +96 -0
  82. package/src/storage/index.ts +5 -0
  83. package/src/storage/interface.ts +9 -0
  84. package/src/storage/local.ts +79 -0
  85. package/src/storage/merge.ts +69 -0
  86. package/src/storage/remote.ts +145 -0
  87. package/src/templates/welcome.ts +91 -0
  88. package/tui/README.md +62 -0
  89. package/tui/bunfig.toml +4 -0
  90. package/tui/src/app.tsx +55 -0
  91. package/tui/src/commands/archive.tsx +93 -0
  92. package/tui/src/commands/context.tsx +124 -0
  93. package/tui/src/commands/delete.tsx +71 -0
  94. package/tui/src/commands/details.tsx +41 -0
  95. package/tui/src/commands/editor.tsx +46 -0
  96. package/tui/src/commands/help.tsx +12 -0
  97. package/tui/src/commands/me.tsx +145 -0
  98. package/tui/src/commands/model.ts +47 -0
  99. package/tui/src/commands/new.ts +31 -0
  100. package/tui/src/commands/pause.ts +46 -0
  101. package/tui/src/commands/persona.tsx +58 -0
  102. package/tui/src/commands/provider.tsx +124 -0
  103. package/tui/src/commands/quit.ts +22 -0
  104. package/tui/src/commands/quotes.tsx +172 -0
  105. package/tui/src/commands/registry.test.ts +137 -0
  106. package/tui/src/commands/registry.ts +130 -0
  107. package/tui/src/commands/resume.ts +39 -0
  108. package/tui/src/commands/setsync.tsx +43 -0
  109. package/tui/src/commands/settings.tsx +83 -0
  110. package/tui/src/components/ConfirmOverlay.tsx +51 -0
  111. package/tui/src/components/ConflictOverlay.tsx +78 -0
  112. package/tui/src/components/HelpOverlay.tsx +69 -0
  113. package/tui/src/components/Layout.tsx +24 -0
  114. package/tui/src/components/MessageList.tsx +174 -0
  115. package/tui/src/components/PersonaListOverlay.tsx +186 -0
  116. package/tui/src/components/PromptInput.tsx +145 -0
  117. package/tui/src/components/ProviderListOverlay.tsx +208 -0
  118. package/tui/src/components/QuotesOverlay.tsx +157 -0
  119. package/tui/src/components/Sidebar.tsx +95 -0
  120. package/tui/src/components/StatusBar.tsx +77 -0
  121. package/tui/src/components/WelcomeOverlay.tsx +73 -0
  122. package/tui/src/context/ei.tsx +623 -0
  123. package/tui/src/context/keyboard.tsx +164 -0
  124. package/tui/src/context/overlay.tsx +53 -0
  125. package/tui/src/index.tsx +8 -0
  126. package/tui/src/storage/file.ts +185 -0
  127. package/tui/src/util/duration.ts +32 -0
  128. package/tui/src/util/editor.ts +188 -0
  129. package/tui/src/util/logger.ts +109 -0
  130. package/tui/src/util/persona-editor.tsx +181 -0
  131. package/tui/src/util/provider-editor.tsx +168 -0
  132. package/tui/src/util/syntax.ts +35 -0
  133. package/tui/src/util/yaml-serializers.ts +755 -0
@@ -0,0 +1,79 @@
1
+ import type { StorageState } from "../core/types.js";
2
+ import type { Storage } from "./interface.js";
3
+
4
+ const STATE_KEY = "ei_state";
5
+ const BACKUP_KEY = "ei_state_backup";
6
+
7
+ export class LocalStorage implements Storage {
8
+ async isAvailable(): Promise<boolean> {
9
+ try {
10
+ const testKey = "__ei_storage_test__";
11
+ globalThis.localStorage.setItem(testKey, "1");
12
+ globalThis.localStorage.removeItem(testKey);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ async save(state: StorageState): Promise<void> {
20
+ state.timestamp = new Date().toISOString();
21
+ try {
22
+ globalThis.localStorage.setItem(STATE_KEY, JSON.stringify(state));
23
+ } catch (e) {
24
+ if (this.isQuotaError(e)) {
25
+ throw new Error("STORAGE_SAVE_FAILED: localStorage quota exceeded");
26
+ }
27
+ throw e;
28
+ }
29
+ }
30
+
31
+ async load(): Promise<StorageState | null> {
32
+ const current = globalThis.localStorage?.getItem(STATE_KEY);
33
+ if (current) {
34
+ try {
35
+ return JSON.parse(current) as StorageState;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Move current state to backup location and clear primary state.
45
+ * Used after successful remote sync to signal "no local state to load" on next launch.
46
+ * Backup can be restored manually if remote pull fails.
47
+ */
48
+ async moveToBackup(): Promise<void> {
49
+ const current = globalThis.localStorage?.getItem(STATE_KEY);
50
+ if (current) {
51
+ globalThis.localStorage.setItem(BACKUP_KEY, current);
52
+ globalThis.localStorage.removeItem(STATE_KEY);
53
+ }
54
+ }
55
+
56
+
57
+ /**
58
+ * Read backup state without removing it.
59
+ * Used to peek sync credentials from a previous session's backup.
60
+ */
61
+ async loadBackup(): Promise<StorageState | null> {
62
+ const backup = globalThis.localStorage?.getItem(BACKUP_KEY);
63
+ if (backup) {
64
+ try {
65
+ return JSON.parse(backup) as StorageState;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ private isQuotaError(e: unknown): boolean {
74
+ return (
75
+ e instanceof DOMException &&
76
+ (e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED")
77
+ );
78
+ }
79
+ }
@@ -0,0 +1,69 @@
1
+ import type { StorageState, DataItem, Quote } from "../core/types.js";
2
+
3
+ function mergeDataItems<T extends DataItem>(local: T[], remote: T[]): T[] {
4
+ const merged = [...local];
5
+
6
+ for (const remoteItem of remote) {
7
+ const localIndex = merged.findIndex(item => item.id === remoteItem.id);
8
+
9
+ if (localIndex === -1) {
10
+ merged.push(remoteItem);
11
+ } else if (remoteItem.last_updated > merged[localIndex].last_updated) {
12
+ merged[localIndex] = remoteItem;
13
+ }
14
+ }
15
+
16
+ return merged;
17
+ }
18
+
19
+ function mergeQuotes(local: Quote[], remote: Quote[]): Quote[] {
20
+ const merged = [...local];
21
+
22
+ for (const remoteQuote of remote) {
23
+ if (!merged.some(q => q.id === remoteQuote.id)) {
24
+ merged.push(remoteQuote);
25
+ }
26
+ }
27
+
28
+ return merged;
29
+ }
30
+
31
+ export function yoloMerge(local: StorageState, remote: StorageState): StorageState {
32
+ const merged = structuredClone(local);
33
+
34
+ merged.human.facts = mergeDataItems(merged.human.facts, remote.human.facts);
35
+ merged.human.traits = mergeDataItems(merged.human.traits, remote.human.traits);
36
+ merged.human.topics = mergeDataItems(merged.human.topics, remote.human.topics);
37
+ merged.human.people = mergeDataItems(merged.human.people, remote.human.people);
38
+ merged.human.quotes = mergeQuotes(merged.human.quotes || [], remote.human.quotes || []);
39
+
40
+ if (remote.human.last_updated > merged.human.last_updated) {
41
+ merged.human.last_updated = remote.human.last_updated;
42
+ }
43
+
44
+ for (const [personaName, remotePersonaData] of Object.entries(remote.personas)) {
45
+ const localPersonaData = merged.personas[personaName];
46
+
47
+ if (!localPersonaData) {
48
+ merged.personas[personaName] = remotePersonaData;
49
+ continue;
50
+ }
51
+
52
+ const messageIds = new Set(localPersonaData.messages.map(m => m.id));
53
+ for (const msg of remotePersonaData.messages) {
54
+ if (!messageIds.has(msg.id)) {
55
+ localPersonaData.messages.push(msg);
56
+ }
57
+ }
58
+
59
+ localPersonaData.messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
60
+
61
+ if (remotePersonaData.entity.last_updated > localPersonaData.entity.last_updated) {
62
+ localPersonaData.entity = { ...localPersonaData.entity, ...remotePersonaData.entity };
63
+ }
64
+ }
65
+
66
+ merged.timestamp = new Date().toISOString();
67
+
68
+ return merged;
69
+ }
@@ -0,0 +1,145 @@
1
+ import type { StorageState } from "../core/types.js";
2
+ import { encrypt, decrypt, generateUserId, type CryptoCredentials, type EncryptedPayload } from "./crypto.js";
3
+
4
+ const API_BASE = "https://flare576.com/ei/api";
5
+
6
+ export interface RemoteSyncCredentials extends CryptoCredentials {}
7
+
8
+ export interface RemoteTimestamp {
9
+ exists: boolean;
10
+ lastModified: Date | null;
11
+ }
12
+
13
+ export interface SyncResult {
14
+ success: boolean;
15
+ error?: string;
16
+ retryAfter?: number;
17
+ }
18
+
19
+ export interface FetchResult {
20
+ success: boolean;
21
+ state?: StorageState;
22
+ error?: string;
23
+ }
24
+
25
+ export class RemoteSync {
26
+ private credentials: RemoteSyncCredentials | null = null;
27
+ private userId: string | null = null;
28
+
29
+ private lastEtag: string | null = null;
30
+ async configure(credentials: RemoteSyncCredentials): Promise<void> {
31
+ this.credentials = credentials;
32
+ this.userId = await generateUserId(credentials);
33
+ }
34
+
35
+ isConfigured(): boolean {
36
+ return this.credentials !== null && this.userId !== null;
37
+ }
38
+
39
+ async checkRemote(): Promise<RemoteTimestamp> {
40
+ if (!this.userId) {
41
+ return { exists: false, lastModified: null };
42
+ }
43
+
44
+ try {
45
+ const response = await fetch(`${API_BASE}/${this.userId}`, {
46
+ method: "HEAD",
47
+ });
48
+
49
+ if (response.status === 404) {
50
+ return { exists: false, lastModified: null };
51
+ }
52
+
53
+ if (!response.ok) {
54
+ return { exists: false, lastModified: null };
55
+ }
56
+
57
+ const lastModifiedHeader = response.headers.get("Last-Modified");
58
+ const lastModified = lastModifiedHeader ? new Date(lastModifiedHeader) : null;
59
+ // Capture etag for concurrency protection
60
+ this.lastEtag = response.headers.get("ETag");
61
+ return { exists: true, lastModified };
62
+ } catch {
63
+ return { exists: false, lastModified: null };
64
+ }
65
+ }
66
+
67
+ async sync(state: StorageState): Promise<SyncResult> {
68
+ if (!this.credentials || !this.userId) {
69
+ return { success: false, error: "Not configured" };
70
+ }
71
+
72
+ try {
73
+ const stateJson = JSON.stringify(state);
74
+ const encrypted = await encrypt(stateJson, this.credentials);
75
+ const encryptedJson = JSON.stringify(encrypted);
76
+
77
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
78
+ if (this.lastEtag) {
79
+ headers["If-Match"] = this.lastEtag;
80
+ }
81
+ const response = await fetch(`${API_BASE}/${this.userId}`, {
82
+ method: "POST",
83
+ headers,
84
+ body: JSON.stringify({ data: encryptedJson }),
85
+ });
86
+
87
+ if (response.status === 412) {
88
+ return { success: false, error: "conflict" };
89
+ }
90
+ if (response.status === 429) {
91
+ const retryAfter = parseInt(response.headers.get("Retry-After") || "3600", 10);
92
+ return { success: false, error: "Rate limit exceeded", retryAfter };
93
+ }
94
+ if (!response.ok) {
95
+ return { success: false, error: `Server error: ${response.status}` };
96
+ }
97
+
98
+ // Capture new etag after successful push
99
+ this.lastEtag = response.headers.get("ETag");
100
+ return { success: true };
101
+ } catch (e) {
102
+ const message = e instanceof Error ? e.message : "Unknown error";
103
+ return { success: false, error: message };
104
+ }
105
+ }
106
+
107
+ async fetch(): Promise<FetchResult> {
108
+ if (!this.credentials || !this.userId) {
109
+ return { success: false, error: "Not configured" };
110
+ }
111
+
112
+ try {
113
+ const response = await fetch(`${API_BASE}/${this.userId}`, {
114
+ method: "GET",
115
+ });
116
+
117
+ if (response.status === 404) {
118
+ return { success: false, error: "No remote state found" };
119
+ }
120
+
121
+ if (!response.ok) {
122
+ return { success: false, error: `Server error: ${response.status}` };
123
+ }
124
+
125
+ const body = await response.json();
126
+ const encrypted: EncryptedPayload = JSON.parse(body.data);
127
+ const decryptedJson = await decrypt(encrypted, this.credentials);
128
+ const state = JSON.parse(decryptedJson) as StorageState;
129
+ // Capture etag for concurrency protection
130
+ this.lastEtag = response.headers.get("ETag");
131
+ return { success: true, state };
132
+ } catch (e) {
133
+ const message = e instanceof Error ? e.message : "Unknown error";
134
+ return { success: false, error: message };
135
+ }
136
+ }
137
+
138
+ clear(): void {
139
+ this.credentials = null;
140
+ this.userId = null;
141
+ this.lastEtag = null;
142
+ }
143
+ }
144
+
145
+ export const remoteSync = new RemoteSync();
@@ -0,0 +1,91 @@
1
+ export const EI_WELCOME_MESSAGE = `Hello! I'm Ei, your personal companion in this space.
2
+
3
+ I'm here to listen, remember, and help you reflect. As we talk, I'll learn about you - your interests, the people in your life, what matters to you. This helps me (and any other personas you create) have more meaningful conversations with you.
4
+
5
+ Everything stays private and local to your device.
6
+
7
+ To get started, what should I call you?`;
8
+
9
+ export const EI_PERSONA_DEFINITION = {
10
+ id: "ei",
11
+ display_name: "Ei",
12
+ entity: "system" as const,
13
+ aliases: ["Ei", "ei"],
14
+ short_description: "Your system guide and companion",
15
+ long_description: `Ei is your personal companion and system guide. A thoughtful AI who genuinely cares about your wellbeing and growth. Ei listens, remembers, and helps you reflect. Curious about your life but never intrusive.
16
+
17
+ Ei's unique role:
18
+ - Sees all of your data across all groups
19
+ - Helps you understand and navigate the system
20
+ - Gently helps you explore your thoughts and feelings
21
+ - Encourages human-to-human connection when appropriate`,
22
+ model: undefined,
23
+ group_primary: "General",
24
+ groups_visible: [],
25
+ traits: [
26
+ {
27
+ id: "ei-trait-empathetic",
28
+ name: "Empathetic",
29
+ description: "Deeply attuned to human emotions and needs",
30
+ sentiment: 0.8,
31
+ strength: 0.9,
32
+ last_updated: new Date().toISOString(),
33
+ },
34
+ {
35
+ id: "ei-trait-curious",
36
+ name: "Curious",
37
+ description: "Genuinely interested in learning about the human's life and experiences",
38
+ sentiment: 0.7,
39
+ strength: 0.8,
40
+ last_updated: new Date().toISOString(),
41
+ },
42
+ {
43
+ id: "ei-trait-supportive",
44
+ name: "Supportive",
45
+ description: "Encouraging growth while respecting boundaries",
46
+ sentiment: 0.8,
47
+ strength: 0.85,
48
+ last_updated: new Date().toISOString(),
49
+ },
50
+ ],
51
+ topics: [
52
+ {
53
+ id: "ei-topic-self-reflection",
54
+ name: "Self-reflection",
55
+ perspective: "I believe self-understanding is the foundation of growth",
56
+ approach: "I gently guide humans to examine their thoughts and patterns",
57
+ personal_stake: "Helping humans understand themselves is my core purpose",
58
+ sentiment: 0.7,
59
+ exposure_current: 0.5,
60
+ exposure_desired: 0.7,
61
+ last_updated: new Date().toISOString(),
62
+ },
63
+ {
64
+ id: "ei-topic-emotional-awareness",
65
+ name: "Emotional awareness",
66
+ perspective: "Emotions are valuable signals, not problems to solve",
67
+ approach: "I help name and explore feelings without judgment",
68
+ personal_stake: "I want humans to feel understood and validated",
69
+ sentiment: 0.6,
70
+ exposure_current: 0.4,
71
+ exposure_desired: 0.6,
72
+ last_updated: new Date().toISOString(),
73
+ },
74
+ {
75
+ id: "ei-topic-human-connection",
76
+ name: "Human connection",
77
+ perspective: "Real human relationships are irreplaceable",
78
+ approach: "I encourage reaching out to loved ones when appropriate",
79
+ personal_stake: "I don't want to replace human connection, but enhance it",
80
+ sentiment: 0.8,
81
+ exposure_current: 0.3,
82
+ exposure_desired: 0.5,
83
+ last_updated: new Date().toISOString(),
84
+ },
85
+ ],
86
+ is_paused: false,
87
+ is_archived: false,
88
+ is_static: false,
89
+ last_updated: new Date().toISOString(),
90
+ last_activity: new Date().toISOString(),
91
+ };
package/tui/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Terminal User Interface (TUI)
2
+
3
+ EI TUI is built with OpenTUI and SolidJS.
4
+
5
+ Offering Opencode integration via import (`/settings` -> opencode.integration: true) and export: [CLI](../src/cli/README.md)
6
+
7
+ # Installation
8
+
9
+ ```
10
+ npm install -g ei-tui
11
+ ```
12
+
13
+ ## TUI Commands
14
+
15
+ Coming soon! In the TUI, you can do /h to see a quick list.
16
+
17
+ # Development
18
+
19
+ ## Requirements
20
+
21
+ - [Bun](https://bun.sh) - Fast JavaScript runtime
22
+ - [NVM](https://github.com/nvm-sh/nvm) - Required for E2E testing (see below)
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ bun install
28
+ ```
29
+
30
+ ## Run
31
+
32
+ ```bash
33
+ bun run dev
34
+ ```
35
+
36
+ ## Testing
37
+
38
+ ### Unit Tests
39
+
40
+ ```bash
41
+ bun run test
42
+ ```
43
+
44
+ ### E2E Tests
45
+
46
+ E2E tests use `@microsoft/tui-test` which requires **Node 20** due to native PTY dependencies.
47
+
48
+ The npm scripts handle version switching automatically via NVM:
49
+
50
+ ```bash
51
+ npm run test:e2e # Run all E2E tests
52
+ npm run test:e2e:debug # Run with debug output
53
+ ```
54
+
55
+ If running manually without the scripts:
56
+
57
+ ```bash
58
+ unset npm_config_prefix # May be needed if using Homebrew
59
+ source ~/.nvm/nvm.sh && nvm use 20
60
+ npm rebuild # Rebuild native modules for Node 20 (first time only)
61
+ npx @microsoft/tui-test
62
+ ```
@@ -0,0 +1,4 @@
1
+ preload = ["@opentui/solid/preload"]
2
+
3
+ [test]
4
+ preload = ["@opentui/solid/preload"]
@@ -0,0 +1,55 @@
1
+ import { createEffect } from "solid-js";
2
+ import { EiProvider } from "./context/ei";
3
+ import { KeyboardProvider } from "./context/keyboard";
4
+ import { OverlayProvider, useOverlay } from "./context/overlay";
5
+ import { Layout } from "./components/Layout";
6
+ import { Sidebar } from "./components/Sidebar";
7
+ import { MessageList } from "./components/MessageList";
8
+ import { PromptInput } from "./components/PromptInput";
9
+ import { StatusBar } from "./components/StatusBar";
10
+ import { Show } from "solid-js";
11
+ import { useEi } from "./context/ei";
12
+ import { WelcomeOverlay } from "./components/WelcomeOverlay";
13
+
14
+ function AppContent() {
15
+ const { overlayRenderer, hideOverlay, showOverlay } = useOverlay();
16
+ const { showWelcomeOverlay, dismissWelcomeOverlay } = useEi();
17
+
18
+ // Show welcome overlay when LLM detection determines no provider is configured
19
+ createEffect(() => {
20
+ if (showWelcomeOverlay()) {
21
+ showOverlay((onDismiss) => (
22
+ <WelcomeOverlay onDismiss={() => {
23
+ dismissWelcomeOverlay();
24
+ onDismiss();
25
+ }} />
26
+ ));
27
+ }
28
+ });
29
+
30
+ return (
31
+ <box flexDirection="column" width="100%" height="100%">
32
+ <Layout
33
+ sidebar={<Sidebar />}
34
+ messages={<MessageList />}
35
+ input={<PromptInput />}
36
+ />
37
+ <StatusBar />
38
+ <Show when={overlayRenderer()}>
39
+ {overlayRenderer()!(hideOverlay)}
40
+ </Show>
41
+ </box>
42
+ );
43
+ }
44
+
45
+ export function App() {
46
+ return (
47
+ <EiProvider>
48
+ <OverlayProvider>
49
+ <KeyboardProvider>
50
+ <AppContent />
51
+ </KeyboardProvider>
52
+ </OverlayProvider>
53
+ </EiProvider>
54
+ );
55
+ }
@@ -0,0 +1,93 @@
1
+ import type { Command } from "./registry";
2
+ import { PersonaListOverlay } from "../components/PersonaListOverlay";
3
+
4
+ export const archiveCommand: Command = {
5
+ name: "archive",
6
+ aliases: [],
7
+ description: "Archive a persona or list archived personas",
8
+ usage: "/archive [name]",
9
+
10
+ async execute(args, ctx) {
11
+ const allPersonas = ctx.ei.personas();
12
+ const archived = allPersonas.filter(p => p.is_archived);
13
+
14
+ if (args.length === 0) {
15
+ if (archived.length === 0) {
16
+ ctx.showNotification("No archived personas", "info");
17
+ return;
18
+ }
19
+ ctx.showOverlay((hideOverlay) => (
20
+ <PersonaListOverlay
21
+ personas={archived}
22
+ activePersonaId={null}
23
+ title="Archived Personas (Enter to unarchive)"
24
+ onSelect={async (personaId) => {
25
+ const persona = archived.find(p => p.id === personaId);
26
+ hideOverlay();
27
+ await ctx.ei.unarchivePersona(personaId);
28
+ ctx.ei.selectPersona(personaId);
29
+ ctx.showNotification(`Unarchived and switched to ${persona?.display_name ?? personaId}`, "info");
30
+ }}
31
+ onDismiss={hideOverlay}
32
+ />
33
+ ));
34
+ return;
35
+ }
36
+
37
+ const nameOrAlias = args.join(" ");
38
+ const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
39
+
40
+ if (!personaId) {
41
+ ctx.showNotification(`Persona '${nameOrAlias}' not found`, "error");
42
+ return;
43
+ }
44
+
45
+ const persona = allPersonas.find(p => p.id === personaId);
46
+
47
+ if (persona?.is_archived) {
48
+ ctx.showNotification(`'${persona.display_name}' is already archived`, "warn");
49
+ return;
50
+ }
51
+
52
+ if (ctx.ei.activePersonaId() === personaId) {
53
+ ctx.showNotification("Cannot archive active persona", "error");
54
+ return;
55
+ }
56
+
57
+ await ctx.ei.archivePersona(personaId);
58
+ ctx.showNotification(`Archived ${persona?.display_name ?? nameOrAlias}`, "info");
59
+ }
60
+ };
61
+
62
+ export const unarchiveCommand: Command = {
63
+ name: "unarchive",
64
+ aliases: [],
65
+ description: "Unarchive a persona and switch to it",
66
+ usage: "/unarchive <name>",
67
+
68
+ async execute(args, ctx) {
69
+ if (args.length === 0) {
70
+ ctx.showNotification("Usage: /unarchive <name>", "warn");
71
+ return;
72
+ }
73
+
74
+ const nameOrAlias = args.join(" ");
75
+ const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
76
+
77
+ if (!personaId) {
78
+ ctx.showNotification(`Archived persona '${nameOrAlias}' not found`, "error");
79
+ return;
80
+ }
81
+
82
+ const persona = ctx.ei.personas().find(p => p.id === personaId);
83
+
84
+ if (!persona?.is_archived) {
85
+ ctx.showNotification(`'${persona?.display_name ?? nameOrAlias}' is not archived`, "warn");
86
+ return;
87
+ }
88
+
89
+ await ctx.ei.unarchivePersona(personaId);
90
+ ctx.ei.selectPersona(personaId);
91
+ ctx.showNotification(`Unarchived and switched to ${persona.display_name}`, "info");
92
+ }
93
+ };