@elliotsystems/eai 0.1.2 → 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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The Elliot knowledge panel — `sidebar_content`, plus Elliot's identity in
3
+ * `sidebar_title` and `sidebar_footer`.
4
+ *
5
+ * It shows only what nothing else in the sidebar shows. Upstream's built-in
6
+ * sections already list MCP servers, LSP clients, todos and changed files, so
7
+ * restating any of them here would be exactly the duplication this redesign is
8
+ * removing. What is left — and what no stock sidebar can know — is the retrieval
9
+ * scope in force and the sources Elliot actually cited in this session.
10
+ *
11
+ * Knowledge health therefore earns a row only when it is NOT healthy. A problem
12
+ * explains a bad answer and is worth the space; "working normally" is already
13
+ * visible in the MCP section and is not.
14
+ */
15
+ import { For, Show } from "solid-js"
16
+
17
+ import type { SessionSlotProps, TuiPluginApi } from "./api.js"
18
+ import {
19
+ collectKnowledgeResults,
20
+ distinctCitations,
21
+ elliotServerNames,
22
+ knowledgeConnection,
23
+ type Citation,
24
+ type KnowledgeResult,
25
+ } from "./knowledge.js"
26
+
27
+ /** A height-bounded panel: past this, rows push the built-in sections off. */
28
+ const MAX_CITATIONS = 6
29
+
30
+ /** A small-caps-style section label, matching upstream's sidebar rhythm. */
31
+ function Label(props: { api: TuiPluginApi; children: string }) {
32
+ return (
33
+ <text fg={props.api.theme.current.border} selectable={false}>
34
+ {props.children}
35
+ </text>
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Every knowledge lookup in this session.
41
+ *
42
+ * Reads the transcript rather than calling the backend — see `knowledge.ts` for
43
+ * why a fetch here would violate the no-second-credential-path rule.
44
+ */
45
+ function resultsFor(api: TuiPluginApi, sessionID: string): KnowledgeResult[] {
46
+ const servers = elliotServerNames(api.state.mcp())
47
+ if (servers.length === 0) return []
48
+
49
+ const parts = api.state.session
50
+ .messages(sessionID)
51
+ .flatMap((message) => [...api.state.part(message.id)])
52
+ return collectKnowledgeResults(parts, servers)
53
+ }
54
+
55
+ /**
56
+ * The scope retrieval actually ran under.
57
+ *
58
+ * The backend echoes `repo_scope` in its envelope, so the most recent lookup is
59
+ * authoritative — that is the scope Elliot *used*, not the one we assume from
60
+ * the worktree. Falling back to the branch keeps the row useful before the first
61
+ * lookup, and a missing scope is reported as tenant-wide rather than left blank,
62
+ * because "unscoped" is a real and meaningful state.
63
+ */
64
+ function scopeLabel(results: readonly KnowledgeResult[]): string | undefined {
65
+ for (const result of [...results].reverse()) {
66
+ if (result.repoScope) return result.repoScope
67
+ }
68
+ return undefined
69
+ }
70
+
71
+ function CitationRow(props: { api: TuiPluginApi; citation: Citation }) {
72
+ const theme = props.api.theme.current
73
+ return (
74
+ <box flexDirection="row" gap={1}>
75
+ <text fg={theme.textMuted} selectable={true}>
76
+ {props.citation.path}
77
+ </text>
78
+ <Show when={props.citation.symbol}>
79
+ <text fg={theme.border} selectable={false}>
80
+ {props.citation.symbol}
81
+ </text>
82
+ </Show>
83
+ </box>
84
+ )
85
+ }
86
+
87
+ /** The one row that appears only on trouble. */
88
+ function Trouble(props: { api: TuiPluginApi }) {
89
+ const theme = props.api.theme.current
90
+ const connection = () => knowledgeConnection(props.api.state.mcp())
91
+
92
+ return (
93
+ <Show when={connection().status === "failed" || connection().status === undefined}>
94
+ <box flexDirection="row" gap={1}>
95
+ <text fg={connection().status === "failed" ? theme.error : theme.warning} selectable={false}>
96
+ !
97
+ </text>
98
+ <text fg={theme.textMuted} selectable={false}>
99
+ {connection().status === "failed"
100
+ ? "knowledge unreachable — answers are unsourced"
101
+ : "no knowledge server configured"}
102
+ </text>
103
+ </box>
104
+ </Show>
105
+ )
106
+ }
107
+
108
+ export function SidebarContent(props: { api: TuiPluginApi; slot: SessionSlotProps }) {
109
+ const theme = props.api.theme.current
110
+ const results = () => resultsFor(props.api, props.slot.session_id)
111
+ const scope = () => scopeLabel(results())
112
+ const citations = () => distinctCitations(results(), MAX_CITATIONS)
113
+
114
+ return (
115
+ <box gap={1}>
116
+ <box>
117
+ <Label api={props.api}>KNOWLEDGE</Label>
118
+ <Trouble api={props.api} />
119
+ <Show
120
+ when={scope()}
121
+ fallback={
122
+ <text fg={theme.textMuted} selectable={false}>
123
+ organisation-wide
124
+ </text>
125
+ }
126
+ >
127
+ <text fg={theme.text} selectable={true}>
128
+ {scope()}
129
+ </text>
130
+ </Show>
131
+ </box>
132
+
133
+ <Show when={citations().length > 0}>
134
+ <box>
135
+ <Label api={props.api}>SOURCES CITED</Label>
136
+ <For each={citations()}>
137
+ {(citation) => <CitationRow api={props.api} citation={citation} />}
138
+ </For>
139
+ </box>
140
+ </Show>
141
+ </box>
142
+ )
143
+ }
144
+
145
+ /**
146
+ * The session title, with Elliot's mark ahead of it.
147
+ *
148
+ * `sidebar_title` is `single_winner`, so this replaces upstream's title block
149
+ * rather than stacking with it — which is why the title itself is reproduced
150
+ * here instead of only the branding.
151
+ */
152
+ export function SidebarTitle(props: { api: TuiPluginApi; slot: SessionSlotProps & { title: string } }) {
153
+ const theme = props.api.theme.current
154
+ return (
155
+ <box paddingRight={1}>
156
+ <box flexDirection="row" gap={1}>
157
+ <text fg={theme.primary} selectable={false}>
158
+ ●
159
+ </text>
160
+ <text fg={theme.text} selectable={true}>
161
+ {props.slot.title}
162
+ </text>
163
+ </box>
164
+ </box>
165
+ )
166
+ }
167
+
168
+ /** Replaces upstream's "OpenCode <version>" line; the build is Elliot's. */
169
+ export function SidebarFooter(props: { api: TuiPluginApi }) {
170
+ const theme = props.api.theme.current
171
+ return (
172
+ <box flexDirection="row" gap={1}>
173
+ <text fg={theme.primary} selectable={false}>
174
+ ●
175
+ </text>
176
+ <text fg={theme.text} selectable={false}>
177
+ Elliot AI
178
+ </text>
179
+ <text fg={theme.border} selectable={false}>
180
+ {props.api.app.version}
181
+ </text>
182
+ </box>
183
+ )
184
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The ELLIOT AI wordmark, filling `home_logo` (declared `mode="replace"`, so it
3
+ * supplants upstream's `<Logo/>`).
4
+ *
5
+ * Colour comes from the active theme, never from literals, so `themes/elliot.json`
6
+ * stays the single source of truth and the mark degrades correctly under another
7
+ * theme.
8
+ */
9
+ import { For } from "solid-js"
10
+ import { RGBA, TextAttributes } from "@opentui/core"
11
+
12
+ import type { TuiPluginApi } from "./api.js"
13
+
14
+ /**
15
+ * ELLIOT / AI, transcribed from the design's glyph grid.
16
+ *
17
+ * 4-column glyphs separated by one space, except `I` and `T` which are 3 — that
18
+ * is the design's own metric. `left` is 27 columns, `right` is 8.
19
+ *
20
+ * `_` and `^` are the host's shading marks (`packages/tui/src/logo.ts`): a cell
21
+ * painted on a tinted background, and `▀` on that same tint.
22
+ */
23
+ const WORDMARK = {
24
+ left: [
25
+ "█▀▀▀ █ █ ▀▀▀ █▀▀█ ▀▀▀",
26
+ "█^^^ █ █ █ █__█ █ ",
27
+ "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀▀ ▀ ",
28
+ ],
29
+ right: ["█▀▀█ ▀▀▀", "█^^█ █ ", "▀ ▀ ▀▀▀"],
30
+ } as const
31
+
32
+ /** The design sets ELLIOT and AI two columns apart, not one. */
33
+ const WORDMARK_GAP = 2
34
+
35
+ /**
36
+ * Tint a shadow from the theme, mirroring the host's `tint(background, fg, 0.25)`
37
+ * so a plugin-rendered wordmark shades identically to a host-rendered one.
38
+ * Reimplemented rather than imported because the host's theme context is not a
39
+ * shared module.
40
+ *
41
+ * It must return a real `RGBA`, not a matching plain object. The renderer hands
42
+ * colours to opentui's FFI (`textBufferSetDefaultBg`), which needs the
43
+ * Float32Array an `RGBA` carries; a structurally identical literal reaches the
44
+ * boundary and dies with "Expected ArrayBufferView but received Cell to a
45
+ * pointer", taking the whole TUI down. That crash was latent for as long as this
46
+ * plugin never loaded -- fixing the loader is what surfaced it.
47
+ */
48
+ function shadowOf(background: RGBA, fg: RGBA): RGBA {
49
+ const mix = (a: number, b: number) => a + (b - a) * 0.25
50
+ return RGBA.fromValues(
51
+ mix(background.r, fg.r),
52
+ mix(background.g, fg.g),
53
+ mix(background.b, fg.b),
54
+ background.a ?? 1,
55
+ )
56
+ }
57
+
58
+ function Glyphs(props: { line: string; fg: RGBA; shadow: RGBA; bold: boolean }) {
59
+ const attrs = props.bold ? TextAttributes.BOLD : undefined
60
+ return (
61
+ <box flexDirection="row">
62
+ {Array.from(props.line).map((char) => {
63
+ if (char === "_")
64
+ return (
65
+ <text fg={props.fg} bg={props.shadow} attributes={attrs} selectable={false}>
66
+ {" "}
67
+ </text>
68
+ )
69
+ if (char === "^")
70
+ return (
71
+ <text fg={props.fg} bg={props.shadow} attributes={attrs} selectable={false}>
72
+ ▀
73
+ </text>
74
+ )
75
+ return (
76
+ <text fg={props.fg} attributes={attrs} selectable={false}>
77
+ {char}
78
+ </text>
79
+ )
80
+ })}
81
+ </box>
82
+ )
83
+ }
84
+
85
+ export function Wordmark(props: { api: TuiPluginApi }) {
86
+ const theme = props.api.theme.current
87
+ return (
88
+ <box>
89
+ <For each={WORDMARK.left}>
90
+ {(line, index) => (
91
+ <box flexDirection="row" gap={WORDMARK_GAP}>
92
+ <Glyphs
93
+ line={line}
94
+ fg={theme.textMuted}
95
+ shadow={shadowOf(theme.background, theme.textMuted)}
96
+ bold={false}
97
+ />
98
+ <Glyphs
99
+ line={WORDMARK.right[index()] ?? ""}
100
+ fg={theme.primary}
101
+ shadow={shadowOf(theme.background, theme.primary)}
102
+ bold={true}
103
+ />
104
+ </box>
105
+ )}
106
+ </For>
107
+ </box>
108
+ )
109
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "defs": {
4
+ "brandOrange": "#FF6600",
5
+ "accentOrange": "#FF7A2E",
6
+ "accentOrangeLit": "#FF9354",
7
+ "accentOrangeSoft": "#FFB07A",
8
+ "accentOrangeDeep": "#C2410C",
9
+ "accentOrangeInk": "#7C2D12",
10
+
11
+ "ground": "#0B0C0E",
12
+ "panel": "#121315",
13
+ "elevated": "#191B1D",
14
+ "elevated2": "#212325",
15
+ "border": "#262829",
16
+ "borderSoft": "#1C1E20",
17
+
18
+ "text": "#F5F6F8",
19
+ "textFaint": "#A0A3A8",
20
+ "textDim": "#8E9297",
21
+ "textGhost": "#3A3D3F",
22
+
23
+ "success": "#5DD89B",
24
+ "warning": "#E8C33A",
25
+ "error": "#F7768E",
26
+ "teal": "#5EEAD4",
27
+
28
+ "lightGround": "#FFFFFF",
29
+ "lightPanel": "#FAFAFA",
30
+ "lightElevated": "#F2F2F4",
31
+ "lightElevated2": "#E9E9EC",
32
+ "lightBorder": "#D9D9DE",
33
+ "lightBorderSoft": "#EDEDF0",
34
+ "lightText": "#0B0C0E",
35
+ "lightTextDim": "#6B6E73",
36
+ "lightTextFaint": "#8E9297",
37
+ "lightTextGhost": "#C4C6CA",
38
+ "lightSuccess": "#1B7F4F",
39
+ "lightWarning": "#8A6D0B",
40
+ "lightError": "#C2334C",
41
+ "lightTeal": "#0F766E"
42
+ },
43
+ "theme": {
44
+ "primary": { "dark": "brandOrange", "light": "brandOrange" },
45
+ "accent": { "dark": "accentOrange", "light": "accentOrangeDeep" },
46
+ "secondary": { "dark": "teal", "light": "lightTeal" },
47
+ "info": { "dark": "accentOrangeSoft", "light": "accentOrangeDeep" },
48
+
49
+ "error": { "dark": "error", "light": "lightError" },
50
+ "warning": { "dark": "warning", "light": "lightWarning" },
51
+ "success": { "dark": "success", "light": "lightSuccess" },
52
+
53
+ "text": { "dark": "text", "light": "lightText" },
54
+ "textMuted": { "dark": "textDim", "light": "lightTextDim" },
55
+
56
+ "selectedListItemText": { "dark": "ground", "light": "lightGround" },
57
+
58
+ "background": { "dark": "ground", "light": "lightGround" },
59
+ "backgroundPanel": { "dark": "panel", "light": "lightPanel" },
60
+ "backgroundElement": { "dark": "elevated", "light": "lightElevated" },
61
+ "backgroundMenu": { "dark": "elevated2", "light": "lightElevated2" },
62
+
63
+ "border": { "dark": "border", "light": "lightBorder" },
64
+ "borderActive": { "dark": "accentOrange", "light": "accentOrangeDeep" },
65
+ "borderSubtle": { "dark": "borderSoft", "light": "lightBorderSoft" },
66
+
67
+ "diffAdded": { "dark": "success", "light": "lightSuccess" },
68
+ "diffRemoved": { "dark": "error", "light": "lightError" },
69
+ "diffContext": { "dark": "textDim", "light": "lightTextDim" },
70
+ "diffHunkHeader": { "dark": "textFaint", "light": "lightTextFaint" },
71
+ "diffHighlightAdded": { "dark": "#8DE8B8", "light": "#12603B" },
72
+ "diffHighlightRemoved": { "dark": "#FF9BAC", "light": "#A62740" },
73
+ "diffAddedBg": { "dark": "#0F2018", "light": "#E4F5EB" },
74
+ "diffRemovedBg": { "dark": "#241318", "light": "#FBE4E9" },
75
+ "diffContextBg": { "dark": "panel", "light": "lightPanel" },
76
+ "diffLineNumber": { "dark": "textGhost", "light": "lightTextGhost" },
77
+ "diffAddedLineNumberBg": { "dark": "#0C1913", "light": "#D6EDDF" },
78
+ "diffRemovedLineNumberBg": { "dark": "#1D0F13", "light": "#F6D8DF" },
79
+
80
+ "markdownText": { "dark": "text", "light": "lightText" },
81
+ "markdownHeading": { "dark": "brandOrange", "light": "accentOrangeDeep" },
82
+ "markdownLink": { "dark": "accentOrange", "light": "accentOrangeDeep" },
83
+ "markdownLinkText": { "dark": "accentOrangeSoft", "light": "accentOrangeDeep" },
84
+ "markdownCode": { "dark": "accentOrangeSoft", "light": "accentOrangeDeep" },
85
+ "markdownBlockQuote": { "dark": "textFaint", "light": "lightTextFaint" },
86
+ "markdownEmph": { "dark": "accentOrangeLit", "light": "accentOrangeDeep" },
87
+ "markdownStrong": { "dark": "accentOrange", "light": "accentOrangeDeep" },
88
+ "markdownHorizontalRule": { "dark": "borderSoft", "light": "lightBorderSoft" },
89
+ "markdownListItem": { "dark": "accentOrange", "light": "accentOrangeDeep" },
90
+ "markdownListEnumeration": { "dark": "accentOrangeLit", "light": "accentOrangeDeep" },
91
+ "markdownImage": { "dark": "accentOrange", "light": "accentOrangeDeep" },
92
+ "markdownImageText": { "dark": "textFaint", "light": "lightTextFaint" },
93
+ "markdownCodeBlock": { "dark": "text", "light": "lightText" },
94
+
95
+ "syntaxComment": { "dark": "textGhost", "light": "lightTextGhost" },
96
+ "syntaxKeyword": { "dark": "accentOrange", "light": "accentOrangeDeep" },
97
+ "syntaxFunction": { "dark": "accentOrangeLit", "light": "accentOrangeInk" },
98
+ "syntaxVariable": { "dark": "text", "light": "lightText" },
99
+ "syntaxString": { "dark": "success", "light": "lightSuccess" },
100
+ "syntaxNumber": { "dark": "accentOrangeSoft", "light": "accentOrangeDeep" },
101
+ "syntaxType": { "dark": "teal", "light": "lightTeal" },
102
+ "syntaxOperator": { "dark": "textFaint", "light": "lightTextFaint" },
103
+ "syntaxPunctuation": { "dark": "textDim", "light": "lightTextDim" }
104
+ }
105
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "lib": [
6
+ "ES2022"
7
+ ],
8
+ "moduleResolution": "bundler",
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "noEmit": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "jsx": "preserve"
14
+ },
15
+ "include": [
16
+ "src/**/*",
17
+ "test/**/*"
18
+ ]
19
+ }