@agentprojectcontext/apx 1.32.0 → 1.32.2

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 (219) hide show
  1. package/package.json +6 -1
  2. package/skills/apc-context/SKILL.md +5 -2
  3. package/skills/apx/SKILL.md +3 -3
  4. package/skills/apx-agency-agents/SKILL.md +5 -5
  5. package/skills/apx-agent/SKILL.md +7 -7
  6. package/skills/apx-mcp/SKILL.md +6 -4
  7. package/skills/apx-mcp-builder/SKILL.md +4 -7
  8. package/skills/apx-project/SKILL.md +4 -5
  9. package/skills/apx-routine/SKILL.md +14 -12
  10. package/skills/apx-runtime/SKILL.md +5 -3
  11. package/skills/apx-sessions/SKILL.md +5 -5
  12. package/skills/apx-skill-builder/SKILL.md +10 -6
  13. package/skills/apx-task/SKILL.md +8 -8
  14. package/skills/apx-telegram/SKILL.md +23 -7
  15. package/skills/apx-voice/SKILL.md +8 -6
  16. package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
  17. package/src/core/agent/index.js +0 -2
  18. package/src/core/{agent-memory.js → agent/memory.js} +2 -2
  19. package/src/core/agent/model-router.js +21 -43
  20. package/src/core/agent/prompt-builder.js +17 -63
  21. package/src/core/agent/prompts/action-discipline.md +17 -0
  22. package/src/core/agent/prompts/channels/code.md +8 -12
  23. package/src/core/agent/prompts/channels/desktop.md +6 -4
  24. package/src/core/agent/prompts/channels/routine.md +10 -1
  25. package/src/core/agent/prompts/channels/telegram.md +5 -0
  26. package/src/core/agent/prompts/channels/web_code.md +20 -0
  27. package/src/core/agent/prompts/modes/voice.md +2 -2
  28. package/src/core/agent/prompts/super-agent-base.md +2 -2
  29. package/src/core/agent/run-agent.js +37 -35
  30. package/src/core/agent/runtime-bridge.js +42 -0
  31. package/src/core/agent/self-memory.js +19 -9
  32. package/src/core/agent/skills/catalog.js +65 -0
  33. package/src/core/agent/skills/index.js +6 -0
  34. package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
  35. package/src/core/agent/skills/rag.js +91 -0
  36. package/src/core/agent/skills/trigger.js +71 -0
  37. package/src/{host/daemon → core/agent}/super-agent.js +5 -5
  38. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
  39. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
  40. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
  41. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
  42. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
  43. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
  44. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
  45. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
  46. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
  47. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
  48. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
  49. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
  50. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
  51. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
  52. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
  53. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
  54. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
  55. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
  56. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
  57. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
  58. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
  59. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
  60. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
  61. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
  62. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
  63. package/src/core/agent/tools/helpers.js +74 -0
  64. package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
  65. package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
  66. package/src/core/apc/agents-vault.js +37 -0
  67. package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
  68. package/src/core/{config.js → config/index.js} +21 -27
  69. package/src/core/config/paths.js +32 -0
  70. package/src/core/constants/actors.js +8 -0
  71. package/src/core/constants/channels.js +19 -0
  72. package/src/core/constants/index.js +5 -0
  73. package/src/core/constants/permissions.js +17 -0
  74. package/src/core/constants/roles.js +9 -0
  75. package/src/core/engines/_streaming.js +63 -0
  76. package/src/core/engines/anthropic.js +11 -22
  77. package/src/core/engines/ollama.js +7 -16
  78. package/src/core/identity/index.js +8 -0
  79. package/src/core/{identity.js → identity/self.js} +5 -5
  80. package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
  81. package/src/core/logging.js +1 -1
  82. package/src/core/mascot.js +1 -1
  83. package/src/core/memory/active-threads.js +10 -10
  84. package/src/core/memory/broker.js +9 -9
  85. package/src/core/memory/compactor.js +2 -2
  86. package/src/core/memory/index.js +2 -2
  87. package/src/core/memory/indexer.js +1 -1
  88. package/src/core/{code-sessions-store.js → stores/code-sessions.js} +3 -7
  89. package/src/core/{messages-store.js → stores/messages.js} +6 -4
  90. package/src/core/stores/routine-memory.js +71 -0
  91. package/src/core/{routines-store.js → stores/routines.js} +1 -3
  92. package/src/core/stores/runtime-sessions.js +99 -0
  93. package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
  94. package/src/core/update-check.js +1 -1
  95. package/src/core/util/ids.js +14 -0
  96. package/src/core/util/index.js +2 -0
  97. package/src/core/util/text-similarity.js +52 -0
  98. package/src/core/util/time.js +9 -0
  99. package/src/core/voice/tts.js +1 -1
  100. package/src/host/daemon/api/admin-config.js +4 -3
  101. package/src/host/daemon/api/admin.js +1 -1
  102. package/src/host/daemon/api/agents.js +4 -25
  103. package/src/host/daemon/api/artifacts.js +1 -1
  104. package/src/host/daemon/api/code.js +48 -16
  105. package/src/host/daemon/api/confirm.js +1 -1
  106. package/src/host/daemon/api/connections.js +2 -2
  107. package/src/host/daemon/api/conversations.js +2 -2
  108. package/src/host/daemon/api/deck.js +1 -1
  109. package/src/host/daemon/api/desktop.js +1 -1
  110. package/src/host/daemon/api/embeddings.js +4 -4
  111. package/src/host/daemon/api/engines.js +2 -2
  112. package/src/host/daemon/api/exec.js +3 -3
  113. package/src/host/daemon/api/identity.js +1 -1
  114. package/src/host/daemon/api/mcps.js +1 -1
  115. package/src/host/daemon/api/messages.js +1 -1
  116. package/src/host/daemon/api/runtimes.js +9 -8
  117. package/src/host/daemon/api/sessions-search.js +1 -1
  118. package/src/host/daemon/api/sessions.js +2 -2
  119. package/src/host/daemon/api/shared.js +5 -4
  120. package/src/host/daemon/api/skills.js +30 -0
  121. package/src/host/daemon/api/super-agent.js +29 -9
  122. package/src/host/daemon/api/tasks.js +2 -2
  123. package/src/host/daemon/api/telegram.js +1 -1
  124. package/src/host/daemon/api/tools.js +6 -6
  125. package/src/host/daemon/api/tts.js +2 -2
  126. package/src/host/daemon/api/voice.js +14 -12
  127. package/src/host/daemon/api.js +2 -0
  128. package/src/host/daemon/compact.js +1 -1
  129. package/src/host/daemon/db.js +4 -4
  130. package/src/host/daemon/desktop-ws.js +1 -1
  131. package/src/host/daemon/index.js +4 -4
  132. package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +11 -6
  133. package/src/host/daemon/plugins/index.js +2 -2
  134. package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +66 -195
  135. package/src/host/daemon/plugins/telegram/media.js +162 -0
  136. package/src/host/daemon/projects-helpers.js +54 -0
  137. package/src/host/daemon/routines.js +28 -12
  138. package/src/host/daemon/smoke.js +2 -2
  139. package/src/host/daemon/token-store.js +1 -1
  140. package/src/host/daemon/transcription.js +2 -2
  141. package/src/host/daemon/wakeup.js +2 -2
  142. package/src/interfaces/cli/commands/agent.js +3 -3
  143. package/src/interfaces/cli/commands/command.js +1 -1
  144. package/src/interfaces/cli/commands/config.js +3 -2
  145. package/src/interfaces/cli/commands/desktop.js +1 -1
  146. package/src/interfaces/cli/commands/exec.js +2 -1
  147. package/src/interfaces/cli/commands/identity.js +2 -2
  148. package/src/interfaces/cli/commands/init.js +1 -1
  149. package/src/interfaces/cli/commands/mcp.js +1 -1
  150. package/src/interfaces/cli/commands/memory.js +2 -2
  151. package/src/interfaces/cli/commands/model.js +16 -6
  152. package/src/interfaces/cli/commands/project.js +1 -1
  153. package/src/interfaces/cli/commands/routine.js +58 -0
  154. package/src/interfaces/cli/commands/search.js +1 -1
  155. package/src/interfaces/cli/commands/session.js +4 -4
  156. package/src/interfaces/cli/commands/setup.js +4 -3
  157. package/src/interfaces/cli/commands/skills.js +25 -4
  158. package/src/interfaces/cli/commands/status.js +1 -1
  159. package/src/interfaces/cli/commands/sys.js +11 -4
  160. package/src/interfaces/cli/commands/update.js +1 -1
  161. package/src/interfaces/cli/index.js +4 -4
  162. package/src/interfaces/cli/postinstall.js +2 -2
  163. package/src/interfaces/mcp-server/index.js +1 -1
  164. package/src/interfaces/tui/component/prompt/index.tsx +3 -1
  165. package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
  166. package/src/interfaces/tui/context/sync-apx.tsx +20 -2
  167. package/src/interfaces/tui/context/sync.tsx +2 -1
  168. package/src/interfaces/tui/routes/session/index.tsx +151 -136
  169. package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
  170. package/src/interfaces/tui/run.ts +2 -0
  171. package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +1 -0
  172. package/src/interfaces/web/dist/assets/index-BkybwwRn.js +570 -0
  173. package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +1 -0
  174. package/src/interfaces/web/dist/index.html +2 -2
  175. package/src/interfaces/web/package-lock.json +3 -3
  176. package/src/interfaces/web/src/App.tsx +51 -32
  177. package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
  178. package/src/interfaces/web/src/components/UiSelect.tsx +1 -1
  179. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
  180. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
  181. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +33 -18
  182. package/src/interfaces/web/src/components/common/TabLayout.tsx +9 -5
  183. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
  184. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
  185. package/src/interfaces/web/src/hooks/useChat.ts +47 -2
  186. package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +59 -0
  187. package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
  188. package/src/interfaces/web/src/i18n/en.ts +7 -7
  189. package/src/interfaces/web/src/i18n/es.ts +7 -7
  190. package/src/interfaces/web/src/lib/api/skills.ts +25 -0
  191. package/src/interfaces/web/src/lib/api.ts +1 -0
  192. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +18 -18
  193. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
  194. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
  195. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
  196. package/src/interfaces/web/src/screens/project/ChatTab.tsx +12 -9
  197. package/src/skills/apc-context/SKILL.md +159 -0
  198. package/src/core/agent/ghost-guard.js +0 -24
  199. package/src/core/agent/prompts/channels/terminal.md +0 -16
  200. package/src/host/daemon/apc-runtime-context.js +0 -124
  201. package/src/host/daemon/super-agent-tools/helpers.js +0 -124
  202. package/src/host/daemon/tool-call-parser.js +0 -2
  203. package/src/interfaces/web/dist/assets/index-63P_ji1a.js +0 -571
  204. package/src/interfaces/web/dist/assets/index-63P_ji1a.js.map +0 -1
  205. package/src/interfaces/web/dist/assets/index-DLWy6dYz.css +0 -1
  206. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/ask-questions.js +0 -0
  207. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
  208. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
  209. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
  210. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
  211. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
  212. /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
  213. /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
  214. /package/src/core/{parser.js → apc/parser.js} +0 -0
  215. /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
  216. /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
  217. /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
  218. /package/src/core/{session-store.js → stores/sessions.js} +0 -0
  219. /package/src/host/daemon/plugins/{telegram-ask.js → telegram/ask.js} +0 -0
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
18
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
20
  import { z } from "zod";
21
- import { findApfRoot } from "../../core/parser.js";
21
+ import { findApfRoot } from "#core/apc/parser.js";
22
22
  import { ensureDaemon, http } from "../../cli/http.js";
23
23
 
24
24
  const __filename = fileURLToPath(import.meta.url);
@@ -1569,7 +1569,9 @@ export function Prompt(props: PromptProps) {
1569
1569
  {(agent) => (
1570
1570
  <>
1571
1571
  <text fg={fadeColor(highlight(), agentMetaAlpha())}>
1572
- {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}
1572
+ {/* APX: show the coding mode (Build) here, not the agent
1573
+ the agent (Roby) is shown in the sidebar. */}
1574
+ {store.mode === "shell" ? "Shell" : "Build"}
1573
1575
  </text>
1574
1576
  <Show when={store.mode === "normal"}>
1575
1577
  <box flexDirection="row" gap={1}>
@@ -4,10 +4,23 @@ import { onCleanup } from "solid-js"
4
4
  import fs from "node:fs"
5
5
  import os from "node:os"
6
6
  import path from "node:path"
7
- import { spawn } from "node:child_process"
7
+ import { spawn, execSync } from "node:child_process"
8
8
 
9
9
  const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token")
10
10
 
11
+ /** Current git branch for `dir`, or "" when not a repo. Cheap, best-effort. */
12
+ function gitBranch(dir: string): string {
13
+ try {
14
+ return execSync("git rev-parse --abbrev-ref HEAD", {
15
+ cwd: dir,
16
+ stdio: ["ignore", "pipe", "ignore"],
17
+ encoding: "utf8",
18
+ }).trim()
19
+ } catch {
20
+ return ""
21
+ }
22
+ }
23
+
11
24
  function readToken(): string {
12
25
  try {
13
26
  return fs.readFileSync(TOKEN_PATH, "utf8").trim()
@@ -74,10 +87,21 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
74
87
  prompt: string,
75
88
  previousMessages: Array<{ role: string; content: string }> = [],
76
89
  ) {
77
- // Send the live model selection as `body.model` so the daemon's
78
- // super-agent honours the model the user picked via /models. When unset
79
- // the daemon falls back to ~/.apx/config.json (super_agent.model).
80
- const body: Record<string, unknown> = { prompt, previousMessages }
90
+ // Run on the `code` channel and hand the daemon our working directory so
91
+ // the agent knows WHERE it is (CWD/project) otherwise it falls back to
92
+ // the generic API channel with no cwd and asks "which file? which project?".
93
+ // maxIters gives room to chain read→edit→verify; the code.md prompt already
94
+ // carries the "keep going until done" guidance. We deliberately do NOT send
95
+ // completionContract here — on weaker models (e.g. gemini-flash) the hard
96
+ // loop-until-finish contract causes runaway edit/rewrite loops.
97
+ const body: Record<string, unknown> = {
98
+ prompt,
99
+ previousMessages,
100
+ channel: "code",
101
+ channelMeta: { cwd: props.directory ?? process.cwd() },
102
+ maxIters: 40,
103
+ maxTokens: 8192,
104
+ }
81
105
  if (currentModel) body.model = currentModel
82
106
  const res = await fetch(`${props.url}/projects/${props.pid}/super-agent/chat/stream`, {
83
107
  method: "POST",
@@ -101,7 +125,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
101
125
  const reader = res.body.getReader()
102
126
  const dec = new TextDecoder()
103
127
  let buf = ""
104
- while (true) {
128
+ // The daemon may keep the HTTP connection open after the final event, so
129
+ // we can't rely on stream-close to know the turn is done. Resolve as soon
130
+ // as we see `final` or `error` — otherwise the caller's `await` hangs and
131
+ // the next message queues forever.
132
+ let finished = false
133
+ while (!finished) {
105
134
  const { done, value } = await reader.read()
106
135
  if (done) break
107
136
  buf += dec.decode(value, { stream: true })
@@ -158,16 +187,25 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
158
187
  usage: ev.result?.usage,
159
188
  name: ev.result?.name,
160
189
  })
190
+ finished = true
161
191
  break
162
192
  case "error":
163
193
  emitter.emit("event", { type: "error", sessionID, error: ev.error })
194
+ finished = true
164
195
  break
165
196
  }
166
197
  } catch {
167
198
  // ignore parse errors for partial lines
168
199
  }
200
+ if (finished) break
169
201
  }
170
202
  }
203
+ // Stop reading and release the connection so the awaiting caller resumes.
204
+ try {
205
+ await reader.cancel()
206
+ } catch {
207
+ /* already closed */
208
+ }
171
209
  }
172
210
 
173
211
  // The APX daemon has no generic "create session" route — a chat turn is
@@ -296,8 +334,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
296
334
  // passed, show "APX" in the sidebar.
297
335
  agent: props.agent ?? "APX",
298
336
  model: props.model ?? "claude-3-5-sonnet",
299
- // Legacy opencode compat
337
+ // Working directory (the user's project root, passed via --cwd) and its
338
+ // current git branch — shown in the sidebar / footer like OpenCode.
300
339
  directory: props.directory ?? process.cwd(),
340
+ branch: gitBranch(props.directory ?? process.cwd()),
301
341
  event: emitter,
302
342
  client,
303
343
  streamChat,
@@ -45,6 +45,10 @@ export interface ApxMessage {
45
45
  /** shell: process exit code (undefined while running) */
46
46
  exitCode?: number | null
47
47
  createdAt: number
48
+ /** assistant: model that produced the turn (from model_start). */
49
+ model?: string
50
+ /** assistant: when the final answer landed (for response-time display). */
51
+ completedAt?: number
48
52
  }
49
53
 
50
54
  export interface ApxUsage {
@@ -133,9 +137,11 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
133
137
  pushUser(event.sessionID, event.text)
134
138
  break
135
139
 
136
- case "model_start":
137
- ensureAssistant(event.sessionID)
140
+ case "model_start": {
141
+ const id = ensureAssistant(event.sessionID)
142
+ if (event.model) patchAssistant(event.sessionID, id, (msg) => { if (!msg.model) msg.model = event.model })
138
143
  break
144
+ }
139
145
 
140
146
  case "assistant_text": {
141
147
  if (!event.text) break
@@ -205,6 +211,7 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
205
211
  const id = activeAssistant.id
206
212
  patchAssistant(sessionID, id, (msg) => {
207
213
  msg.streaming = false
214
+ msg.completedAt = Date.now()
208
215
  const parts = msg.parts ?? (msg.parts = [])
209
216
  const finalText = (event.text ?? "").trim()
210
217
  const lastText = [...parts].reverse().find((p) => p.kind === "text") as
@@ -376,6 +383,17 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
376
383
  }
377
384
 
378
385
  return {
386
+ // The opencode sync shim (context/sync.tsx) reads these. apx has no async
387
+ // bootstrap / message-fetch, so we're always "ready". `data.messages` is
388
+ // only consumed by the Prompt's cost footer, which expects opencode's
389
+ // Message shape (item.tokens, providerID, …) that ApxMessage doesn't have.
390
+ // We render our own message list from session.messages(), so an empty map
391
+ // here is correct and avoids the Prompt crashing on missing fields.
392
+ status: "ready" as const,
393
+ ready: true,
394
+ data: {
395
+ messages: {} as Record<string, ApxMessage[]>,
396
+ },
379
397
  session: {
380
398
  current: currentSession,
381
399
  messages: messagesFor,
@@ -40,7 +40,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
40
40
  const apxModelKey = () => args.model || "apx-default"
41
41
  const apxProvider = () => ({
42
42
  id: "apx",
43
- name: "APX",
43
+ // Shown as the gray label in the prompt status line: "Build · model · APX Code".
44
+ name: "APX Code",
44
45
  models: {
45
46
  [apxModelKey()]: {
46
47
  id: apxModelKey(),
@@ -10,18 +10,18 @@
10
10
  *
11
11
  * The APX-tailored sidebar shows session / agent / model / context usage.
12
12
  */
13
- import { For, Show, createMemo, createSignal, createEffect, onCleanup } from "solid-js"
14
- import { TextareaRenderable, MouseButton } from "@opentui/core"
13
+ import { For, Show, createMemo, createEffect, onCleanup } from "solid-js"
14
+ import { MouseButton } from "@opentui/core"
15
15
  import { useTerminalDimensions } from "@opentui/solid"
16
16
  import { useTheme } from "@tui/context/theme"
17
17
  import { useRoute } from "@tui/context/route"
18
18
  import { useApxSync } from "@tui/context/sync-apx"
19
19
  import { useSDK } from "@tui/context/sdk-apx"
20
20
  import { useLocal } from "@tui/context/local"
21
- import { useToast, Toast } from "@tui/ui/toast"
21
+ import { Toast } from "@tui/ui/toast"
22
22
  import { useDialog } from "@tui/ui/dialog"
23
- import { useExit } from "@tui/context/exit"
24
23
  import { usePromptRef } from "@tui/context/prompt"
24
+ import { Prompt, type PromptRef } from "@tui/component/prompt"
25
25
  import type { ApxMessage, ApxPart } from "@tui/context/sync-apx"
26
26
  import { SidebarApx } from "./sidebar-apx"
27
27
  import { MessageActions } from "./message-actions"
@@ -39,36 +39,80 @@ function parseError(raw: string): { message: string; trace?: string; hint?: stri
39
39
  return { message, trace, hint }
40
40
  }
41
41
 
42
- /** One-line summary of a tool call, e.g. `read src/app.tsx` or `glob "**​/*.ts"`. */
43
- function toolSummary(part: Extract<ApxPart, { kind: "tool" }>): string {
44
- const a = part.args
45
- let detail = ""
46
- if (a && typeof a === "object") {
47
- detail =
48
- a.filePath ?? a.path ?? a.file ?? a.pattern ?? a.query ?? a.command ?? a.url ?? a.description ?? ""
49
- if (!detail) {
50
- const first = Object.values(a).find((v) => typeof v === "string") as string | undefined
51
- detail = first ?? ""
52
- }
53
- } else if (typeof a === "string") {
54
- detail = a
42
+ // Coding modes (mirrors web Code). Only Build is wired today; Plan/Zen are
43
+ // shown as the active label is "Build" until a mode toggle lands.
44
+ const MODES = ["Build", "Plan", "Zen"] as const
45
+
46
+ const TOOL_LABELS: Record<string, string> = {
47
+ read_file: "Read",
48
+ write_file: "Write",
49
+ edit_file: "Edit",
50
+ search_files: "Search",
51
+ list_files: "List",
52
+ run_shell: "Shell",
53
+ load_skill: "Skill",
54
+ }
55
+
56
+ const MAX_DIFF_LINES = 24
57
+
58
+ /** Git-style diff block: removed lines in red (-), added lines in green (+). */
59
+ function DiffBlock(props: { search?: string; replace?: string; content?: string }) {
60
+ const { theme } = useTheme()
61
+ const removed = () => (props.search ? props.search.replace(/\n$/, "").split("\n") : [])
62
+ const added = () => ((props.replace ?? props.content) ? (props.replace ?? props.content ?? "").replace(/\n$/, "").split("\n") : [])
63
+ const shown = () => {
64
+ const r = removed().map((t) => ({ sign: "-", t, color: theme.error }))
65
+ const a = added().map((t) => ({ sign: "+", t, color: theme.success }))
66
+ const all = [...r, ...a]
67
+ return all.length > MAX_DIFF_LINES
68
+ ? [...all.slice(0, MAX_DIFF_LINES), { sign: " ", t: `… ${all.length - MAX_DIFF_LINES} more lines`, color: theme.textMuted }]
69
+ : all
55
70
  }
56
- if (detail.length > 60) detail = detail.slice(0, 57) + "…"
57
- return detail ? `${part.name} ${detail}` : part.name
71
+ return (
72
+ <box flexDirection="column" marginLeft={2} marginTop={0} backgroundColor={theme.backgroundPanel} paddingLeft={1} paddingRight={1}>
73
+ <For each={shown()}>
74
+ {(line) => (
75
+ <text color={line.color}>
76
+ {line.sign} {line.t}
77
+ </text>
78
+ )}
79
+ </For>
80
+ </box>
81
+ )
58
82
  }
59
83
 
60
84
  function ToolPart(props: { part: Extract<ApxPart, { kind: "tool" }> }) {
61
85
  const { theme } = useTheme()
62
86
  const color = () => (props.part.running ? theme.warning ?? theme.primary : props.part.ok === false ? theme.error : theme.success)
63
87
  const marker = () => (props.part.running ? "▸" : props.part.ok === false ? "✗" : "→")
88
+ const label = () => TOOL_LABELS[props.part.name] ?? props.part.name
89
+ const a = () => (props.part.args && typeof props.part.args === "object" ? (props.part.args as any) : {})
90
+ const target = () => a().path ?? a().filePath ?? a().file ?? a().pattern ?? a().query ?? a().command ?? ""
91
+ const isEdit = () => props.part.name === "edit_file" && (a().search || a().replace)
92
+ const isWrite = () => props.part.name === "write_file" && a().content
64
93
  return (
65
- <text color={color()}>
66
- {marker()} {toolSummary(props.part)}
67
- </text>
94
+ <box flexDirection="column">
95
+ <text color={color()}>
96
+ {marker()} {label()}
97
+ {target() ? " " + (String(target()).length > 60 ? String(target()).slice(0, 57) + "…" : target()) : ""}
98
+ </text>
99
+ <Show when={isEdit()}>
100
+ <DiffBlock search={a().search} replace={a().replace} />
101
+ </Show>
102
+ <Show when={isWrite()}>
103
+ <DiffBlock content={a().content} />
104
+ </Show>
105
+ </box>
68
106
  )
69
107
  }
70
108
 
71
- function AssistantBubble(props: { msg: ApxMessage; onActivate: () => void }) {
109
+ function AssistantBubble(props: {
110
+ msg: ApxMessage
111
+ onActivate: () => void
112
+ agentName: string
113
+ modelLabel: string
114
+ mode: string
115
+ }) {
72
116
  const { theme, syntax } = useTheme()
73
117
  const parts = createMemo<ApxPart[]>(() => {
74
118
  const p = props.msg.parts
@@ -77,9 +121,19 @@ function AssistantBubble(props: { msg: ApxMessage; onActivate: () => void }) {
77
121
  return props.msg.text ? [{ kind: "text", text: props.msg.text }] : []
78
122
  })
79
123
  const empty = () => parts().length === 0
124
+ // OpenCode-style meta line shown after the answer: mode · model · response time.
125
+ const elapsed = () => {
126
+ if (!props.msg.completedAt) return ""
127
+ return `${((props.msg.completedAt - props.msg.createdAt) / 1000).toFixed(1)}s`
128
+ }
129
+ const meta = () => {
130
+ const parts = [props.mode, props.msg.model || props.modelLabel, elapsed()].filter(Boolean)
131
+ return parts.join(" · ")
132
+ }
80
133
  return (
81
134
  <box
82
135
  flexDirection="column"
136
+ marginTop={1}
83
137
  marginBottom={1}
84
138
  paddingLeft={2}
85
139
  paddingRight={2}
@@ -88,8 +142,8 @@ function AssistantBubble(props: { msg: ApxMessage; onActivate: () => void }) {
88
142
  if (e?.button === undefined || e.button === MouseButton.LEFT) props.onActivate()
89
143
  }}
90
144
  >
91
- <text color={theme.success} bold>
92
- {props.msg.streaming ? `${props.msg.role === "assistant" ? "Assistant" : "Assistant"} ▸` : "Assistant"}
145
+ <text color={theme.success} bold marginBottom={1}>
146
+ {props.msg.streaming ? `${props.agentName} ▸` : props.agentName}
93
147
  </text>
94
148
  <Show when={!empty()} fallback={<text color={theme.textMuted}>…</text>}>
95
149
  <box flexDirection="column">
@@ -120,6 +174,12 @@ function AssistantBubble(props: { msg: ApxMessage; onActivate: () => void }) {
120
174
  </For>
121
175
  </box>
122
176
  </Show>
177
+ <Show when={!props.msg.streaming && !props.msg.error}>
178
+ <box flexDirection="row" marginTop={1}>
179
+ <text color={theme.primary}>■ </text>
180
+ <text color={theme.textMuted}>{meta()}</text>
181
+ </box>
182
+ </Show>
123
183
  </box>
124
184
  )
125
185
  }
@@ -127,19 +187,31 @@ function AssistantBubble(props: { msg: ApxMessage; onActivate: () => void }) {
127
187
  function UserBubble(props: { msg: ApxMessage; onActivate: () => void }) {
128
188
  const { theme } = useTheme()
129
189
  const queued = () => props.msg.queued === true
190
+ const accent = () => (queued() ? theme.textMuted : theme.primary)
130
191
  return (
131
192
  <box
132
193
  flexDirection="row"
194
+ marginTop={1}
133
195
  marginBottom={1}
196
+ paddingLeft={2}
134
197
  paddingRight={2}
135
198
  onMouseDown={(e: any) => {
136
199
  // Left click (or plain click w/o button info) opens Message Actions.
137
200
  if (e?.button === undefined || e.button === MouseButton.LEFT) props.onActivate()
138
201
  }}
139
202
  >
140
- {/* left accent bar */}
141
- <box width={1} backgroundColor={queued() ? theme.textMuted : theme.primary} flexShrink={0} />
142
- <box flexDirection="column" flexGrow={1} paddingLeft={1} backgroundColor={theme.backgroundElement}>
203
+ {/* single colored accent bar on the left + filled background (OpenCode style) */}
204
+ <box width={1} backgroundColor={accent()} flexShrink={0} />
205
+ <box
206
+ flexDirection="column"
207
+ flexGrow={1}
208
+ minWidth={0}
209
+ paddingLeft={2}
210
+ paddingRight={2}
211
+ paddingTop={1}
212
+ paddingBottom={1}
213
+ backgroundColor={theme.backgroundElement}
214
+ >
143
215
  <text color={queued() ? theme.textMuted : theme.text} wrap>
144
216
  {props.msg.text}
145
217
  </text>
@@ -197,12 +269,12 @@ export function Session() {
197
269
  const sync = useApxSync()
198
270
  const sdk = useSDK()
199
271
  const local = useLocal()
200
- const toast = useToast()
201
272
  const dialog = useDialog()
202
- const exit = useExit()
203
273
  const promptRef = usePromptRef()
204
- const [sending, setSending] = createSignal(false)
205
- let inputEl: TextareaRenderable | undefined
274
+
275
+ // Show the sidebar only on wide terminals; on narrow ones the chat keeps
276
+ // full width (the directory/branch live in the sidebar, shown when wide).
277
+ const wide = createMemo(() => dims().width >= 100)
206
278
 
207
279
  // Bridge the user's /models selection into the SDK so the next turn uses it.
208
280
  createEffect(() => {
@@ -215,8 +287,27 @@ export function Session() {
215
287
  return sync.session.current() ?? ""
216
288
  })
217
289
 
290
+ // Keep the sync store's "current session" pinned to the session we're viewing
291
+ // so the sidebar/usage track the right bucket.
292
+ createEffect(() => {
293
+ const id = sessionID()
294
+ if (id) sync.session.setCurrent(id)
295
+ })
296
+
218
297
  const messages = createMemo(() => sync.session.messages(sessionID()))
219
298
 
299
+ // Active mode + model, shown after each answer (mode · model · response time).
300
+ const mode = createMemo(() => MODES[0]) // Build (Plan/Zen toggle: future work)
301
+ const modelLabel = createMemo(() => {
302
+ const parsed = local.model?.parsed?.()
303
+ if (parsed?.modelID) return parsed.providerID ? `${parsed.providerID}:${parsed.modelID}` : parsed.modelID
304
+ return sdk.model || "—"
305
+ })
306
+ const agentName = createMemo(() => {
307
+ const a = sdk.agent || "Assistant"
308
+ return a.charAt(0).toUpperCase() + a.slice(1)
309
+ })
310
+
220
311
  onCleanup(() => {
221
312
  promptRef.set(undefined)
222
313
  })
@@ -225,78 +316,6 @@ export function Session() {
225
316
  dialog.replace(() => <MessageActions sessionID={sessionID()} message={msg} />)
226
317
  }
227
318
 
228
- function makeRef(r: TextareaRenderable) {
229
- return {
230
- get focused() {
231
- return r.focused
232
- },
233
- get current() {
234
- return { input: r.plainText, parts: [] as any[] }
235
- },
236
- set(prompt: { input: string; parts: any[] }) {
237
- r.setText(prompt.input)
238
- },
239
- reset() {
240
- r.clear()
241
- },
242
- blur() {
243
- r.blur()
244
- },
245
- focus() {
246
- r.focus()
247
- },
248
- submit() {
249
- void handleSubmit()
250
- },
251
- }
252
- }
253
-
254
- async function handleSubmit() {
255
- if (!inputEl) return
256
- const text = inputEl.plainText.trim()
257
- if (!text) return
258
-
259
- if (text === "exit" || text === "quit" || text === ":q") {
260
- void exit()
261
- return
262
- }
263
-
264
- inputEl.clear()
265
-
266
- // Shell command
267
- if (text.startsWith("!") && text.length > 1) {
268
- try {
269
- await sync.runShell(text.slice(1).trim())
270
- } catch (e) {
271
- toast.error(e instanceof Error ? e : new Error(String(e)))
272
- }
273
- return
274
- }
275
-
276
- // A turn is already in flight → queue it (OpenCode behaviour).
277
- if (sending()) {
278
- const id = sync.queueMessage(text)
279
- if (!id) toast.show({ message: "No hay sesión activa todavía", variant: "warning" })
280
- else toast.show({ message: "Mensaje en cola", variant: "info" })
281
- return
282
- }
283
-
284
- setSending(true)
285
- try {
286
- await sync.sendMessage(text)
287
- // Flush any messages queued while this turn was streaming.
288
- let next = messages().find((m) => m.queued)
289
- while (next) {
290
- await sync.sendQueued(sessionID(), next.id)
291
- next = messages().find((m) => m.queued)
292
- }
293
- } catch (e) {
294
- toast.error(e instanceof Error ? e : new Error(String(e)))
295
- } finally {
296
- setSending(false)
297
- }
298
- }
299
-
300
319
  return (
301
320
  <box flexDirection="column" flexGrow={1} width={dims().width} height={dims().height}>
302
321
  <box flexDirection="row" flexGrow={1} minHeight={0}>
@@ -319,7 +338,15 @@ export function Session() {
319
338
  if (msg.role === "user") return <UserBubble msg={msg} onActivate={() => openActions(msg)} />
320
339
  if (msg.role === "shell") return <ShellBubble msg={msg} />
321
340
  if (msg.error) return <ErrorBubble msg={msg} />
322
- return <AssistantBubble msg={msg} onActivate={() => openActions(msg)} />
341
+ return (
342
+ <AssistantBubble
343
+ msg={msg}
344
+ onActivate={() => openActions(msg)}
345
+ agentName={agentName()}
346
+ modelLabel={modelLabel()}
347
+ mode={mode()}
348
+ />
349
+ )
323
350
  }}
324
351
  </For>
325
352
  </Show>
@@ -327,40 +354,28 @@ export function Session() {
327
354
  </box>
328
355
  </scrollbox>
329
356
 
330
- {/* Input area */}
331
- <box flexShrink={0} flexDirection="column" borderTop={1} borderColor={theme.border} backgroundColor={theme.backgroundElement}>
332
- <box paddingLeft={2} paddingRight={2} paddingTop={1}>
333
- <textarea
334
- ref={(r: TextareaRenderable) => {
335
- inputEl = r
336
- promptRef.set(makeRef(r))
337
- }}
338
- placeholder={sending() ? "Streaming… (enter to queue)" : "Ask anything... (prefix ! to run shell, e.g. !ls)"}
339
- placeholderColor={theme.textMuted}
340
- textColor={theme.text}
341
- focusedTextColor={theme.text}
342
- minHeight={1}
343
- maxHeight={6}
344
- onSubmit={() => {
345
- setTimeout(() => setTimeout(() => handleSubmit(), 0), 0)
346
- }}
347
- />
348
- </box>
349
- <box height={1} paddingLeft={2} paddingRight={2} justifyContent="space-between" flexDirection="row">
350
- <Show
351
- when={sending()}
352
- fallback={<text color={theme.textMuted}>enter send · ! shell · click msg for actions · exit quit</text>}
353
- >
354
- <text color={theme.warning ?? theme.primary} italic>
355
- ▸ Streaming… (enter queues your next message)
356
- </text>
357
- </Show>
358
- </box>
357
+ {/* Input the OpenCode prompt component (colored box, Build/Plan mode
358
+ selector, model label, Enter-to-send). With sessionID set it streams
359
+ to the *current* session via session.prompt instead of creating a
360
+ new one. This is also what fixes Enter submission and focus, since
361
+ the prompt owns the keymap submit wiring. */}
362
+ <box flexShrink={0} paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={1}>
363
+ <Prompt
364
+ sessionID={sessionID()}
365
+ visible={true}
366
+ ref={(r?: PromptRef) => promptRef.set(r)}
367
+ placeholders={{
368
+ normal: ["Ask anything…", "Fix a TODO in the codebase", "Explain this code"],
369
+ shell: ["ls -la", "git status", "pwd"],
370
+ }}
371
+ />
359
372
  </box>
360
373
  </box>
361
374
 
362
- {/* Sidebar */}
363
- <SidebarApx sessionID={sessionID()} />
375
+ {/* Sidebar — only on wide terminals; carries the directory + branch */}
376
+ <Show when={wide()}>
377
+ <SidebarApx sessionID={sessionID()} />
378
+ </Show>
364
379
  </box>
365
380
  <Toast />
366
381
  </box>