@agentprojectcontext/apx 1.15.5 → 1.16.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.
Files changed (222) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/wakeup.js +14 -19
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,100 @@
1
+ import { cmd } from "../cmd"
2
+ import { UI } from "@/cli/ui"
3
+ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
4
+ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
5
+ import { errorMessage } from "@/util/error"
6
+ import { validateSession } from "./validate-session"
7
+ import { ServerAuth } from "@/server/auth"
8
+
9
+ export const AttachCommand = cmd({
10
+ command: "attach <url>",
11
+ describe: "attach to a running opencode server",
12
+ builder: (yargs) =>
13
+ yargs
14
+ .positional("url", {
15
+ type: "string",
16
+ describe: "http://localhost:4096",
17
+ demandOption: true,
18
+ })
19
+ .option("dir", {
20
+ type: "string",
21
+ description: "directory to run in",
22
+ })
23
+ .option("continue", {
24
+ alias: ["c"],
25
+ describe: "continue the last session",
26
+ type: "boolean",
27
+ })
28
+ .option("session", {
29
+ alias: ["s"],
30
+ type: "string",
31
+ describe: "session id to continue",
32
+ })
33
+ .option("fork", {
34
+ type: "boolean",
35
+ describe: "fork the session when continuing (use with --continue or --session)",
36
+ })
37
+ .option("password", {
38
+ alias: ["p"],
39
+ type: "string",
40
+ describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
41
+ })
42
+ .option("username", {
43
+ alias: ["u"],
44
+ type: "string",
45
+ describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
46
+ }),
47
+ handler: async (args) => {
48
+ const unguard = win32InstallCtrlCGuard()
49
+ try {
50
+ win32DisableProcessedInput()
51
+
52
+ if (args.fork && !args.continue && !args.session) {
53
+ UI.error("--fork requires --continue or --session")
54
+ process.exitCode = 1
55
+ return
56
+ }
57
+
58
+ const directory = (() => {
59
+ if (!args.dir) return undefined
60
+ try {
61
+ process.chdir(args.dir)
62
+ return process.cwd()
63
+ } catch {
64
+ // If the directory doesn't exist locally (remote attach), pass it through.
65
+ return args.dir
66
+ }
67
+ })()
68
+ const headers = ServerAuth.headers({ password: args.password, username: args.username })
69
+ const config = await TuiConfig.get()
70
+ const { tui } = await import("./app")
71
+
72
+ try {
73
+ await validateSession({
74
+ url: args.url,
75
+ sessionID: args.session,
76
+ directory,
77
+ headers,
78
+ })
79
+ } catch (error) {
80
+ UI.error(errorMessage(error))
81
+ process.exitCode = 1
82
+ return
83
+ }
84
+
85
+ await tui({
86
+ url: args.url,
87
+ config,
88
+ args: {
89
+ continue: args.continue,
90
+ sessionID: args.session,
91
+ fork: args.fork,
92
+ },
93
+ directory,
94
+ headers,
95
+ })
96
+ } finally {
97
+ unguard?.()
98
+ }
99
+ },
100
+ })
@@ -0,0 +1,436 @@
1
+ import { OptimizedBuffer, RGBA, TextAttributes } from "@opentui/core"
2
+ import { go } from "@/cli/logo"
3
+
4
+ const PERIOD = 4600
5
+ const RINGS = 3
6
+ const WIDTH = 3.8
7
+ const TAIL = 9.5
8
+ const AMP = 0.55
9
+ const TAIL_AMP = 0.16
10
+ const BREATH_AMP = 0.05
11
+ const BREATH_SPEED = 0.0008
12
+ // Offset so the bg ring emits from the estimated GO center when the logo shimmer peaks.
13
+ const PHASE_OFFSET = 0.29
14
+ const LOGO_GAP = 1
15
+ const LOGO_TOP_BIAS = -1
16
+ const LOGO_LEFT_WIDTH = go.left[0]?.length ?? 0
17
+ const LOGO_LINES = go.left.map((line, index) => line + " ".repeat(LOGO_GAP) + go.right[index])
18
+ const LOGO_WIDTH = LOGO_LINES[0]?.length ?? 0
19
+ const LOGO_HEIGHT = LOGO_LINES.length
20
+ const SPACE = " ".codePointAt(0)!
21
+ const TOP_HALF = "▀".codePointAt(0)!
22
+ const FULL_BLOCK = "█".codePointAt(0)!
23
+ const RING_SCALE = 1 / RINGS
24
+ const TAIL_SCALE = 1 / TAIL
25
+ const LOGO_REACH = Math.hypot(LOGO_WIDTH, LOGO_HEIGHT * 2) + 3
26
+
27
+ const enum LogoCellKind {
28
+ Background,
29
+ Top,
30
+ ShadowTop,
31
+ Solid,
32
+ Char,
33
+ }
34
+
35
+ type LogoTemplateCell = {
36
+ x: number
37
+ y: number
38
+ kind: LogoCellKind
39
+ charCode: number
40
+ attributes: number
41
+ topDist: number
42
+ bottomDist: number
43
+ }
44
+
45
+ const LOGO_TEMPLATE: LogoTemplateCell[] = LOGO_LINES.flatMap((line, y) =>
46
+ Array.from(line)
47
+ .map((char, x) => {
48
+ if (char === " ") return
49
+ const kind =
50
+ char === "_"
51
+ ? LogoCellKind.Background
52
+ : char === "^"
53
+ ? LogoCellKind.Top
54
+ : char === "~"
55
+ ? LogoCellKind.ShadowTop
56
+ : char === "█"
57
+ ? LogoCellKind.Solid
58
+ : LogoCellKind.Char
59
+ return {
60
+ x,
61
+ y,
62
+ kind,
63
+ charCode: char.codePointAt(0) ?? SPACE,
64
+ attributes: x > LOGO_LEFT_WIDTH ? TextAttributes.BOLD : 0,
65
+ topDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 - LOGO_HEIGHT),
66
+ bottomDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 + 1 - LOGO_HEIGHT),
67
+ }
68
+ })
69
+ .filter((cell): cell is LogoTemplateCell => !!cell),
70
+ )
71
+
72
+ export type Rgb = [number, number, number]
73
+
74
+ export type GoUpsellArtRenderOptions = {
75
+ deltaTime?: number
76
+ rgb?: boolean
77
+ cache?: boolean
78
+ }
79
+
80
+ const CACHE_FRAME_COUNT = Math.round(PERIOD / (1000 / 30))
81
+ const CACHE_FRAMES_PER_RENDER = 1
82
+
83
+ export function toRgb(color: RGBA): Rgb {
84
+ const [r, g, b] = color.toInts()
85
+ return [r, g, b]
86
+ }
87
+
88
+ function clamp(n: number) {
89
+ return Math.max(0, Math.min(1, n))
90
+ }
91
+
92
+ function writeRgb(buffer: Uint16Array, offset: number, r: number, g: number, b: number, a = 255) {
93
+ buffer[offset] = r
94
+ buffer[offset + 1] = g
95
+ buffer[offset + 2] = b
96
+ buffer[offset + 3] = a
97
+ }
98
+
99
+ function mixChannel(base: number, overlay: number, alpha: number) {
100
+ return Math.round(base + (overlay - base) * clamp(alpha))
101
+ }
102
+
103
+ function writeLogoTint(
104
+ buffer: Uint16Array,
105
+ offset: number,
106
+ base: Rgb,
107
+ primary: Rgb,
108
+ primaryMix: number,
109
+ peakMix: number,
110
+ ) {
111
+ const p = clamp(primaryMix)
112
+ const q = clamp(peakMix)
113
+ const r = mixChannel(mixChannel(base[0], primary[0], p), 255, q)
114
+ const g = mixChannel(mixChannel(base[1], primary[1], p), 255, q)
115
+ const b = mixChannel(mixChannel(base[2], primary[2], p), 255, q)
116
+ writeRgb(buffer, offset, r, g, b)
117
+ }
118
+
119
+ function sameRgb(a: Rgb, b: Rgb) {
120
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
121
+ }
122
+
123
+ export class GoUpsellArtPainter {
124
+ private panelRgb: Rgb = [0, 0, 0]
125
+ private primaryRgb: Rgb = [255, 255, 255]
126
+ private logoBaseRgb: Rgb = [180, 180, 180]
127
+ private elapsed = 0
128
+ private distances = new Float32Array(0)
129
+ private edgeFalloff = new Float32Array(0)
130
+ private geometryWidth = 0
131
+ private geometryHeight = 0
132
+ private reach = 1
133
+ private logoX = 0
134
+ private logoY = 0
135
+ private logoIndexes = new Int32Array(0)
136
+ private logoRgb: boolean | undefined
137
+ private pulsePeak = 0
138
+ private pulsePrimary = 0
139
+ private cacheDirty = true
140
+ private frameCache: Array<{ fg: Uint16Array; bg: Uint16Array }> = []
141
+ private cacheBuildIndex = 0
142
+
143
+ setBackgroundPanel(value: RGBA | Rgb | undefined) {
144
+ if (!value) return false
145
+ const next = value instanceof RGBA ? toRgb(value) : value
146
+ if (sameRgb(this.panelRgb, next)) return false
147
+ this.panelRgb = next
148
+ this.invalidateCache()
149
+ return true
150
+ }
151
+
152
+ setLogoBase(value: RGBA | Rgb | undefined) {
153
+ if (!value) return false
154
+ const next = value instanceof RGBA ? toRgb(value) : value
155
+ if (sameRgb(this.logoBaseRgb, next)) return false
156
+ this.logoBaseRgb = next
157
+ this.invalidateCache()
158
+ return true
159
+ }
160
+
161
+ setPrimary(value: RGBA | Rgb | undefined) {
162
+ if (!value) return false
163
+ const next = value instanceof RGBA ? toRgb(value) : value
164
+ if (sameRgb(this.primaryRgb, next)) return false
165
+ this.primaryRgb = next
166
+ this.invalidateCache()
167
+ return true
168
+ }
169
+
170
+ render(frameBuffer: OptimizedBuffer, options: GoUpsellArtRenderOptions = {}) {
171
+ const rgb = options.rgb === true
172
+ this.elapsed = (this.elapsed + (options.deltaTime ?? 0)) % PERIOD
173
+ this.rebuildGeometry(frameBuffer, rgb)
174
+ if (options.cache !== false) {
175
+ this.drawCached(frameBuffer, rgb)
176
+ return
177
+ }
178
+ this.drawBackground(frameBuffer, this.elapsed)
179
+ this.drawLogo(frameBuffer, this.elapsed, rgb)
180
+ }
181
+
182
+ private invalidateCache() {
183
+ this.cacheDirty = true
184
+ this.cacheBuildIndex = 0
185
+ this.frameCache = []
186
+ }
187
+
188
+ private rebuildGeometry(frameBuffer: OptimizedBuffer, rgb: boolean) {
189
+ const width = frameBuffer.width
190
+ const height = frameBuffer.height
191
+ const geometryChanged = width !== this.geometryWidth || height !== this.geometryHeight
192
+ const logoTemplateChanged = this.logoRgb !== rgb
193
+ if (!geometryChanged && !logoTemplateChanged) return
194
+
195
+ if (geometryChanged) {
196
+ this.geometryWidth = width
197
+ this.geometryHeight = height
198
+ this.logoX = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
199
+ this.logoY = Math.max(
200
+ 0,
201
+ Math.min(Math.max(0, height - LOGO_HEIGHT), Math.round((height - LOGO_HEIGHT) / 2) + LOGO_TOP_BIAS),
202
+ )
203
+
204
+ const centerX = this.logoX + LOGO_WIDTH / 2
205
+ const centerY = this.logoY + LOGO_HEIGHT / 2
206
+ this.reach = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2) + TAIL
207
+ this.distances = new Float32Array(width * height)
208
+ this.edgeFalloff = new Float32Array(width * height)
209
+
210
+ for (let y = 0; y < height; y++) {
211
+ for (let x = 0; x < width; x++) {
212
+ const index = y * width + x
213
+ const dist = Math.hypot(x + 0.5 - centerX, (y + 0.5 - centerY) * 2)
214
+ this.distances[index] = dist
215
+ this.edgeFalloff[index] = Math.max(0, 1 - (dist / (this.reach * 0.85)) ** 2)
216
+ }
217
+ }
218
+ }
219
+
220
+ this.logoRgb = rgb
221
+ this.invalidateCache()
222
+ this.rebuildCellTemplate(frameBuffer, rgb)
223
+ }
224
+
225
+ private drawCached(frameBuffer: OptimizedBuffer, rgb: boolean) {
226
+ if (this.cacheDirty) this.startFrameCache(frameBuffer, rgb)
227
+ if (this.cacheBuildIndex < CACHE_FRAME_COUNT) {
228
+ this.buildFrameCache(frameBuffer, rgb)
229
+ this.drawBackground(frameBuffer, this.elapsed)
230
+ this.drawLogo(frameBuffer, this.elapsed, rgb)
231
+ return
232
+ }
233
+
234
+ const frame = this.frameCache[Math.floor((this.elapsed / PERIOD) * CACHE_FRAME_COUNT) % CACHE_FRAME_COUNT]
235
+ if (frame) {
236
+ frameBuffer.buffers.fg.set(frame.fg)
237
+ frameBuffer.buffers.bg.set(frame.bg)
238
+ }
239
+ }
240
+
241
+ private startFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
242
+ this.frameCache = []
243
+ this.cacheBuildIndex = 0
244
+ this.rebuildCellTemplate(frameBuffer, rgb)
245
+ this.cacheDirty = false
246
+ }
247
+
248
+ private buildFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
249
+ const end = Math.min(CACHE_FRAME_COUNT, this.cacheBuildIndex + CACHE_FRAMES_PER_RENDER)
250
+ for (; this.cacheBuildIndex < end; this.cacheBuildIndex++) {
251
+ const t = (this.cacheBuildIndex / CACHE_FRAME_COUNT) * PERIOD
252
+ this.drawBackground(frameBuffer, t)
253
+ this.drawLogo(frameBuffer, t, rgb)
254
+ this.frameCache.push({
255
+ fg: new Uint16Array(frameBuffer.buffers.fg),
256
+ bg: new Uint16Array(frameBuffer.buffers.bg),
257
+ })
258
+ }
259
+ }
260
+
261
+ private rebuildCellTemplate(frameBuffer: OptimizedBuffer, rgb: boolean) {
262
+ const buffers = frameBuffer.buffers
263
+ buffers.char.fill(SPACE)
264
+ buffers.attributes.fill(0)
265
+
266
+ if (this.geometryWidth < LOGO_WIDTH || this.geometryHeight < LOGO_HEIGHT) {
267
+ this.logoIndexes = new Int32Array(0)
268
+ return
269
+ }
270
+
271
+ this.logoIndexes = new Int32Array(LOGO_TEMPLATE.length)
272
+ for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
273
+ const cell = LOGO_TEMPLATE[i]!
274
+ const index = (this.logoY + cell.y) * this.geometryWidth + this.logoX + cell.x
275
+ this.logoIndexes[i] = index
276
+ buffers.attributes[index] = cell.attributes
277
+ buffers.char[index] =
278
+ cell.kind === LogoCellKind.Background
279
+ ? SPACE
280
+ : cell.kind === LogoCellKind.Top || cell.kind === LogoCellKind.ShadowTop
281
+ ? TOP_HALF
282
+ : cell.kind === LogoCellKind.Solid
283
+ ? rgb
284
+ ? TOP_HALF
285
+ : FULL_BLOCK
286
+ : cell.charCode
287
+ }
288
+ }
289
+
290
+ private drawBackground(frameBuffer: OptimizedBuffer, t: number) {
291
+ const buffers = frameBuffer.buffers
292
+ const fg = buffers.fg
293
+ const bg = buffers.bg
294
+ const distances = this.distances
295
+ const edgeFalloff = this.edgeFalloff
296
+ const baseR = this.panelRgb[0]
297
+ const baseG = this.panelRgb[1]
298
+ const baseB = this.panelRgb[2]
299
+ const deltaR = this.primaryRgb[0] - baseR
300
+ const deltaG = this.primaryRgb[1] - baseG
301
+ const deltaB = this.primaryRgb[2] - baseB
302
+ const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP
303
+
304
+ const phase0 = (t / PERIOD - PHASE_OFFSET + 1) % 1
305
+ const phase1 = (t / PERIOD + 1 / RINGS - PHASE_OFFSET + 1) % 1
306
+ const phase2 = (t / PERIOD + 2 / RINGS - PHASE_OFFSET + 1) % 1
307
+ const envelope0 = Math.sin(phase0 * Math.PI)
308
+ const envelope1 = Math.sin(phase1 * Math.PI)
309
+ const envelope2 = Math.sin(phase2 * Math.PI)
310
+ const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
311
+ const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
312
+ const eased2 = envelope2 * envelope2 * (3 - 2 * envelope2)
313
+ const head0 = phase0 * this.reach
314
+ const head1 = phase1 * this.reach
315
+ const head2 = phase2 * this.reach
316
+
317
+ for (let index = 0; index < distances.length; index++) {
318
+ const dist = distances[index]
319
+ const delta0 = dist - head0
320
+ const abs0 = delta0 < 0 ? -delta0 : delta0
321
+ const crest0 = abs0 < WIDTH ? 0.5 + 0.5 * Math.cos((delta0 / WIDTH) * Math.PI) : 0
322
+ const tail0 = delta0 < 0 && delta0 > -TAIL ? (1 + delta0 * TAIL_SCALE) ** 2.3 : 0
323
+
324
+ const delta1 = dist - head1
325
+ const abs1 = delta1 < 0 ? -delta1 : delta1
326
+ const crest1 = abs1 < WIDTH ? 0.5 + 0.5 * Math.cos((delta1 / WIDTH) * Math.PI) : 0
327
+ const tail1 = delta1 < 0 && delta1 > -TAIL ? (1 + delta1 * TAIL_SCALE) ** 2.3 : 0
328
+
329
+ const delta2 = dist - head2
330
+ const abs2 = delta2 < 0 ? -delta2 : delta2
331
+ const crest2 = abs2 < WIDTH ? 0.5 + 0.5 * Math.cos((delta2 / WIDTH) * Math.PI) : 0
332
+ const tail2 = delta2 < 0 && delta2 > -TAIL ? (1 + delta2 * TAIL_SCALE) ** 2.3 : 0
333
+
334
+ const level =
335
+ (crest0 * AMP + tail0 * TAIL_AMP) * eased0 +
336
+ (crest1 * AMP + tail1 * TAIL_AMP) * eased1 +
337
+ (crest2 * AMP + tail2 * TAIL_AMP) * eased2
338
+ const rawStrength = (level * RING_SCALE + breath) * edgeFalloff[index]
339
+ const strength = (rawStrength > 1 ? 1 : rawStrength) * 0.7
340
+ const offset = index * 4
341
+ const r = Math.round(baseR + deltaR * strength)
342
+ const g = Math.round(baseG + deltaG * strength)
343
+ const b = Math.round(baseB + deltaB * strength)
344
+ bg[offset] = fg[offset] = r
345
+ bg[offset + 1] = fg[offset + 1] = g
346
+ bg[offset + 2] = fg[offset + 2] = b
347
+ bg[offset + 3] = fg[offset + 3] = 255
348
+ }
349
+ }
350
+
351
+ private setLogoPulse(dist: number, head0: number, eased0: number, head1: number, eased1: number) {
352
+ let peak = 0.04
353
+ let primary = 0
354
+
355
+ const delta0 = dist - head0
356
+ const core0 = Math.exp(-(Math.abs(delta0 / 1.2) ** 1.8))
357
+ const soft0 = Math.exp(-(Math.abs(delta0 / 7) ** 1.6))
358
+ const tail0 = delta0 < 0 && delta0 > -7 ? (1 + delta0 / 7) ** 2.6 : 0
359
+ peak += core0 * 0.65 * eased0
360
+ primary += (soft0 * 0.16 + tail0 * 0.22) * eased0
361
+
362
+ const delta1 = dist - head1
363
+ const core1 = Math.exp(-(Math.abs(delta1 / 1.2) ** 1.8))
364
+ const soft1 = Math.exp(-(Math.abs(delta1 / 7) ** 1.6))
365
+ const tail1 = delta1 < 0 && delta1 > -7 ? (1 + delta1 / 7) ** 2.6 : 0
366
+ peak += core1 * 0.65 * eased1
367
+ primary += (soft1 * 0.16 + tail1 * 0.22) * eased1
368
+
369
+ this.pulsePeak = peak > 1 ? 1 : peak
370
+ this.pulsePrimary = primary > 1 ? 1 : primary
371
+ }
372
+
373
+ private drawLogo(frameBuffer: OptimizedBuffer, t: number, rgb: boolean) {
374
+ if (this.logoIndexes.length === 0) return
375
+
376
+ const buffers = frameBuffer.buffers
377
+ const fg = buffers.fg
378
+ const bg = buffers.bg
379
+ const shadow: Rgb = [
380
+ mixChannel(this.panelRgb[0], this.logoBaseRgb[0], 0.25),
381
+ mixChannel(this.panelRgb[1], this.logoBaseRgb[1], 0.25),
382
+ mixChannel(this.panelRgb[2], this.logoBaseRgb[2], 0.25),
383
+ ]
384
+ const phase0 = (t / PERIOD) % 1
385
+ const phase1 = (t / PERIOD + 0.5) % 1
386
+ const envelope0 = Math.sin(phase0 * Math.PI)
387
+ const envelope1 = Math.sin(phase1 * Math.PI)
388
+ const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
389
+ const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
390
+ const head0 = phase0 * LOGO_REACH
391
+ const head1 = phase1 * LOGO_REACH
392
+
393
+ for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
394
+ const cell = LOGO_TEMPLATE[i]!
395
+ const index = this.logoIndexes[i]!
396
+ const offset = index * 4
397
+ this.setLogoPulse(cell.topDist, head0, eased0, head1, eased1)
398
+ const topPeak = this.pulsePeak
399
+ const topPrimary = this.pulsePrimary
400
+ this.setLogoPulse(cell.bottomDist, head0, eased0, head1, eased1)
401
+ const bottomPeak = this.pulsePeak
402
+ const bottomPrimary = this.pulsePrimary
403
+
404
+ if (cell.kind === LogoCellKind.Background) {
405
+ writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, Math.max(topPeak, bottomPeak) * 0.18)
406
+ continue
407
+ }
408
+
409
+ if (cell.kind === LogoCellKind.Top) {
410
+ writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
411
+ writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, bottomPeak * 0.18)
412
+ continue
413
+ }
414
+
415
+ if (cell.kind === LogoCellKind.ShadowTop) {
416
+ writeLogoTint(fg, offset, shadow, this.primaryRgb, 0, topPeak * 0.18)
417
+ continue
418
+ }
419
+
420
+ if (cell.kind === LogoCellKind.Solid && rgb) {
421
+ writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
422
+ writeLogoTint(bg, offset, this.logoBaseRgb, this.primaryRgb, bottomPrimary, bottomPeak)
423
+ continue
424
+ }
425
+
426
+ writeLogoTint(
427
+ fg,
428
+ offset,
429
+ this.logoBaseRgb,
430
+ this.primaryRgb,
431
+ (topPrimary + bottomPrimary) / 2,
432
+ (topPeak + bottomPeak) / 2,
433
+ )
434
+ }
435
+ }
436
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ FrameBufferRenderable,
3
+ RGBA,
4
+ type OptimizedBuffer,
5
+ type RenderContext,
6
+ type RenderableOptions,
7
+ } from "@opentui/core"
8
+ import { extend, useRenderer } from "@opentui/solid"
9
+ import { onCleanup, onMount } from "solid-js"
10
+ import { tint, useTheme } from "@tui/context/theme"
11
+ import { GoUpsellArtPainter } from "./bg-pulse-render"
12
+
13
+ type GoUpsellArtOptions = RenderableOptions<FrameBufferRenderable> & {
14
+ backgroundPanel?: RGBA
15
+ primary?: RGBA
16
+ logoBase?: RGBA
17
+ }
18
+
19
+ class GoUpsellArtRenderable extends FrameBufferRenderable {
20
+ private painter = new GoUpsellArtPainter()
21
+
22
+ constructor(ctx: RenderContext, options: GoUpsellArtOptions = {}) {
23
+ const width = typeof options.width === "number" ? options.width : 1
24
+ const height = typeof options.height === "number" ? options.height : 1
25
+ super(ctx, {
26
+ ...options,
27
+ width,
28
+ height,
29
+ live: options.live ?? true,
30
+ respectAlpha: false,
31
+ })
32
+
33
+ if (options.width !== undefined && typeof options.width !== "number") this.width = options.width
34
+ if (options.height !== undefined && typeof options.height !== "number") this.height = options.height
35
+ this.painter.setBackgroundPanel(options.backgroundPanel)
36
+ this.painter.setPrimary(options.primary)
37
+ this.painter.setLogoBase(options.logoBase)
38
+ }
39
+
40
+ set backgroundPanel(value: RGBA | undefined) {
41
+ if (this.painter.setBackgroundPanel(value)) this.requestRender()
42
+ }
43
+
44
+ set logoBase(value: RGBA | undefined) {
45
+ if (this.painter.setLogoBase(value)) this.requestRender()
46
+ }
47
+
48
+ set primary(value: RGBA | undefined) {
49
+ if (this.painter.setPrimary(value)) this.requestRender()
50
+ }
51
+
52
+ protected override renderSelf(buffer: OptimizedBuffer, deltaTime = 0): void {
53
+ if (!this.visible || this.isDestroyed) return
54
+
55
+ this.painter.render(this.frameBuffer, {
56
+ deltaTime,
57
+ rgb: this._ctx.capabilities?.rgb === true,
58
+ })
59
+ super.renderSelf(buffer)
60
+ }
61
+ }
62
+
63
+ declare module "@opentui/solid" {
64
+ interface OpenTUIComponents {
65
+ go_upsell_art: typeof GoUpsellArtRenderable
66
+ }
67
+ }
68
+
69
+ extend({ go_upsell_art: GoUpsellArtRenderable })
70
+
71
+ export function BgPulse() {
72
+ const { theme } = useTheme()
73
+ const renderer = useRenderer()
74
+ let targetFps = renderer.targetFps
75
+ let maxFps = renderer.maxFps
76
+
77
+ onMount(() => {
78
+ targetFps = renderer.targetFps
79
+ maxFps = renderer.maxFps
80
+ renderer.targetFps = 30
81
+ renderer.maxFps = 30
82
+ })
83
+
84
+ onCleanup(() => {
85
+ renderer.targetFps = targetFps
86
+ renderer.maxFps = maxFps
87
+ })
88
+
89
+ return (
90
+ <go_upsell_art
91
+ width="100%"
92
+ height="100%"
93
+ backgroundPanel={theme.backgroundPanel}
94
+ primary={theme.primary}
95
+ logoBase={tint(theme.background, theme.text, 0.62)}
96
+ live
97
+ />
98
+ )
99
+ }
@@ -0,0 +1,21 @@
1
+ export const EmptyBorder = {
2
+ topLeft: "",
3
+ bottomLeft: "",
4
+ vertical: "",
5
+ topRight: "",
6
+ bottomRight: "",
7
+ horizontal: " ",
8
+ bottomT: "",
9
+ topT: "",
10
+ cross: "",
11
+ leftT: "",
12
+ rightT: "",
13
+ }
14
+
15
+ export const SplitBorder = {
16
+ border: ["left" as const, "right" as const],
17
+ customBorderChars: {
18
+ ...EmptyBorder,
19
+ vertical: "┃",
20
+ },
21
+ }
@@ -0,0 +1,31 @@
1
+ import { createMemo } from "solid-js"
2
+ import { useLocal } from "@tui/context/local"
3
+ import { DialogSelect } from "@tui/ui/dialog-select"
4
+ import { useDialog } from "@tui/ui/dialog"
5
+
6
+ export function DialogAgent() {
7
+ const local = useLocal()
8
+ const dialog = useDialog()
9
+
10
+ const options = createMemo(() =>
11
+ local.agent.list().map((item) => {
12
+ return {
13
+ value: item.name,
14
+ title: item.name,
15
+ description: item.native ? "native" : item.description,
16
+ }
17
+ }),
18
+ )
19
+
20
+ return (
21
+ <DialogSelect
22
+ title="Select agent"
23
+ current={local.agent.current()?.name}
24
+ options={options()}
25
+ onSelect={(option) => {
26
+ local.agent.set(option.value)
27
+ dialog.clear()
28
+ }}
29
+ />
30
+ )
31
+ }