@agentprojectcontext/apx 1.20.0 → 1.22.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.
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * A self-contained chat interface that streams messages through the APX daemon
5
5
  * using the APX sync context. The complex opencode session view is replaced with
6
- * a simple but functional chat layout.
6
+ * a simple but functional chat layout, plus an APX-tailored sidebar.
7
7
  */
8
- import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
8
+ import { For, Show, createMemo, createSignal, onCleanup } from "solid-js"
9
9
  import { TextareaRenderable } from "@opentui/core"
10
10
  import { useTerminalDimensions } from "@opentui/solid"
11
11
  import { useTheme } from "@tui/context/theme"
@@ -15,6 +15,20 @@ import { useToast, Toast } from "@tui/ui/toast"
15
15
  import { useExit } from "@tui/context/exit"
16
16
  import { usePromptRef } from "@tui/context/prompt"
17
17
  import type { ApxMessage } from "@tui/context/sync-apx"
18
+ import { SidebarApx } from "./sidebar-apx"
19
+
20
+ /** Split a daemon error like "fetch failed (trace: abc-123)" into message + trace. */
21
+ function parseError(raw: string): { message: string; trace?: string; hint?: string } {
22
+ const m = raw.match(/^([\s\S]*?)\s*\(trace:\s*([^)]+)\)\s*$/)
23
+ const message = (m ? m[1] : raw).trim()
24
+ const trace = m ? m[2].trim() : undefined
25
+ let hint: string | undefined
26
+ if (/fetch failed/i.test(message))
27
+ hint = "No se pudo contactar el modelo. Verificá que el proveedor esté disponible (p. ej. Ollama corriendo)."
28
+ else if (/not enabled/i.test(message)) hint = "El super-agent está deshabilitado en ~/.apx/config.json."
29
+ else if (/\b401\b|unauthorized|api[_ ]?key/i.test(message)) hint = "Revisá la API key del proveedor en ~/.apx/config.json."
30
+ return { message, trace, hint }
31
+ }
18
32
 
19
33
  function UserBubble(props: { msg: ApxMessage }) {
20
34
  const { theme } = useTheme()
@@ -30,18 +44,51 @@ function UserBubble(props: { msg: ApxMessage }) {
30
44
  )
31
45
  }
32
46
 
33
- function AssistantBubble(props: { msg: ApxMessage }) {
47
+ function ErrorBubble(props: { msg: ApxMessage }) {
34
48
  const { theme } = useTheme()
35
- const hasText = () => props.msg.text.length > 0
49
+ const parsed = createMemo(() => parseError(props.msg.text))
50
+ return (
51
+ <box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
52
+ <text color={theme.error} bold>
53
+ ⚠ Error
54
+ </text>
55
+ <text color={theme.error} wrap>
56
+ {parsed().message}
57
+ </text>
58
+ <Show when={parsed().hint}>
59
+ {(hint) => (
60
+ <text color={theme.textMuted} wrap>
61
+ {hint()}
62
+ </text>
63
+ )}
64
+ </Show>
65
+ <Show when={parsed().trace}>
66
+ {(trace) => <text color={theme.textMuted}>trace: {trace()}</text>}
67
+ </Show>
68
+ </box>
69
+ )
70
+ }
71
+
72
+ function AssistantBubble(props: { msg: ApxMessage }) {
73
+ const { theme, syntax } = useTheme()
74
+ const hasText = () => props.msg.text.trim().length > 0
36
75
  return (
37
76
  <box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
38
77
  <text color={theme.success} bold>
39
78
  {props.msg.streaming ? "Assistant ▸" : "Assistant"}
40
79
  </text>
41
80
  <Show when={hasText()} fallback={<text color={theme.textMuted}>…</text>}>
42
- <text color={props.msg.error ? theme.error : theme.text} wrap>
43
- {props.msg.text}
44
- </text>
81
+ <box flexShrink={0}>
82
+ <code
83
+ filetype="markdown"
84
+ drawUnstyledText={false}
85
+ streaming={props.msg.streaming ?? false}
86
+ syntaxStyle={syntax()}
87
+ content={props.msg.text.trim()}
88
+ conceal={true}
89
+ fg={theme.text}
90
+ />
91
+ </box>
45
92
  </Show>
46
93
  </box>
47
94
  )
@@ -66,10 +113,7 @@ function ShellBubble(props: { msg: ApxMessage }) {
66
113
  <text color={theme.warning ?? theme.primary} bold>
67
114
  {header()}
68
115
  </text>
69
- <text
70
- color={props.msg.exitCode && props.msg.exitCode !== 0 ? theme.error : theme.text}
71
- wrap
72
- >
116
+ <text color={props.msg.exitCode && props.msg.exitCode !== 0 ? theme.error : theme.text} wrap>
73
117
  {body()}
74
118
  </text>
75
119
  </box>
@@ -152,68 +196,77 @@ export function Session() {
152
196
 
153
197
  return (
154
198
  <box flexDirection="column" flexGrow={1} width={dims().width} height={dims().height}>
155
- {/* Message list */}
156
- <scrollbox
157
- flexGrow={1}
158
- stickyScroll
159
- stickyStart="bottom"
160
- verticalScrollbarOptions={{ visible: true }}
161
- >
162
- <box flexDirection="column" width={dims().width}>
163
- <Show
164
- when={messages().length > 0}
165
- fallback={
166
- <box paddingLeft={2} paddingTop={2}>
199
+ <box flexDirection="row" flexGrow={1} minHeight={0}>
200
+ {/* Chat column */}
201
+ <box flexDirection="column" flexGrow={1} minWidth={0}>
202
+ {/* Message list */}
203
+ <scrollbox
204
+ flexGrow={1}
205
+ stickyScroll
206
+ stickyStart="bottom"
207
+ verticalScrollbarOptions={{ visible: true }}
208
+ >
209
+ <box flexDirection="column">
210
+ <Show
211
+ when={messages().length > 0}
212
+ fallback={
213
+ <box paddingLeft={2} paddingTop={2}>
214
+ <text color={theme.textMuted} italic>
215
+ Type a message to chat, or prefix with ! to run a shell command (e.g. !ls).
216
+ </text>
217
+ </box>
218
+ }
219
+ >
220
+ <For each={messages()}>
221
+ {(msg) => {
222
+ if (msg.role === "user") return <UserBubble msg={msg} />
223
+ if (msg.role === "shell") return <ShellBubble msg={msg} />
224
+ if (msg.error) return <ErrorBubble msg={msg} />
225
+ return <AssistantBubble msg={msg} />
226
+ }}
227
+ </For>
228
+ </Show>
229
+ <box height={1} />
230
+ </box>
231
+ </scrollbox>
232
+
233
+ {/* Input area */}
234
+ <box
235
+ flexShrink={0}
236
+ flexDirection="column"
237
+ borderTop={1}
238
+ borderColor={theme.border}
239
+ backgroundColor={theme.backgroundElement}
240
+ >
241
+ <box paddingLeft={2} paddingRight={2} paddingTop={1}>
242
+ <textarea
243
+ ref={(r: TextareaRenderable) => {
244
+ inputEl = r
245
+ promptRef.set(makeRef(r))
246
+ }}
247
+ placeholder={sending() ? "Waiting for response…" : "Ask anything... (prefix ! to run shell, e.g. !ls)"}
248
+ placeholderColor={theme.textMuted}
249
+ textColor={theme.text}
250
+ focusedTextColor={theme.text}
251
+ minHeight={1}
252
+ maxHeight={6}
253
+ onSubmit={() => {
254
+ setTimeout(() => setTimeout(() => handleSubmit(), 0), 0)
255
+ }}
256
+ />
257
+ </box>
258
+ <box height={1} paddingLeft={2} paddingRight={2} justifyContent="space-between" flexDirection="row">
259
+ <Show when={sending()} fallback={<text color={theme.textMuted}>enter send · ! shell · exit quit</text>}>
167
260
  <text color={theme.textMuted} italic>
168
- Type a message to chat, or prefix with ! to run a shell command (e.g. !ls).
261
+ Streaming…
169
262
  </text>
170
- </box>
171
- }
172
- >
173
- <For each={messages()}>
174
- {(msg) => {
175
- if (msg.role === "user") return <UserBubble msg={msg} />
176
- if (msg.role === "shell") return <ShellBubble msg={msg} />
177
- return <AssistantBubble msg={msg} />
178
- }}
179
- </For>
180
- </Show>
181
- <box height={1} />
182
- </box>
183
- </scrollbox>
184
-
185
- {/* Input area */}
186
- <box
187
- flexShrink={0}
188
- flexDirection="column"
189
- borderTop={1}
190
- borderColor={theme.border}
191
- backgroundColor={theme.backgroundElement}
192
- >
193
- <box paddingLeft={2} paddingRight={2} paddingTop={1}>
194
- <textarea
195
- ref={(r: TextareaRenderable) => {
196
- inputEl = r
197
- promptRef.set(makeRef(r))
198
- }}
199
- placeholder={sending() ? "Waiting for response…" : "Ask anything... (prefix ! to run shell, e.g. !ls)"}
200
- placeholderColor={theme.textMuted}
201
- textColor={theme.text}
202
- focusedTextColor={theme.text}
203
- minHeight={1}
204
- maxHeight={6}
205
- onSubmit={() => {
206
- setTimeout(() => setTimeout(() => handleSubmit(), 0), 0)
207
- }}
208
- />
209
- </box>
210
- <box height={1} paddingLeft={2} paddingRight={2}>
211
- <Show when={sending()}>
212
- <text color={theme.textMuted} italic>
213
- Streaming…
214
- </text>
215
- </Show>
263
+ </Show>
264
+ </box>
265
+ </box>
216
266
  </box>
267
+
268
+ {/* Sidebar */}
269
+ <SidebarApx sessionID={sessionID()} />
217
270
  </box>
218
271
  <Toast />
219
272
  </box>
@@ -0,0 +1,90 @@
1
+ /**
2
+ * APX session sidebar.
3
+ *
4
+ * A self-contained panel tailored to APX data (session, agent, model, token
5
+ * usage, working directory). Replaces opencode's plugin-driven sidebar.tsx,
6
+ * which depends on feature plugins APX does not ship.
7
+ */
8
+ import { createMemo, Show } from "solid-js"
9
+ import { useTheme } from "@tui/context/theme"
10
+ import { useApxSync } from "@tui/context/sync-apx"
11
+ import { useSDK } from "@tui/context/sdk-apx"
12
+ import pkg from "../../../../package.json"
13
+
14
+ function titlecase(value: string) {
15
+ if (!value) return value
16
+ return value.charAt(0).toUpperCase() + value.slice(1)
17
+ }
18
+
19
+ function Section(props: { title: string; children: any }) {
20
+ const { theme } = useTheme()
21
+ return (
22
+ <box flexDirection="column" flexShrink={0} marginBottom={1}>
23
+ <text fg={theme.text}>
24
+ <b>{props.title}</b>
25
+ </text>
26
+ {props.children}
27
+ </box>
28
+ )
29
+ }
30
+
31
+ export function SidebarApx(props: { sessionID: string }) {
32
+ const { theme } = useTheme()
33
+ const sync = useApxSync()
34
+ const sdk = useSDK()
35
+
36
+ const session = createMemo(() => sync.session.get(props.sessionID))
37
+ const messages = createMemo(() => sync.session.messages(props.sessionID))
38
+ const usage = createMemo(() => sync.data.usage)
39
+ const totalTokens = createMemo(() => usage().input + usage().output)
40
+
41
+ return (
42
+ <box
43
+ backgroundColor={theme.backgroundPanel}
44
+ width={40}
45
+ height="100%"
46
+ flexShrink={0}
47
+ flexDirection="column"
48
+ paddingTop={1}
49
+ paddingBottom={1}
50
+ paddingLeft={2}
51
+ paddingRight={2}
52
+ >
53
+ <box flexGrow={1} flexDirection="column">
54
+ <Section title="Sesión">
55
+ <text fg={theme.textMuted}>{session()?.title || "chat local"}</text>
56
+ </Section>
57
+
58
+ <Section title="Agente">
59
+ <text fg={theme.textMuted}>{titlecase(sdk.agent)}</text>
60
+ </Section>
61
+
62
+ <Section title="Modelo">
63
+ <text fg={theme.textMuted} wrap>
64
+ {sdk.model}
65
+ </text>
66
+ </Section>
67
+
68
+ <Section title="Contexto">
69
+ <text fg={theme.textMuted}>{totalTokens().toLocaleString()} tokens</text>
70
+ <text fg={theme.textMuted}>
71
+ {usage().input.toLocaleString()} in · {usage().output.toLocaleString()} out
72
+ </text>
73
+ <text fg={theme.textMuted}>{messages().length} mensajes</text>
74
+ </Section>
75
+
76
+ <Section title="Directorio">
77
+ <text fg={theme.textMuted} wrap>
78
+ {process.cwd()}
79
+ </text>
80
+ </Section>
81
+ </box>
82
+
83
+ <box flexShrink={0} paddingTop={1}>
84
+ <text fg={theme.textMuted}>
85
+ <span style={{ fg: theme.success }}>•</span> <b>APX</b> <span>{pkg.version}</span>
86
+ </text>
87
+ </box>
88
+ </box>
89
+ )
90
+ }
@@ -23,6 +23,7 @@
23
23
  "@/snapshot": ["./_shims/snapshot.ts"],
24
24
  "@/config/console-state": ["./_shims/config-console-state.ts"],
25
25
  "@/cli/error": ["./_shims/cli-error.ts"],
26
+ "@/cli/cmd/prompt-display": ["./_shims/prompt-display.ts"],
26
27
  "@/cli/logo": ["./_shims/cli-logo.ts"],
27
28
  "@/cli/ui.ts": ["./_shims/cli-ui.ts"],
28
29
  "@/cli/ui": ["./_shims/cli-ui.ts"],