clay-server 4.5.1-beta.1 → 4.6.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/lib/public/css/session-github.css +12 -2
- package/lib/public/css/stt.css +2 -0
- package/lib/public/modules/home-mate-chat.js +8 -2
- package/lib/public/modules/input.js +9 -1
- package/lib/public/modules/session-github.js +11 -4
- package/lib/public/modules/split-view.js +4 -1
- package/lib/public/modules/stt-meter.js +6 -1
- package/lib/public/modules/stt-recording.js +33 -6
- package/lib/public/modules/stt.js +3 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
gap: 3px;
|
|
6
6
|
flex: 1 1 auto;
|
|
7
7
|
min-width: 0;
|
|
8
|
-
overflow:
|
|
8
|
+
overflow: visible;
|
|
9
9
|
}
|
|
10
10
|
.session-work-column > .session-item-text,
|
|
11
11
|
.session-work-column > .mobile-session-title {
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
font-size: 11px;
|
|
28
28
|
line-height: 18px;
|
|
29
29
|
}
|
|
30
|
+
.session-work-column .session-row-github-links {
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
gap: 3px 5px;
|
|
33
|
+
overflow: visible;
|
|
34
|
+
}
|
|
30
35
|
.session-github-link {
|
|
31
36
|
display: inline-flex;
|
|
32
37
|
align-items: center;
|
|
@@ -54,7 +59,11 @@
|
|
|
54
59
|
.mobile-session-item .session-github-link { min-height: 28px; padding-right: 4px; }
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
.session-item:has(.session-work-column) {
|
|
62
|
+
.session-item:has(.session-work-column) {
|
|
63
|
+
height: auto;
|
|
64
|
+
min-height: 52px;
|
|
65
|
+
contain: layout;
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
.github-kind-pr.github-state-closed .github-work-status svg { color: #f85149; }
|
|
60
69
|
@media (max-width: 600px) {
|
|
@@ -108,5 +117,6 @@
|
|
|
108
117
|
.github-work-entry-meta { color: var(--text-muted, #888); font-size: 11px; overflow-wrap: anywhere; }
|
|
109
118
|
@media (max-width: 600px) {
|
|
110
119
|
#header-github-links { max-width: 120px; }
|
|
120
|
+
.mobile-session-item:has(.session-work-column) { align-items: flex-start; }
|
|
111
121
|
.mobile-session-item .github-work-more { min-height: 28px; }
|
|
112
122
|
}
|
package/lib/public/css/stt.css
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
.stt-progress button { border: 1px solid var(--border); border-radius: 6px; background: transparent; color: var(--text); font: inherit; cursor: pointer; padding: 5px 8px; }
|
|
29
29
|
.stt-progress-active { flex: 1 1 0; flex-wrap: nowrap; padding: 0 8px 0 0; height: 62px; }
|
|
30
30
|
.stt-progress-active .stt-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); min-width: 0; }
|
|
31
|
+
.stt-progress-active.stt-auto-send .stt-status { position: static; width: auto; height: auto; clip-path: none; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
32
|
+
.stt-progress-active.stt-auto-send .stt-meter { opacity: .58; }
|
|
31
33
|
.stt-progress-pending .stt-status { position: static; width: auto; height: auto; clip-path: none; }
|
|
32
34
|
.stt-progress-pending .stt-meter { display: none; }
|
|
33
35
|
.stt-meter canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stopSTT } from './stt-controller.js';
|
|
2
|
+
import { hasRecordedSpeechFor, requestRecordedSend } from './stt-recording.js';
|
|
2
3
|
// home-mate-chat.js - Embedded conversation surface for the selected mate.
|
|
3
4
|
import { store } from './store.js';
|
|
4
5
|
import { getWs } from './ws-ref.js';
|
|
@@ -86,6 +87,7 @@ function renderSessionModel() {
|
|
|
86
87
|
sessionModelEl.setAttribute("aria-label", "Model for this conversation: " + label);
|
|
87
88
|
}
|
|
88
89
|
function submitMessage() {
|
|
90
|
+
if (requestRecordedSend(inputEl, submitMessage)) return true;
|
|
89
91
|
stopSTT();
|
|
90
92
|
var mateId = store.get('homeChatMateId');
|
|
91
93
|
var text = inputEl ? inputEl.value.trim() : "";
|
|
@@ -146,7 +148,7 @@ function bindComposer() {
|
|
|
146
148
|
bound = true;
|
|
147
149
|
inputEl.addEventListener("input", function () {
|
|
148
150
|
resizeInput();
|
|
149
|
-
sendBtn.disabled = streaming || hasPendingHomeDebateQuestion(messages) || !store.get('homeChatMateId') || !hasCommittedSessionModel() || !inputEl.value.trim();
|
|
151
|
+
sendBtn.disabled = streaming || hasPendingHomeDebateQuestion(messages) || !store.get('homeChatMateId') || !hasCommittedSessionModel() || (!inputEl.value.trim() && !hasRecordedSpeechFor(inputEl));
|
|
150
152
|
});
|
|
151
153
|
bindHomeComposerSubmission(inputEl, sendBtn, submitMessage);
|
|
152
154
|
sessionModelChooseEl.addEventListener("click", function () {
|
|
@@ -157,6 +159,10 @@ function bindComposer() {
|
|
|
157
159
|
handleHomeMateModelConfirmed(event.detail || {});
|
|
158
160
|
});
|
|
159
161
|
}
|
|
162
|
+
store.subscribe(function (state, previous) {
|
|
163
|
+
if ((state.speechActive === previous.speechActive && state.speechStatuses === previous.speechStatuses) || !inputEl || !sendBtn) return;
|
|
164
|
+
renderHomeChat();
|
|
165
|
+
});
|
|
160
166
|
function ensureDom() {
|
|
161
167
|
if (!chatEl) chatEl = document.getElementById("home-mate-chat");
|
|
162
168
|
if (!stageEl) stageEl = chatEl ? chatEl.querySelector(".home-mate-chat-stage") : null;
|
|
@@ -224,7 +230,7 @@ export function renderHomeChat() {
|
|
|
224
230
|
inputEl.disabled = !mateId || awaitingQuestion || !hasCommittedSessionModel(); if (debateLaunching || mateCreationActive || !!debatePhase) inputEl.disabled = true;
|
|
225
231
|
inputEl.placeholder = debateLaunching ? "Preparing your debate…" : (mateCreationActive ? "Use the interview cards above" : (debatePhase ? "Debate session" : (awaitingQuestion ? "Answer the question above to continue" : (mateId ? "Message " + mateName : "Message"))));
|
|
226
232
|
sendBtn.disabled = !mateId || streaming || awaitingQuestion || !hasCommittedSessionModel();
|
|
227
|
-
if (debateLaunching || mateCreationActive || !!debatePhase || !inputEl.value.trim()) sendBtn.disabled = true;
|
|
233
|
+
if (debateLaunching || mateCreationActive || !!debatePhase || (!inputEl.value.trim() && !hasRecordedSpeechFor(inputEl))) sendBtn.disabled = true;
|
|
228
234
|
renderSessionModel();
|
|
229
235
|
renderHomeDebateControls(messages, activeSessionRequestId, function () { store.set({ homeDebateTopicFormOpen: true }); setHomeSubSurface("debates"); });
|
|
230
236
|
if (animateSwitch) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stopSTT } from './stt-controller.js';
|
|
2
|
+
import { hasRecordedSpeechFor, requestRecordedSend } from './stt-recording.js';
|
|
2
3
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
4
|
import { setRewindMode, isRewindMode } from './rewind.js';
|
|
4
5
|
import { renderPicker as renderContextPicker } from './context-sources.js';
|
|
@@ -34,7 +35,8 @@ export function hasSendableContent() {
|
|
|
34
35
|
(ctx && ctx.inputEl && ctx.inputEl.value.trim()) ||
|
|
35
36
|
pendingPastes.length > 0 ||
|
|
36
37
|
pendingImages.length > 0 ||
|
|
37
|
-
pendingFiles.length > 0
|
|
38
|
+
pendingFiles.length > 0 ||
|
|
39
|
+
(ctx && hasRecordedSpeechFor(ctx.inputEl))
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -102,6 +104,11 @@ store.subscribe(function (state, previous) {
|
|
|
102
104
|
if (Object.keys(scheduleAdmissionMap()).length > 0) store.set({ pendingScheduleAdmissions: {}, pendingScheduleAdmission: null });
|
|
103
105
|
});
|
|
104
106
|
|
|
107
|
+
store.subscribe(function (state, previous) {
|
|
108
|
+
if ((state.speechActive === previous.speechActive && state.speechStatuses === previous.speechStatuses) || !ctx || !ctx.processing || !ctx.setSendBtnMode) return;
|
|
109
|
+
ctx.setSendBtnMode(hasSendableContent() ? 'send' : 'stop');
|
|
110
|
+
});
|
|
111
|
+
|
|
105
112
|
function showPreThinkingForTurn() {
|
|
106
113
|
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
107
114
|
ctx.showMatePreThinking();
|
|
@@ -112,6 +119,7 @@ function showPreThinkingForTurn() {
|
|
|
112
119
|
|
|
113
120
|
// --- Send ---
|
|
114
121
|
export function sendMessage() {
|
|
122
|
+
if (ctx && requestRecordedSend(ctx.inputEl, sendMessage)) return;
|
|
115
123
|
stopSTT();
|
|
116
124
|
// Debate ended mode intercept: route to resume handler
|
|
117
125
|
if (ctx.isDebateEndedMode && ctx.isDebateEndedMode() && ctx.handleDebateEndedSend) {
|
|
@@ -26,10 +26,17 @@ export function orderedGithubWork(session) {
|
|
|
26
26
|
if (latestIssue) links = [latestIssue].concat(links.filter(function (link) { return link !== latestIssue; }));
|
|
27
27
|
return links;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
function compactGithubWork(links, sessionRow) {
|
|
30
|
+
if (!sessionRow) return links.slice(0, 1);
|
|
31
|
+
var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
|
|
32
|
+
var latestPr = links.find(function (link) { return link.kind === 'pr'; });
|
|
33
|
+
return [latestIssue, latestPr].filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
|
|
30
36
|
var links = orderedGithubWork(session);
|
|
31
37
|
if (!links.length) return '';
|
|
32
|
-
|
|
38
|
+
var displayed = detailed || expanded ? links : compactGithubWork(links, sessionRow);
|
|
39
|
+
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + (sessionRow ? ' session-row-github-links' : '') + '">' + displayed.map(function (link) {
|
|
33
40
|
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
|
|
34
41
|
var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
|
|
35
42
|
var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + state + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
|
|
@@ -43,10 +50,10 @@ export function githubWorkMarkup(session, detailed, expanded) {
|
|
|
43
50
|
});
|
|
44
51
|
}
|
|
45
52
|
return html;
|
|
46
|
-
}).join('') + (!detailed && !expanded && links.length >
|
|
53
|
+
}).join('') + (!detailed && !expanded && links.length > displayed.length ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked GitHub items" aria-haspopup="dialog" aria-expanded="false" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - displayed.length) + '</button>' : '') + '</span>';
|
|
47
54
|
}
|
|
48
55
|
export function appendGithubWork(row, session, title) {
|
|
49
|
-
var html = githubWorkMarkup(session, false);
|
|
56
|
+
var html = githubWorkMarkup(session, false, false, true);
|
|
50
57
|
bindGithubWorkPopover();
|
|
51
58
|
if (!html) return;
|
|
52
59
|
var column = document.createElement('span');
|
|
@@ -342,7 +342,10 @@ function handleSessionDragStart(event) {
|
|
|
342
342
|
|
|
343
343
|
function handleSidebarSessionClick(event) {
|
|
344
344
|
if (!store.get('splitPanes') || event.button !== 0) return;
|
|
345
|
-
|
|
345
|
+
// This document capture listener runs before a row's bubble handlers. Leave
|
|
346
|
+
// real controls alone so GitHub anchors retain native click, modified-click,
|
|
347
|
+
// and keyboard navigation rather than selecting their owning session.
|
|
348
|
+
if (event.target.closest(".session-close-btn, .session-more-btn, .session-github-link, .github-work-more")) return;
|
|
346
349
|
var item = event.target.closest(".session-item[data-session-id], .session-loop-child[data-session-id]");
|
|
347
350
|
if (!item) return;
|
|
348
351
|
var sessionId = parseInt(item.dataset.sessionId, 10);
|
|
@@ -19,7 +19,12 @@ export function createSpeechMeter(id) {
|
|
|
19
19
|
canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
|
|
20
20
|
}
|
|
21
21
|
context.setTransform(ratio, 0, 0, ratio, 0, 0); context.clearRect(0, 0, width, height);
|
|
22
|
-
|
|
22
|
+
var style = getComputedStyle(element);
|
|
23
|
+
var gradient = context.createLinearGradient(0, 0, width, 0);
|
|
24
|
+
gradient.addColorStop(0, style.getPropertyValue('--brand-green').trim() || style.color);
|
|
25
|
+
gradient.addColorStop(0.5, style.getPropertyValue('--brand-blue').trim() || style.color);
|
|
26
|
+
gradient.addColorStop(1, style.getPropertyValue('--brand-green').trim() || style.color);
|
|
27
|
+
context.fillStyle = gradient;
|
|
23
28
|
var signal = active.audioSignal; var levels = signal ? signal.levels : [];
|
|
24
29
|
var age = signal ? Date.now() - signal.updatedAt : 0;
|
|
25
30
|
var fade = Math.max(0, 1 - Math.max(0, age - 250) / 900);
|
|
@@ -7,7 +7,7 @@ import { claimSpeech, currentSpeech, writeSpeech, usableSpeechInput, setSpeechSt
|
|
|
7
7
|
async function upload(active, blob) {
|
|
8
8
|
if (!currentSpeech(active)) return;
|
|
9
9
|
active.phase = 'transcribing'; store.set({ speechActive: active });
|
|
10
|
-
setSpeechStatus(active.id, 'Transcribing…');
|
|
10
|
+
setSpeechStatus(active.id, active.autoSend ? 'Transcribing before sending…' : 'Transcribing…');
|
|
11
11
|
speechTrace(active, 'upload-start', { bytes: blob.size });
|
|
12
12
|
try {
|
|
13
13
|
var response = await fetch('/api/speech/transcribe', { method: 'POST', signal: active.abortController.signal,
|
|
@@ -16,8 +16,10 @@ async function upload(active, blob) {
|
|
|
16
16
|
speechTrace(active, 'upload-response', { status: response.status, transcriptCharacters: typeof result.text === 'string' ? result.text.length : 0 });
|
|
17
17
|
if (!response.ok) throw new Error(result.error || 'Transcription failed. Please try again.');
|
|
18
18
|
if (!currentSpeech(active)) return;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
var transcript = typeof result.text === 'string' ? result.text : '';
|
|
20
|
+
var accepted = writeSpeech(active, transcript);
|
|
21
|
+
stopSTT(transcript ? (active.autoSend ? 'Sending transcribed voice input…' : 'Review your draft before sending.') : 'No speech detected. Try recording again.');
|
|
22
|
+
return !!(accepted && transcript.trim());
|
|
21
23
|
} catch (error) {
|
|
22
24
|
if (!currentSpeech(active)) return;
|
|
23
25
|
var retry = { id: active.id, input: active.input, context: active.context, value: active.lastValue,
|
|
@@ -25,6 +27,7 @@ async function upload(active, blob) {
|
|
|
25
27
|
failSTT(error.name === 'AbortError' ? 'Transcription timed out. Please try again.' : error.message);
|
|
26
28
|
store.set({ speechRetry: retry });
|
|
27
29
|
setTimeout(function () { if (store.get('speechRetry') === retry) store.set({ speechRetry: null }); }, 300000);
|
|
30
|
+
return false;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
function initialize(id, input, model, language) {
|
|
@@ -103,8 +106,8 @@ export async function startRecording(id, input, model) {
|
|
|
103
106
|
}
|
|
104
107
|
export async function finishRecording() {
|
|
105
108
|
var active = store.get('speechActive');
|
|
106
|
-
if (!active || !active.model) { stopSTT(); return; }
|
|
107
|
-
if (active.phase !== 'recording') return;
|
|
109
|
+
if (!active || !active.model) { stopSTT(); return false; }
|
|
110
|
+
if (active.phase !== 'recording') return false;
|
|
108
111
|
speechTrace(active, 'finish-requested', { bytes: active.bytes });
|
|
109
112
|
active.phase = 'finishing'; clearInterval(active.clock); store.set({ speechActive: active });
|
|
110
113
|
await active.capture.finish();
|
|
@@ -117,9 +120,33 @@ export async function finishRecording() {
|
|
|
117
120
|
} else {
|
|
118
121
|
var blob = wavRecording(active.chunks); active.chunks = [];
|
|
119
122
|
active.timeout = setTimeout(function () { active.abortController.abort(); }, 125000);
|
|
120
|
-
|
|
123
|
+
return upload(active, blob);
|
|
121
124
|
}
|
|
122
125
|
}
|
|
126
|
+
export function hasRecordedSpeechFor(input) {
|
|
127
|
+
var active = store.get('speechActive');
|
|
128
|
+
return !!(active && active.input === input && active.model && active.model.mode === 'recorded' &&
|
|
129
|
+
(active.phase === 'recording' || active.autoSend));
|
|
130
|
+
}
|
|
131
|
+
export function requestRecordedSend(input, send) {
|
|
132
|
+
var active = store.get('speechActive');
|
|
133
|
+
if (!active || active.input !== input || !active.model || active.model.mode !== 'recorded') return false;
|
|
134
|
+
if (active.autoSend) return true;
|
|
135
|
+
if (active.phase !== 'recording') return false;
|
|
136
|
+
active.autoSend = true;
|
|
137
|
+
store.set({ speechActive: active });
|
|
138
|
+
setSpeechStatus(active.id, 'Transcribing before sending…');
|
|
139
|
+
finishRecording().then(function (sendable) {
|
|
140
|
+
if (!sendable || !active.autoSend || active.context !== speechContext() ||
|
|
141
|
+
!usableSpeechInput(active.input) || active.input.value !== active.lastValue || store.get('speechActive')) return;
|
|
142
|
+
active.autoSend = false;
|
|
143
|
+
send();
|
|
144
|
+
}, function (error) {
|
|
145
|
+
if (!currentSpeech(active)) return;
|
|
146
|
+
failSTT(error && error.message ? error.message : 'Voice input could not finish. Please try again.');
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
123
150
|
export function retryRecording() {
|
|
124
151
|
var retry = store.get('speechRetry');
|
|
125
152
|
if (!retry || retry.context !== speechContext() || retry.input.value !== retry.value || !usableSpeechInput(retry.input)) { store.set({ speechRetry: null }); return; }
|
|
@@ -72,7 +72,9 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
72
72
|
var model = voiceModel(); var provider = voiceProvider(model.provider);
|
|
73
73
|
var browser = model.provider === 'browser' ? browserSpeechInfo() : null;
|
|
74
74
|
var pending = own && active.model && active.phase !== 'recording';
|
|
75
|
+
var autoSend = own && !!active.autoSend;
|
|
75
76
|
composer.classList.toggle('stt-recording', own);
|
|
77
|
+
row.classList.toggle('stt-auto-send', autoSend);
|
|
76
78
|
row.classList.toggle('stt-progress-active', own);
|
|
77
79
|
row.classList.toggle('stt-progress-pending', !!pending);
|
|
78
80
|
var parent = own ? toolbar : composer;
|
|
@@ -83,7 +85,7 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
83
85
|
picker.disabled = input.disabled || input.readOnly || own;
|
|
84
86
|
button.classList.toggle('stt-active', own);
|
|
85
87
|
button.setAttribute('aria-pressed', String(own));
|
|
86
|
-
var action = own ? (pending ? 'Cancel voice input' : (model.mode === 'recorded' ? 'Stop and transcribe' : 'Stop voice input')) : 'Start voice input';
|
|
88
|
+
var action = own ? (autoSend ? 'Cancel pending voice send' : (pending ? 'Cancel voice input' : (model.mode === 'recorded' ? 'Stop and transcribe' : 'Stop voice input'))) : 'Start voice input';
|
|
87
89
|
button.setAttribute('aria-label', action + ' · ' + (provider ? provider.name + ' · ' : '') + model.name);
|
|
88
90
|
button.title = action + ' · ' + model.name;
|
|
89
91
|
var signature = [own, pending, provider && provider.id, browser && browser.id].join(':');
|
package/package.json
CHANGED