@followbin/dsh-selection 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +140 -2
- package/README.zh-CN.md +141 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/client.js +1055 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +1 -0
- package/index.js +41 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +90 -4
- package/src/host/files.mjs +29 -0
- package/src/host/http.mjs +153 -0
- package/src/host/llm.mjs +194 -0
- package/src/host/log.mjs +40 -0
- package/src/host/service.mjs +324 -0
- package/src/host/settings.mjs +279 -0
- package/src/host/skills.mjs +88 -0
- package/src/host/supervisor.mjs +126 -0
- package/src/host/usage.mjs +128 -0
- package/src/shared/icons.mjs +43 -0
- package/src/shared/open-with-system.mjs +15 -0
- package/src/shared/paths.mjs +7 -0
- package/src/shared/protocol.mjs +4 -0
- package/src/worker/controller.mjs +261 -0
- package/src/worker/main.mjs +147 -0
- package/src/worker/native-window.mjs +319 -0
- package/src/worker/open-url.mjs +20 -0
- package/src/worker/overlay.mjs +263 -0
- package/src/worker/page.mjs +14 -0
- package/src/worker/placement.mjs +58 -0
- package/src/worker/speech.mjs +136 -0
- package/ui/markdown.js +122 -0
- package/ui/toolbar.css +152 -0
- package/ui/toolbar.html +52 -0
- package/ui/toolbar.js +492 -0
- package/vendor/selection-hook/LICENSE +21 -0
- package/vendor/selection-hook/index.d.ts +517 -0
- package/vendor/selection-hook/index.js +662 -0
- package/vendor/selection-hook/package.json +10 -0
- package/vendor/selection-hook/prebuilds/darwin-arm64/selection-hook.node +0 -0
- package/vendor/selection-hook/prebuilds/darwin-x64/selection-hook.node +0 -0
- package/vendor/selection-hook/prebuilds/win32-arm64/selection-hook.node +0 -0
- package/vendor/selection-hook/prebuilds/win32-x64/selection-hook.node +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Reads text aloud with the system voice: `say` on macOS, SAPI through PowerShell on Windows.
|
|
2
|
+
// The text goes through stdin, so nothing in it is ever parsed as a command-line argument.
|
|
3
|
+
import { execFile, spawn } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{ log: (level: string, message: string, fields?: object) => void, onChange: (speaking: boolean) => void }} options
|
|
7
|
+
*/
|
|
8
|
+
export function createSpeech({ log, onChange }) {
|
|
9
|
+
let current = null
|
|
10
|
+
let voices = null
|
|
11
|
+
// Bumped on every start and stop so a slow voice lookup cannot resurrect a cancelled request.
|
|
12
|
+
let generation = 0
|
|
13
|
+
|
|
14
|
+
async function macVoiceFor(text) {
|
|
15
|
+
voices ??= await listMacVoices().catch((error) => {
|
|
16
|
+
log('warn', 'listing voices failed', { error: error.message })
|
|
17
|
+
return []
|
|
18
|
+
})
|
|
19
|
+
return pickVoice(voices, localeOf(text))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function speak(text) {
|
|
23
|
+
stop()
|
|
24
|
+
const token = ++generation
|
|
25
|
+
const args = process.platform === 'darwin' ? await macArgs(text) : []
|
|
26
|
+
if (token !== generation) return
|
|
27
|
+
const process_ = process.platform === 'darwin'
|
|
28
|
+
? spawn('say', [...args, '-f', '-'], { stdio: ['pipe', 'ignore', 'pipe'] })
|
|
29
|
+
// stdin would otherwise be decoded with the OEM code page and garble anything but ASCII.
|
|
30
|
+
: spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
31
|
+
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Add-Type -AssemblyName System.Speech; '
|
|
32
|
+
+ '$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.Speak([Console]::In.ReadToEnd())'],
|
|
33
|
+
{ stdio: ['pipe', 'ignore', 'pipe'], windowsHide: true })
|
|
34
|
+
current = process_
|
|
35
|
+
onChange(true)
|
|
36
|
+
process_.on('error', (error) => log('warn', 'speech failed to start', { error: error.message }))
|
|
37
|
+
process_.on('exit', () => {
|
|
38
|
+
if (current === process_) {
|
|
39
|
+
current = null
|
|
40
|
+
onChange(false)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
// Stopping before the voice has read all of stdin closes the pipe; EPIPE is expected then.
|
|
44
|
+
process_.stdin.on('error', () => {})
|
|
45
|
+
process_.stdin.end(text)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function macArgs(text) {
|
|
49
|
+
const voice = await macVoiceFor(text)
|
|
50
|
+
return voice ? ['-v', voice] : []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stop() {
|
|
54
|
+
generation += 1
|
|
55
|
+
if (!current) return
|
|
56
|
+
const process_ = current
|
|
57
|
+
current = null
|
|
58
|
+
process_.kill()
|
|
59
|
+
onChange(false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
/** Starts reading, or stops if something is already being read. */
|
|
64
|
+
toggle(text) {
|
|
65
|
+
if (current) stop()
|
|
66
|
+
else void speak(text)
|
|
67
|
+
},
|
|
68
|
+
stop,
|
|
69
|
+
get speaking() { return current !== null },
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function listMacVoices() {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
execFile('say', ['-v', '?'], { encoding: 'utf8', timeout: 5000 }, (error, stdout) => {
|
|
76
|
+
if (error) return reject(error)
|
|
77
|
+
resolve(parseVoices(stdout))
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `say -v ?` lists voices alphabetically, so the first one for a language is often a novelty voice
|
|
83
|
+
// (Albert, Bad News…). Well-known voices go first; novelty voices are never chosen.
|
|
84
|
+
const PREFERRED_VOICES = {
|
|
85
|
+
en_US: ['Samantha', 'Alex', 'Ava', 'Allison', 'Tom'],
|
|
86
|
+
zh_CN: ['Tingting', 'Lili', 'Yu-shu'],
|
|
87
|
+
ja_JP: ['Kyoko', 'Otoya'],
|
|
88
|
+
ko_KR: ['Yuna', 'Sora'],
|
|
89
|
+
ru_RU: ['Milena', 'Yuri', 'Katya'],
|
|
90
|
+
}
|
|
91
|
+
const NOVELTY_VOICES = new Set([
|
|
92
|
+
'Albert', 'Bad News', 'Bahh', 'Bells', 'Boing', 'Bubbles', 'Cellos', 'Good News', 'Jester', 'Organ',
|
|
93
|
+
'Superstar', 'Trinoids', 'Whisper', 'Wobble', 'Zarvox',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/** @returns {string | undefined} the voice name to pass to `say -v`, or undefined for the system voice */
|
|
97
|
+
export function pickVoice(voices, locale) {
|
|
98
|
+
const base = (name) => name.replace(/\s*\(.*\)$/, '')
|
|
99
|
+
for (const name of PREFERRED_VOICES[locale] ?? []) {
|
|
100
|
+
const voice = voices.find((candidate) => candidate.locale === locale && base(candidate.name) === name)
|
|
101
|
+
if (voice) return voice.name
|
|
102
|
+
}
|
|
103
|
+
const usable = (voice) => !NOVELTY_VOICES.has(base(voice.name))
|
|
104
|
+
return voices.find((voice) => voice.locale === locale && usable(voice))?.name
|
|
105
|
+
?? voices.find((voice) => voice.locale.startsWith(locale.slice(0, 2)) && usable(voice))?.name
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Reads the output of `say -v ?`: one voice per line, then its locale, then a sample sentence. */
|
|
109
|
+
export function parseVoices(stdout) {
|
|
110
|
+
const voices = []
|
|
111
|
+
for (const line of stdout.split('\n')) {
|
|
112
|
+
const match = /^(.+?)\s+([a-z]{2,3}_[A-Z0-9]{2,3})\s+#/.exec(line)
|
|
113
|
+
if (match) voices.push({ name: match[1].trim(), locale: match[2] })
|
|
114
|
+
}
|
|
115
|
+
return voices
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Picks a voice locale from the script the text is written in. */
|
|
119
|
+
export function localeOf(text) {
|
|
120
|
+
let han = 0
|
|
121
|
+
let kana = 0
|
|
122
|
+
let hangul = 0
|
|
123
|
+
let cyrillic = 0
|
|
124
|
+
for (const char of text) {
|
|
125
|
+
const code = char.codePointAt(0)
|
|
126
|
+
if (code >= 0x3040 && code <= 0x30ff) kana += 1
|
|
127
|
+
else if ((code >= 0xac00 && code <= 0xd7af) || (code >= 0x1100 && code <= 0x11ff)) hangul += 1
|
|
128
|
+
else if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf)) han += 1
|
|
129
|
+
else if (code >= 0x0400 && code <= 0x04ff) cyrillic += 1
|
|
130
|
+
}
|
|
131
|
+
if (kana > 0) return 'ja_JP'
|
|
132
|
+
if (hangul > 0) return 'ko_KR'
|
|
133
|
+
if (han > 0) return 'zh_CN'
|
|
134
|
+
if (cyrillic > 0) return 'ru_RU'
|
|
135
|
+
return 'en_US'
|
|
136
|
+
}
|
package/ui/markdown.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// A deliberately small Markdown renderer for model output. Everything is HTML-escaped first and only
|
|
2
|
+
// tags produced here reach the page; links keep their target in data-href and open in the browser.
|
|
3
|
+
// Classic script: the toolbar page inlines it, the tests import it, both read globalThis.DshMarkdown.
|
|
4
|
+
(function (root) {
|
|
5
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
6
|
+
const escapeHtml = (text) => String(text).replace(/[&<>"']/g, (char) => ESCAPES[char])
|
|
7
|
+
|
|
8
|
+
const FENCE = /^\s*(```|~~~)/
|
|
9
|
+
const HEADING = /^(#{1,6})\s+(.*)$/
|
|
10
|
+
const QUOTE = /^\s*>\s?/
|
|
11
|
+
const LIST_ITEM = /^\s*([-*+]|\d+[.)])\s+(.*)$/
|
|
12
|
+
const RULE = /^\s*(-{3,}|\*{3,}|_{3,})\s*$/
|
|
13
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/
|
|
14
|
+
const TABLE_DIVIDER = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/
|
|
15
|
+
|
|
16
|
+
// A link target has no spaces, and parentheses only in balanced pairs, as in .../Python_(programming_language).
|
|
17
|
+
const LINK = /\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)/g
|
|
18
|
+
|
|
19
|
+
function emphasis(html) {
|
|
20
|
+
return html
|
|
21
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>')
|
|
22
|
+
.replace(/__([^_\n]+)__/g, '<strong>$1</strong>')
|
|
23
|
+
.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, '$1<em>$2</em>')
|
|
24
|
+
.replace(/~~([^~\n]+)~~/g, '<del>$1</del>')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function inline(text) {
|
|
28
|
+
return String(text).replace(/\u0000/g, '').split(/(`[^`\n]+`)/g).map((part) => {
|
|
29
|
+
if (/^`[^`\n]+`$/.test(part)) return `<code>${escapeHtml(part.slice(1, -1))}</code>`
|
|
30
|
+
// Links are set aside while emphasis is applied, so an underscore or asterisk in a URL stays
|
|
31
|
+
// part of it, and **[a link](…)** still turns bold.
|
|
32
|
+
const links = []
|
|
33
|
+
const marked = part.replace(LINK, (match, label, href) => {
|
|
34
|
+
links.push(`<a data-href="${escapeHtml(href)}">${emphasis(escapeHtml(label))}</a>`)
|
|
35
|
+
return `\u0000${links.length - 1}\u0000`
|
|
36
|
+
})
|
|
37
|
+
return emphasis(escapeHtml(marked)).replace(/\u0000(\d+)\u0000/g, (match, index) => links[Number(index)])
|
|
38
|
+
}).join('')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isBlockStart(line) {
|
|
42
|
+
return FENCE.test(line) || HEADING.test(line) || QUOTE.test(line) || LIST_ITEM.test(line) || RULE.test(line) || TABLE_ROW.test(line)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cells(row) {
|
|
46
|
+
return row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function render(markdown) {
|
|
50
|
+
const lines = String(markdown ?? '').replace(/\r\n?/g, '\n').split('\n')
|
|
51
|
+
const out = []
|
|
52
|
+
let i = 0
|
|
53
|
+
while (i < lines.length) {
|
|
54
|
+
const line = lines[i]
|
|
55
|
+
if (!line.trim()) {
|
|
56
|
+
i += 1
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
const fence = FENCE.exec(line)
|
|
60
|
+
if (fence) {
|
|
61
|
+
const code = []
|
|
62
|
+
i += 1
|
|
63
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(fence[1])) code.push(lines[i++])
|
|
64
|
+
i += 1
|
|
65
|
+
out.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`)
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
const heading = HEADING.exec(line)
|
|
69
|
+
if (heading) {
|
|
70
|
+
const level = heading[1].length <= 2 ? 3 : 4
|
|
71
|
+
out.push(`<h${level}>${inline(heading[2])}</h${level}>`)
|
|
72
|
+
i += 1
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (QUOTE.test(line)) {
|
|
76
|
+
const quote = []
|
|
77
|
+
while (i < lines.length && QUOTE.test(lines[i])) quote.push(lines[i++].replace(QUOTE, ''))
|
|
78
|
+
out.push(`<blockquote>${render(quote.join('\n'))}</blockquote>`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (RULE.test(line)) {
|
|
82
|
+
out.push('<hr>')
|
|
83
|
+
i += 1
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_DIVIDER.test(lines[i + 1])) {
|
|
87
|
+
const head = cells(line)
|
|
88
|
+
i += 2
|
|
89
|
+
const body = []
|
|
90
|
+
while (i < lines.length && TABLE_ROW.test(lines[i])) body.push(cells(lines[i++]))
|
|
91
|
+
out.push(`<table><thead><tr>${head.map((cell) => `<th>${inline(cell)}</th>`).join('')}</tr></thead>`
|
|
92
|
+
+ `<tbody>${body.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const item = LIST_ITEM.exec(line)
|
|
96
|
+
if (item) {
|
|
97
|
+
const ordered = /\d/.test(item[1])
|
|
98
|
+
const items = []
|
|
99
|
+
while (i < lines.length) {
|
|
100
|
+
const next = LIST_ITEM.exec(lines[i])
|
|
101
|
+
if (next && /\d/.test(next[1]) === ordered) {
|
|
102
|
+
items.push(next[2])
|
|
103
|
+
} else if (lines[i].trim() && /^\s{2,}/.test(lines[i]) && items.length) {
|
|
104
|
+
items[items.length - 1] += `\n${lines[i].trim()}`
|
|
105
|
+
} else {
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
i += 1
|
|
109
|
+
}
|
|
110
|
+
const tag = ordered ? 'ol' : 'ul'
|
|
111
|
+
out.push(`<${tag}>${items.map((text) => `<li>${text.split('\n').map(inline).join('<br>')}</li>`).join('')}</${tag}>`)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const paragraph = []
|
|
115
|
+
while (i < lines.length && lines[i].trim() && !(paragraph.length && isBlockStart(lines[i]))) paragraph.push(lines[i++])
|
|
116
|
+
out.push(`<p>${paragraph.map(inline).join('<br>')}</p>`)
|
|
117
|
+
}
|
|
118
|
+
return out.join('')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
root.DshMarkdown = { render, escapeHtml }
|
|
122
|
+
})(globalThis)
|
package/ui/toolbar.css
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--surface: #ffffff;
|
|
3
|
+
--text: #1f1f1f;
|
|
4
|
+
--muted: #8a8a8e;
|
|
5
|
+
--soft: #f4f4f5;
|
|
6
|
+
--hairline: rgba(0, 0, 0, 0.08);
|
|
7
|
+
--hover: rgba(0, 0, 0, 0.05);
|
|
8
|
+
--press: rgba(0, 0, 0, 0.09);
|
|
9
|
+
--accent: #4d6bfe;
|
|
10
|
+
--danger: #d4302b;
|
|
11
|
+
--shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06), 0 8px 20px rgba(0, 0, 0, 0.1);
|
|
12
|
+
--code: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
13
|
+
color-scheme: light;
|
|
14
|
+
}
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--surface: #2b2b2e;
|
|
18
|
+
--text: #f2f2f3;
|
|
19
|
+
--muted: #9d9da3;
|
|
20
|
+
--soft: #37373b;
|
|
21
|
+
--hairline: rgba(255, 255, 255, 0.1);
|
|
22
|
+
--hover: rgba(255, 255, 255, 0.07);
|
|
23
|
+
--press: rgba(255, 255, 255, 0.12);
|
|
24
|
+
--accent: #8197ff;
|
|
25
|
+
--danger: #ff6b63;
|
|
26
|
+
--shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12), 0 2px 6px rgba(0, 0, 0, 0.3), 0 8px 20px rgba(0, 0, 0, 0.4);
|
|
27
|
+
color-scheme: dark;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* { box-sizing: border-box; margin: 0; }
|
|
32
|
+
/* Only the root hides overflow. Set on body as well, it would stop propagating to the viewport and
|
|
33
|
+
clip body's own box, cutting off the ⋯ menu, which is positioned outside the toolbar's flow. */
|
|
34
|
+
html { background: transparent; overflow: hidden; }
|
|
35
|
+
/* The margins hold the whole shadow; a clipped shadow shows as a hard grey box around the card. */
|
|
36
|
+
body {
|
|
37
|
+
background: transparent;
|
|
38
|
+
padding: 12px 18px 24px;
|
|
39
|
+
font: 13px/1.5 -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", "Microsoft YaHei UI", sans-serif;
|
|
40
|
+
color: var(--text);
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
42
|
+
user-select: none;
|
|
43
|
+
-webkit-user-select: none;
|
|
44
|
+
cursor: default;
|
|
45
|
+
}
|
|
46
|
+
.stage { position: relative; display: inline-block; vertical-align: top; }
|
|
47
|
+
.card { background: var(--surface); box-shadow: var(--shadow); }
|
|
48
|
+
svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
|
|
49
|
+
|
|
50
|
+
button, select, textarea { font: inherit; color: inherit; }
|
|
51
|
+
button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; border: 0; background: none; border-radius: 8px; cursor: default; }
|
|
52
|
+
/* Hover waits for a real pointer movement after each show (data-pointer, see toolbar.js). */
|
|
53
|
+
[data-pointer] button:hover { background: var(--hover); }
|
|
54
|
+
button:active, [data-pointer] button:active { background: var(--press); }
|
|
55
|
+
button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
56
|
+
.icon-button { width: 30px; height: 30px; padding: 0; color: var(--text); }
|
|
57
|
+
.icon-button[disabled] { opacity: 0.35; pointer-events: none; }
|
|
58
|
+
|
|
59
|
+
/* Toolbar */
|
|
60
|
+
.bar { display: none; align-items: center; gap: 2px; padding: 4px; border-radius: 12px; white-space: nowrap; cursor: grab; }
|
|
61
|
+
[data-mode="bar"] .bar { display: inline-flex; }
|
|
62
|
+
.bar-skills { display: flex; gap: 2px; }
|
|
63
|
+
.skill { height: 32px; padding: 0 10px 0 9px; font-weight: 500; letter-spacing: 0.1px; }
|
|
64
|
+
.skill.done svg { color: #1f9d55; }
|
|
65
|
+
.divider { width: 1px; height: 16px; margin: 0 4px; background: var(--hairline); }
|
|
66
|
+
.more-button[aria-expanded="true"] { background: var(--press); }
|
|
67
|
+
|
|
68
|
+
/* ⋯ menu, opened below the more button */
|
|
69
|
+
.menu { display: none; position: absolute; min-width: 190px; padding: 6px; border-radius: 12px; z-index: 1; }
|
|
70
|
+
[data-mode="bar"][data-menu="open"] .menu { display: block; }
|
|
71
|
+
.menu-item { display: flex; width: 100%; justify-content: flex-start; height: 34px; padding: 0 10px; gap: 10px; white-space: nowrap; }
|
|
72
|
+
.menu-item.plain { padding-left: 12px; }
|
|
73
|
+
.menu-divider { height: 1px; margin: 6px 8px; background: var(--hairline); }
|
|
74
|
+
.menu-skills:empty + .menu-divider { display: none; }
|
|
75
|
+
|
|
76
|
+
/* Result panel */
|
|
77
|
+
.panel { display: none; flex-direction: column; width: 420px; max-height: var(--room, none); border-radius: 14px; overflow: hidden; }
|
|
78
|
+
[data-mode="panel"] .panel { display: flex; }
|
|
79
|
+
.panel-head { flex: none; display: flex; align-items: center; gap: 2px; height: 44px; padding: 0 6px 0 14px; cursor: grab; }
|
|
80
|
+
[data-dragging="true"], [data-dragging="true"] * { cursor: grabbing !important; }
|
|
81
|
+
.panel-icon { display: inline-flex; color: var(--accent); margin-right: 6px; }
|
|
82
|
+
.panel-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
|
|
83
|
+
.panel-app { min-width: 0; margin-left: 8px; color: var(--muted); font-size: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
84
|
+
.spacer { flex: 1; }
|
|
85
|
+
/* Drawn by us rather than as a native pop-up button; the list that opens is still the system menu. */
|
|
86
|
+
.panel-language {
|
|
87
|
+
display: none; height: 26px; max-width: 120px; margin-right: 4px; padding: 0 24px 0 10px;
|
|
88
|
+
border: 0; border-radius: 7px; outline: 0; -webkit-appearance: none; appearance: none;
|
|
89
|
+
background: var(--soft) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8a8e' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 7px center / 12px 12px;
|
|
90
|
+
color: var(--text); font-size: 12px; font-weight: 500; line-height: 26px; cursor: default;
|
|
91
|
+
text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
|
|
92
|
+
}
|
|
93
|
+
[data-pointer] .panel-language:hover { background-color: var(--press); }
|
|
94
|
+
[data-kind="translate"] .panel-language { display: inline-block; }
|
|
95
|
+
.tool { width: 28px; height: 28px; color: var(--muted); }
|
|
96
|
+
[data-pointer] .tool:hover { color: var(--text); }
|
|
97
|
+
.tool[aria-pressed="true"] { color: var(--accent); background: var(--hover); }
|
|
98
|
+
.tool.done { color: #1f9d55; }
|
|
99
|
+
[data-kind="ask"] .tool[data-tool="retry"] { display: none; }
|
|
100
|
+
|
|
101
|
+
.panel-body { flex: 0 1 auto; min-height: 0; padding: 0 16px 14px; max-height: 340px; overflow-y: auto; overscroll-behavior: contain; }
|
|
102
|
+
[data-fill] .panel { height: var(--room); }
|
|
103
|
+
[data-fill] .panel-body { flex-grow: 1; max-height: none; }
|
|
104
|
+
.panel-body::-webkit-scrollbar { width: 8px; }
|
|
105
|
+
.panel-body::-webkit-scrollbar-thumb { background: var(--hairline); border-radius: 4px; border: 2px solid var(--surface); }
|
|
106
|
+
.source {
|
|
107
|
+
padding: 7px 10px; border-radius: 8px; background: var(--soft); color: var(--muted); font-size: 12px; line-height: 1.5;
|
|
108
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; white-space: pre-wrap; word-break: break-word;
|
|
109
|
+
}
|
|
110
|
+
.source.expanded { display: block; -webkit-line-clamp: unset; }
|
|
111
|
+
.result { margin-top: 10px; }
|
|
112
|
+
[data-kind="ask"] .result { display: none; }
|
|
113
|
+
.turn { display: flex; flex-direction: column; margin-top: 12px; }
|
|
114
|
+
.question { align-self: flex-end; max-width: 85%; padding: 6px 10px; border-radius: 10px 10px 2px 10px; background: var(--soft); white-space: pre-wrap; word-break: break-word; }
|
|
115
|
+
.answer { margin-top: 8px; }
|
|
116
|
+
|
|
117
|
+
.pending::after { content: ""; display: inline-block; width: 18px; height: 6px; margin-left: 2px;
|
|
118
|
+
background: radial-gradient(circle, var(--muted) 2px, transparent 2.5px) 0 0 / 6px 6px repeat-x;
|
|
119
|
+
animation: pending 1s steps(4) infinite; vertical-align: middle; }
|
|
120
|
+
.pending:empty::before { content: attr(data-pending); color: var(--muted); }
|
|
121
|
+
@keyframes pending { from { width: 0 } to { width: 24px } }
|
|
122
|
+
.failure { color: var(--danger); }
|
|
123
|
+
.failure small { display: block; margin-top: 2px; color: var(--muted); font-size: 11.5px; word-break: break-word; }
|
|
124
|
+
.failure button { margin-top: 6px; height: 26px; padding: 0 10px; background: var(--soft); color: var(--text); }
|
|
125
|
+
|
|
126
|
+
.composer { flex: none; display: none; align-items: flex-end; gap: 6px; padding: 8px 10px 10px 14px; border-top: 1px solid var(--hairline); }
|
|
127
|
+
[data-kind="ask"] .composer, [data-composer="shown"] .composer { display: flex; }
|
|
128
|
+
.composer textarea {
|
|
129
|
+
flex: 1; min-height: 22px; max-height: 96px; padding: 4px 0; border: 0; outline: 0; resize: none; background: transparent;
|
|
130
|
+
line-height: 1.5; user-select: text; -webkit-user-select: text; cursor: text;
|
|
131
|
+
}
|
|
132
|
+
.composer textarea::placeholder { color: var(--muted); }
|
|
133
|
+
.send { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: #fff; }
|
|
134
|
+
[data-pointer] .send:hover { background: var(--accent); filter: brightness(1.08); }
|
|
135
|
+
.send[disabled] { opacity: 0.35; }
|
|
136
|
+
|
|
137
|
+
/* Model output */
|
|
138
|
+
.markdown { font-size: 13.5px; line-height: 1.65; user-select: text; -webkit-user-select: text; cursor: text; word-break: break-word; }
|
|
139
|
+
.markdown > * + * { margin-top: 0.6em; }
|
|
140
|
+
.markdown h1, .markdown h2, .markdown h3, .markdown h4 { font-size: 14px; font-weight: 600; }
|
|
141
|
+
.markdown ul, .markdown ol { padding-left: 1.4em; }
|
|
142
|
+
.markdown li + li { margin-top: 0.2em; }
|
|
143
|
+
.markdown code { font-family: var(--code); font-size: 12px; padding: 1px 4px; border-radius: 4px; background: var(--soft); }
|
|
144
|
+
.markdown pre { padding: 8px 10px; border-radius: 8px; background: var(--soft); overflow-x: auto; }
|
|
145
|
+
.markdown pre code { padding: 0; background: none; font-size: 12px; line-height: 1.5; }
|
|
146
|
+
.markdown blockquote { padding-left: 10px; border-left: 3px solid var(--hairline); color: var(--muted); }
|
|
147
|
+
.markdown a { color: var(--accent); text-decoration: none; cursor: pointer; }
|
|
148
|
+
[data-pointer] .markdown a:hover { text-decoration: underline; }
|
|
149
|
+
.markdown table { border-collapse: collapse; font-size: 12.5px; }
|
|
150
|
+
.markdown th, .markdown td { padding: 4px 8px; border: 1px solid var(--hairline); text-align: left; vertical-align: top; }
|
|
151
|
+
.markdown th { background: var(--soft); font-weight: 600; }
|
|
152
|
+
.markdown hr { border: 0; border-top: 1px solid var(--hairline); }
|
package/ui/toolbar.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:">
|
|
7
|
+
<title>DSH Selection</title>
|
|
8
|
+
<link rel="stylesheet" href="toolbar.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body data-mode="hidden">
|
|
11
|
+
<div class="stage">
|
|
12
|
+
<div class="card bar" role="toolbar">
|
|
13
|
+
<div class="bar-skills"></div>
|
|
14
|
+
<span class="divider" aria-hidden="true"></span>
|
|
15
|
+
<button type="button" class="icon-button more-button" data-i18n-label="more" aria-haspopup="menu" aria-expanded="false"></button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="card menu" role="menu">
|
|
19
|
+
<div class="menu-skills"></div>
|
|
20
|
+
<div class="menu-divider" aria-hidden="true"></div>
|
|
21
|
+
<button type="button" class="menu-item plain" role="menuitem" data-menu-action="disable-app" data-i18n="disableApp"></button>
|
|
22
|
+
<button type="button" class="menu-item plain" role="menuitem" data-menu-action="settings" data-i18n="settings"></button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<section class="card panel" aria-live="polite">
|
|
26
|
+
<header class="panel-head">
|
|
27
|
+
<span class="panel-icon"></span>
|
|
28
|
+
<h1 class="panel-title"></h1>
|
|
29
|
+
<span class="panel-app"></span>
|
|
30
|
+
<span class="spacer"></span>
|
|
31
|
+
<select class="panel-language" data-i18n-label="translateTo"></select>
|
|
32
|
+
<button type="button" class="icon-button tool" data-tool="pin" data-i18n-label="pin" aria-pressed="false"></button>
|
|
33
|
+
<button type="button" class="icon-button tool" data-tool="copy" data-i18n-label="copyResult"></button>
|
|
34
|
+
<button type="button" class="icon-button tool" data-tool="retry" data-i18n-label="retry"></button>
|
|
35
|
+
<button type="button" class="icon-button tool" data-tool="close" data-i18n-label="close"></button>
|
|
36
|
+
</header>
|
|
37
|
+
<div class="panel-body">
|
|
38
|
+
<p class="source" title=""></p>
|
|
39
|
+
<div class="result markdown"></div>
|
|
40
|
+
<div class="thread"></div>
|
|
41
|
+
</div>
|
|
42
|
+
<form class="composer">
|
|
43
|
+
<textarea rows="1" maxlength="2000" data-i18n-placeholder="askPlaceholder"></textarea>
|
|
44
|
+
<button type="submit" class="icon-button send" data-i18n-label="send"></button>
|
|
45
|
+
</form>
|
|
46
|
+
</section>
|
|
47
|
+
</div>
|
|
48
|
+
<script src="icons.js"></script>
|
|
49
|
+
<script src="markdown.js"></script>
|
|
50
|
+
<script src="toolbar.js"></script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|