@neurocode-ai/tui 1.18.8
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.
- package/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- package/tsconfig.json +10 -0
package/src/attention.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/// <reference path="./audio.d.ts" />
|
|
2
|
+
import type {
|
|
3
|
+
TuiAttention,
|
|
4
|
+
TuiAttentionNotifyInput,
|
|
5
|
+
TuiAttentionNotifyResult,
|
|
6
|
+
TuiAttentionNotifySkipReason,
|
|
7
|
+
TuiAttentionWhen,
|
|
8
|
+
TuiKV,
|
|
9
|
+
TuiAttentionSoundName,
|
|
10
|
+
TuiAttentionSoundPack,
|
|
11
|
+
TuiAttentionSoundPackInfo,
|
|
12
|
+
} from "@neurocode-ai/plugin/tui"
|
|
13
|
+
import { AttentionSoundName, type TuiConfig } from "./config"
|
|
14
|
+
import { Schema } from "effect"
|
|
15
|
+
import stripAnsi from "strip-ansi"
|
|
16
|
+
import * as TuiAudio from "./audio"
|
|
17
|
+
import defaultSoundPath from "@neurocode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
|
18
|
+
import questionSoundPath from "@neurocode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
|
|
19
|
+
import permissionSoundPath from "@neurocode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
|
|
20
|
+
import errorSoundPath from "@neurocode-ai/ui/audio/nope-03.mp3" with { type: "file" }
|
|
21
|
+
import doneSoundPath from "@neurocode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
|
22
|
+
import subagentDoneSoundPath from "@neurocode-ai/ui/audio/yup-01.mp3" with { type: "file" }
|
|
23
|
+
|
|
24
|
+
type FocusState = "unknown" | "focused" | "blurred"
|
|
25
|
+
|
|
26
|
+
type AttentionRenderer = {
|
|
27
|
+
readonly isDestroyed: boolean
|
|
28
|
+
on(event: "focus" | "blur", listener: () => void): unknown
|
|
29
|
+
off(event: "focus" | "blur", listener: () => void): unknown
|
|
30
|
+
triggerNotification(message: string, title?: string): boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type RegisteredSoundPack = TuiAttentionSoundPack & {
|
|
34
|
+
builtin: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TuiAttentionHost = TuiAttention & {
|
|
38
|
+
dispose(): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_TITLE = "neurocode"
|
|
42
|
+
const DEFAULT_PACK_ID = "neurocode.default"
|
|
43
|
+
const KV_SOUND_PACK = "attention_sound_pack"
|
|
44
|
+
const TITLE_LIMIT = 80
|
|
45
|
+
const MESSAGE_LIMIT = 240
|
|
46
|
+
const BUILTIN_PACK: RegisteredSoundPack = {
|
|
47
|
+
id: DEFAULT_PACK_ID,
|
|
48
|
+
name: "NeuroCode Default",
|
|
49
|
+
builtin: true,
|
|
50
|
+
sounds: {
|
|
51
|
+
default: defaultSoundPath,
|
|
52
|
+
question: questionSoundPath,
|
|
53
|
+
permission: permissionSoundPath,
|
|
54
|
+
error: errorSoundPath,
|
|
55
|
+
done: doneSoundPath,
|
|
56
|
+
subagent_done: subagentDoneSoundPath,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function skipped(reason: TuiAttentionNotifySkipReason): TuiAttentionNotifyResult {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
notification: false,
|
|
64
|
+
sound: false,
|
|
65
|
+
skipped: reason,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeText(input: string | undefined, fallback: string, limit: number) {
|
|
70
|
+
const text = stripAnsi(input ?? "")
|
|
71
|
+
.replace(/[ \t]*[\r\n]+[ \t]*/g, " ")
|
|
72
|
+
.replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, "")
|
|
73
|
+
.trim()
|
|
74
|
+
const normalized = text.length ? text : fallback
|
|
75
|
+
return Array.from(normalized).slice(0, limit).join("")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clampVolume(volume: number) {
|
|
79
|
+
if (!Number.isFinite(volume)) return 0
|
|
80
|
+
return Math.min(1, Math.max(0, volume))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function soundVolume(input: TuiAttentionNotifyInput, config: Pick<TuiConfig.Resolved, "attention">) {
|
|
84
|
+
if (!config.attention.sound) return
|
|
85
|
+
if (input.sound === false) return
|
|
86
|
+
if (input.sound === undefined) return clampVolume(config.attention.volume)
|
|
87
|
+
if (input.sound === true) return clampVolume(config.attention.volume)
|
|
88
|
+
return clampVolume(input.sound.volume ?? config.attention.volume)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizePack(pack: TuiAttentionSoundPack): RegisteredSoundPack | undefined {
|
|
92
|
+
const id = pack.id.trim()
|
|
93
|
+
if (!id) return
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
name: pack.name?.trim() || undefined,
|
|
97
|
+
builtin: false,
|
|
98
|
+
sounds: Object.fromEntries(
|
|
99
|
+
Object.entries(pack.sounds).filter(
|
|
100
|
+
(item): item is [TuiAttentionSoundName, string] =>
|
|
101
|
+
Schema.is(AttentionSoundName)(item[0]) && typeof item[1] === "string" && item[1].trim().length > 0,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function focusSkip(when: TuiAttentionWhen, focus: FocusState) {
|
|
108
|
+
if (when === "always") return
|
|
109
|
+
if (focus === "unknown") return "focus_unknown"
|
|
110
|
+
if (when === "blurred" && focus === "focused") return "focused"
|
|
111
|
+
if (when === "focused" && focus === "blurred") return "blurred"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createTuiAttention(input: {
|
|
115
|
+
renderer: AttentionRenderer
|
|
116
|
+
config: Pick<TuiConfig.Resolved, "attention">
|
|
117
|
+
kv?: TuiKV
|
|
118
|
+
audio?: Pick<typeof TuiAudio, "loadSoundFile" | "play">
|
|
119
|
+
}): TuiAttentionHost {
|
|
120
|
+
let focus: FocusState = "unknown"
|
|
121
|
+
let disposed = false
|
|
122
|
+
let activePackID: string | undefined
|
|
123
|
+
const packs = new Map<string, RegisteredSoundPack>([[BUILTIN_PACK.id, BUILTIN_PACK]])
|
|
124
|
+
const audio = input.audio ?? TuiAudio
|
|
125
|
+
|
|
126
|
+
const onFocus = () => {
|
|
127
|
+
focus = "focused"
|
|
128
|
+
}
|
|
129
|
+
const onBlur = () => {
|
|
130
|
+
focus = "blurred"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
input.renderer.on("focus", onFocus)
|
|
134
|
+
input.renderer.on("blur", onBlur)
|
|
135
|
+
|
|
136
|
+
function configuredPackID() {
|
|
137
|
+
const stored = input.kv?.get<string | undefined>(KV_SOUND_PACK, undefined)
|
|
138
|
+
return activePackID ?? stored ?? input.config.attention.sound_pack
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function currentPack() {
|
|
142
|
+
return packs.get(configuredPackID()) ?? BUILTIN_PACK
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function soundCandidates(name: TuiAttentionSoundName) {
|
|
146
|
+
return [input.config.attention.sounds[name], currentPack().sounds[name], BUILTIN_PACK.sounds[name]].filter(
|
|
147
|
+
(item, index, list): item is string => typeof item === "string" && list.indexOf(item) === index,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function playSound(name: TuiAttentionSoundName, volume: number) {
|
|
152
|
+
try {
|
|
153
|
+
for (const file of soundCandidates(name)) {
|
|
154
|
+
const current = await audio.loadSoundFile(file).catch((error) => {
|
|
155
|
+
console.debug("failed to load attention sound", { file, error })
|
|
156
|
+
return null
|
|
157
|
+
})
|
|
158
|
+
if (disposed) return false
|
|
159
|
+
if (current == null) continue
|
|
160
|
+
if (audio.play(current, { volume }) != null) return true
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.debug("failed to play attention sound", { error })
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
async notify(request) {
|
|
171
|
+
try {
|
|
172
|
+
if (!input.config.attention.enabled) return skipped("attention_disabled")
|
|
173
|
+
if (disposed || input.renderer.isDestroyed) return skipped("renderer_destroyed")
|
|
174
|
+
|
|
175
|
+
const message = normalizeText(request.message, "", MESSAGE_LIMIT)
|
|
176
|
+
if (!message) return skipped("empty_message")
|
|
177
|
+
|
|
178
|
+
const requestedNotification = typeof request.notification === "object" ? request.notification : undefined
|
|
179
|
+
const notificationSkip = focusSkip(requestedNotification?.when ?? "blurred", focus)
|
|
180
|
+
const notificationRequested = input.config.attention.notifications && request.notification !== false
|
|
181
|
+
const shouldNotify = notificationRequested && !notificationSkip
|
|
182
|
+
const notification = shouldNotify
|
|
183
|
+
? (() => {
|
|
184
|
+
try {
|
|
185
|
+
return input.renderer.triggerNotification(
|
|
186
|
+
message,
|
|
187
|
+
normalizeText(request.title, DEFAULT_TITLE, TITLE_LIMIT),
|
|
188
|
+
)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.debug("failed to trigger attention notification", { error })
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
})()
|
|
194
|
+
: false
|
|
195
|
+
const volume = soundVolume(request, input.config)
|
|
196
|
+
const requestedSound = typeof request.sound === "object" ? request.sound : undefined
|
|
197
|
+
const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus)
|
|
198
|
+
const soundName =
|
|
199
|
+
requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) ? requestedSound.name : "default"
|
|
200
|
+
const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume)
|
|
201
|
+
|
|
202
|
+
if (!notification && !sound) {
|
|
203
|
+
if (notificationRequested && notificationSkip) return skipped(notificationSkip)
|
|
204
|
+
if (soundSkip) return skipped(soundSkip)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
ok: notification || sound,
|
|
209
|
+
notification,
|
|
210
|
+
sound,
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.debug("failed to handle attention notification", { error })
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
notification: false,
|
|
217
|
+
sound: false,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
soundboard: {
|
|
222
|
+
registerPack(pack) {
|
|
223
|
+
const next = normalizePack(pack)
|
|
224
|
+
if (!next) return () => {}
|
|
225
|
+
packs.set(next.id, next)
|
|
226
|
+
let disposed = false
|
|
227
|
+
return () => {
|
|
228
|
+
if (disposed) return
|
|
229
|
+
disposed = true
|
|
230
|
+
if (packs.get(next.id) === next) packs.delete(next.id)
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
activate(id, options) {
|
|
234
|
+
const pack = packs.get(id)
|
|
235
|
+
if (!pack) return false
|
|
236
|
+
activePackID = pack.id
|
|
237
|
+
if (options?.persist) input.kv?.set(KV_SOUND_PACK, pack.id)
|
|
238
|
+
return true
|
|
239
|
+
},
|
|
240
|
+
current() {
|
|
241
|
+
return currentPack().id
|
|
242
|
+
},
|
|
243
|
+
list(): TuiAttentionSoundPackInfo[] {
|
|
244
|
+
const current = currentPack().id
|
|
245
|
+
return Array.from(packs.values()).map((pack) => ({
|
|
246
|
+
id: pack.id,
|
|
247
|
+
name: pack.name,
|
|
248
|
+
active: pack.id === current,
|
|
249
|
+
builtin: pack.builtin,
|
|
250
|
+
}))
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
dispose() {
|
|
254
|
+
if (disposed) return
|
|
255
|
+
disposed = true
|
|
256
|
+
input.renderer.off("focus", onFocus)
|
|
257
|
+
input.renderer.off("blur", onBlur)
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
package/src/audio.d.ts
ADDED
package/src/audio.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Audio, type AudioErrorContext, type AudioPlayOptions, type AudioSound, type AudioVoice } from "@opentui/core"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
let audio: Audio | null | undefined
|
|
5
|
+
const sounds = new Map<string, Promise<AudioSound | null>>()
|
|
6
|
+
|
|
7
|
+
function getAudio() {
|
|
8
|
+
if (audio !== undefined) return audio
|
|
9
|
+
try {
|
|
10
|
+
const next = Audio.create({ autoStart: false })
|
|
11
|
+
next.on("error", (error: Error, context: AudioErrorContext) => {
|
|
12
|
+
console.debug("tui audio error", { error, context })
|
|
13
|
+
})
|
|
14
|
+
audio = next
|
|
15
|
+
return next
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.debug("failed to create tui audio", { error })
|
|
18
|
+
audio = null
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadSoundFile(file: string) {
|
|
24
|
+
const current = getAudio()
|
|
25
|
+
if (!current) return Promise.resolve(null)
|
|
26
|
+
const cached = sounds.get(file)
|
|
27
|
+
if (cached) return cached
|
|
28
|
+
const task = readFile(file)
|
|
29
|
+
.then((bytes) => current.loadSound(bytes))
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
console.debug("failed to load tui sound", { file, error })
|
|
32
|
+
return null
|
|
33
|
+
})
|
|
34
|
+
sounds.set(file, task)
|
|
35
|
+
return task
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function play(sound: AudioSound, options?: AudioPlayOptions) {
|
|
39
|
+
const current = getAudio()
|
|
40
|
+
if (!current) return null
|
|
41
|
+
if (!current.isStarted() && !current.start()) return null
|
|
42
|
+
return current.play(sound, options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stopVoice(voice: AudioVoice) {
|
|
46
|
+
return audio?.stopVoice(voice) ?? false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function dispose() {
|
|
50
|
+
audio?.dispose()
|
|
51
|
+
audio = undefined
|
|
52
|
+
sounds.clear()
|
|
53
|
+
}
|
package/src/clipboard.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process"
|
|
2
|
+
import { readFile, rm } from "node:fs/promises"
|
|
3
|
+
import { platform, release, tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { promisify } from "node:util"
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
|
|
9
|
+
function command(command: string, args: string[] = [], input?: string) {
|
|
10
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, { stdio: [input === undefined ? "ignore" : "pipe", "pipe", "ignore"] })
|
|
12
|
+
const output: Buffer[] = []
|
|
13
|
+
child.on("error", reject)
|
|
14
|
+
child.stdout?.on("data", (chunk: Buffer) => output.push(chunk))
|
|
15
|
+
child.on("close", (code) => {
|
|
16
|
+
if (code === 0) return resolve(Buffer.concat(output))
|
|
17
|
+
reject(new Error(`${command} exited with code ${code}`))
|
|
18
|
+
})
|
|
19
|
+
if (input !== undefined) child.stdin?.end(input)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeOsc52(text: string) {
|
|
24
|
+
if (!process.stdout.isTTY) return
|
|
25
|
+
const sequence = `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`
|
|
26
|
+
process.stdout.write(process.env.TMUX || process.env.STY ? `\x1bPtmux;\x1b${sequence}\x1b\\` : sequence)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function read() {
|
|
30
|
+
if (platform() === "darwin") {
|
|
31
|
+
const file = path.join(tmpdir(), "neurocode-clipboard.png")
|
|
32
|
+
try {
|
|
33
|
+
await exec("osascript", [
|
|
34
|
+
"-e",
|
|
35
|
+
'set imageData to the clipboard as "PNGf"',
|
|
36
|
+
"-e",
|
|
37
|
+
`set fileRef to open for access POSIX file "${file}" with write permission`,
|
|
38
|
+
"-e",
|
|
39
|
+
"set eof fileRef to 0",
|
|
40
|
+
"-e",
|
|
41
|
+
"write imageData to fileRef",
|
|
42
|
+
"-e",
|
|
43
|
+
"close access fileRef",
|
|
44
|
+
])
|
|
45
|
+
return { data: (await readFile(file)).toString("base64"), mime: "image/png" }
|
|
46
|
+
} catch {
|
|
47
|
+
// Fall through to text clipboard.
|
|
48
|
+
} finally {
|
|
49
|
+
await rm(file, { force: true }).catch(() => {})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (platform() === "win32" || release().includes("WSL")) {
|
|
54
|
+
const script =
|
|
55
|
+
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
|
|
56
|
+
const image = await command("powershell.exe", ["-NonInteractive", "-NoProfile", "-command", script]).catch(() =>
|
|
57
|
+
Buffer.alloc(0),
|
|
58
|
+
)
|
|
59
|
+
if (image.length) return { data: image.toString().trim(), mime: "image/png" }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (platform() === "linux") {
|
|
63
|
+
const wayland = await command("wl-paste", ["-t", "image/png"]).catch(() => Buffer.alloc(0))
|
|
64
|
+
if (wayland.length) return { data: wayland.toString("base64"), mime: "image/png" }
|
|
65
|
+
const x11 = await command("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]).catch(() =>
|
|
66
|
+
Buffer.alloc(0),
|
|
67
|
+
)
|
|
68
|
+
if (x11.length) return { data: x11.toString("base64"), mime: "image/png" }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { default: clipboardy } = await import("clipboardy")
|
|
72
|
+
const text = await clipboardy.read().catch(() => undefined)
|
|
73
|
+
if (text) return { data: text, mime: "text/plain" }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function copyCommand(
|
|
77
|
+
os: NodeJS.Platform,
|
|
78
|
+
wayland: boolean,
|
|
79
|
+
has: (name: string) => boolean,
|
|
80
|
+
): string[] | undefined {
|
|
81
|
+
if (os === "darwin" && has("osascript")) return ["osascript"]
|
|
82
|
+
if (os === "linux" && wayland && has("wl-copy")) return ["wl-copy"]
|
|
83
|
+
if (os === "linux" && has("xclip")) return ["xclip", "-selection", "clipboard"]
|
|
84
|
+
if (os === "linux" && has("xsel")) return ["xsel", "--clipboard", "--input"]
|
|
85
|
+
if (os === "win32" && has("powershell.exe")) {
|
|
86
|
+
return [
|
|
87
|
+
"powershell.exe",
|
|
88
|
+
"-NonInteractive",
|
|
89
|
+
"-NoProfile",
|
|
90
|
+
"-Command",
|
|
91
|
+
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let copyMethod: Promise<(text: string) => Promise<void>> | undefined
|
|
97
|
+
|
|
98
|
+
function getCopyMethod() {
|
|
99
|
+
return (copyMethod ??= (async () => {
|
|
100
|
+
const { which } = await import("@neurocode-ai/core/util/which")
|
|
101
|
+
const native = copyCommand(platform(), Boolean(process.env.WAYLAND_DISPLAY), (name) => Boolean(which(name)))
|
|
102
|
+
if (native?.[0] === "osascript") {
|
|
103
|
+
return async (text: string) => {
|
|
104
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
105
|
+
await command("osascript", ["-e", `set the clipboard to "${escaped}"`]).catch(() => undefined)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (native) {
|
|
109
|
+
return async (text: string) => {
|
|
110
|
+
await command(native[0], native.slice(1), text).catch(() => undefined)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return async (text: string) => {
|
|
114
|
+
const { default: clipboardy } = await import("clipboardy")
|
|
115
|
+
await clipboardy.write(text).catch(() => undefined)
|
|
116
|
+
}
|
|
117
|
+
})())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function write(text: string) {
|
|
121
|
+
writeOsc52(text)
|
|
122
|
+
const method = await getCopyMethod()
|
|
123
|
+
await method(text)
|
|
124
|
+
}
|