@phenx-inc/ctlsurf 0.7.0 → 0.8.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/out/headless/index.mjs +26 -10
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +31 -9
- package/out/preload/index.js +8 -0
- package/out/renderer/assets/{cssMode-eTXVdAkZ.js → cssMode-BQN8v2ok.js} +3 -3
- package/out/renderer/assets/{freemarker2-B5BKaiK4.js → freemarker2-DbxGYYVp.js} +1 -1
- package/out/renderer/assets/{handlebars-BIdLd2wU.js → handlebars-3auU1CAd.js} +1 -1
- package/out/renderer/assets/{html-BXL4cnLS.js → html-D8xFiRmI.js} +1 -1
- package/out/renderer/assets/{htmlMode-46N3XG2c.js → htmlMode-M3MApZ4n.js} +3 -3
- package/out/renderer/assets/{index-dRvutfbl.js → index---H6cxNl.js} +696 -33
- package/out/renderer/assets/{index-Cf-RsxoC.css → index-B-iM7dFC.css} +195 -0
- package/out/renderer/assets/{javascript-n_iZZzDX.js → javascript-BO_ViZM5.js} +2 -2
- package/out/renderer/assets/{jsonMode-DXDczSNu.js → jsonMode-CKp2zvZu.js} +3 -3
- package/out/renderer/assets/{liquid-B1QweUh7.js → liquid-C1eHcrht.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-DqzMqkRk.js → lspLanguageFeatures-CHWJx_Tl.js} +1 -1
- package/out/renderer/assets/{mdx-BCv8lm5e.js → mdx-Qqdtk7fL.js} +1 -1
- package/out/renderer/assets/{python-BLNzYwDv.js → python-DKu7rNbs.js} +1 -1
- package/out/renderer/assets/{razor-CvAww8bG.js → razor-BOMpCo6z.js} +1 -1
- package/out/renderer/assets/{tsMode-C7m6Kr5E.js → tsMode-yAjlPR-D.js} +1 -1
- package/out/renderer/assets/{typescript-DhPw4VVg.js → typescript-BiJRCUcL.js} +1 -1
- package/out/renderer/assets/{xml-B0WLFJ2U.js → xml-D4PvYeQq.js} +1 -1
- package/out/renderer/assets/{yaml-BWyn9Wd7.js → yaml-BeHVkmnS.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/index.ts +7 -0
- package/src/main/orchestrator.ts +38 -9
- package/src/preload/index.ts +11 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/components/SpeakControls.tsx +235 -0
- package/src/renderer/components/VoiceInput.tsx +159 -3
- package/src/renderer/lib/localWhisper.ts +48 -4
- package/src/renderer/lib/speech.ts +299 -0
- package/src/renderer/styles.css +195 -0
|
@@ -8119,6 +8119,201 @@ html, body, #root {
|
|
|
8119
8119
|
margin-bottom: 8px;
|
|
8120
8120
|
}
|
|
8121
8121
|
|
|
8122
|
+
/* Mic source picker (small caret badge on the round mic + dropdown) */
|
|
8123
|
+
.voice-source-btn {
|
|
8124
|
+
position: absolute;
|
|
8125
|
+
bottom: -3px;
|
|
8126
|
+
right: -3px;
|
|
8127
|
+
width: 18px;
|
|
8128
|
+
height: 18px;
|
|
8129
|
+
border-radius: 50%;
|
|
8130
|
+
display: inline-flex;
|
|
8131
|
+
align-items: center;
|
|
8132
|
+
justify-content: center;
|
|
8133
|
+
padding: 0;
|
|
8134
|
+
font-size: 10px;
|
|
8135
|
+
line-height: 1;
|
|
8136
|
+
background: #2a2b3d;
|
|
8137
|
+
border: 1px solid #3b3d57;
|
|
8138
|
+
color: #a9b1d6;
|
|
8139
|
+
cursor: pointer;
|
|
8140
|
+
z-index: 60;
|
|
8141
|
+
}
|
|
8142
|
+
.voice-source-btn:hover {
|
|
8143
|
+
border-color: #7aa2f7;
|
|
8144
|
+
color: #c0caf5;
|
|
8145
|
+
}
|
|
8146
|
+
.voice-source-menu {
|
|
8147
|
+
position: absolute;
|
|
8148
|
+
bottom: 100%;
|
|
8149
|
+
right: 0;
|
|
8150
|
+
margin-bottom: 10px;
|
|
8151
|
+
min-width: 200px;
|
|
8152
|
+
max-width: 280px;
|
|
8153
|
+
padding: 4px;
|
|
8154
|
+
background: #16161e;
|
|
8155
|
+
border: 1px solid #3b3d57;
|
|
8156
|
+
border-radius: 8px;
|
|
8157
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
|
|
8158
|
+
z-index: 300;
|
|
8159
|
+
}
|
|
8160
|
+
.voice-source-head {
|
|
8161
|
+
font-size: 10px;
|
|
8162
|
+
text-transform: uppercase;
|
|
8163
|
+
letter-spacing: 0.04em;
|
|
8164
|
+
color: #565f89;
|
|
8165
|
+
padding: 4px 8px 6px;
|
|
8166
|
+
}
|
|
8167
|
+
.voice-source-item {
|
|
8168
|
+
display: flex;
|
|
8169
|
+
align-items: center;
|
|
8170
|
+
gap: 6px;
|
|
8171
|
+
width: 100%;
|
|
8172
|
+
padding: 5px 8px;
|
|
8173
|
+
border: none;
|
|
8174
|
+
border-radius: 5px;
|
|
8175
|
+
background: transparent;
|
|
8176
|
+
color: #a9b1d6;
|
|
8177
|
+
font-size: 12px;
|
|
8178
|
+
text-align: left;
|
|
8179
|
+
cursor: pointer;
|
|
8180
|
+
}
|
|
8181
|
+
.voice-source-item:hover { background: #1f2335; }
|
|
8182
|
+
.voice-source-item.active { color: #7aa2f7; }
|
|
8183
|
+
.voice-source-check {
|
|
8184
|
+
flex: 0 0 12px;
|
|
8185
|
+
width: 12px;
|
|
8186
|
+
color: #7aa2f7;
|
|
8187
|
+
font-size: 11px;
|
|
8188
|
+
}
|
|
8189
|
+
.voice-source-label {
|
|
8190
|
+
overflow: hidden;
|
|
8191
|
+
text-overflow: ellipsis;
|
|
8192
|
+
white-space: nowrap;
|
|
8193
|
+
}
|
|
8194
|
+
.voice-source-empty {
|
|
8195
|
+
padding: 6px 8px;
|
|
8196
|
+
font-size: 11px;
|
|
8197
|
+
color: #565f89;
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
/* Spoken-replies titlebar control */
|
|
8201
|
+
.speak-controls {
|
|
8202
|
+
position: relative;
|
|
8203
|
+
display: inline-flex;
|
|
8204
|
+
align-items: center;
|
|
8205
|
+
}
|
|
8206
|
+
.speak-btn.active {
|
|
8207
|
+
color: #7aa2f7;
|
|
8208
|
+
border-color: #7aa2f7;
|
|
8209
|
+
}
|
|
8210
|
+
.speak-pct {
|
|
8211
|
+
font-size: 9px;
|
|
8212
|
+
margin-left: 3px;
|
|
8213
|
+
color: #e0af68;
|
|
8214
|
+
}
|
|
8215
|
+
.speak-caret {
|
|
8216
|
+
padding: 0 3px;
|
|
8217
|
+
font-size: 10px;
|
|
8218
|
+
min-width: 0;
|
|
8219
|
+
}
|
|
8220
|
+
.speak-stop {
|
|
8221
|
+
color: #f7768e;
|
|
8222
|
+
border-color: #f7768e;
|
|
8223
|
+
}
|
|
8224
|
+
.speak-stop:hover { background: #2d2030; }
|
|
8225
|
+
.speak-error {
|
|
8226
|
+
position: absolute;
|
|
8227
|
+
top: 100%;
|
|
8228
|
+
right: 0;
|
|
8229
|
+
margin-top: 6px;
|
|
8230
|
+
max-width: 340px;
|
|
8231
|
+
padding: 4px 9px;
|
|
8232
|
+
border-radius: 5px;
|
|
8233
|
+
font-size: 11px;
|
|
8234
|
+
line-height: 1.3;
|
|
8235
|
+
white-space: normal;
|
|
8236
|
+
word-break: break-word;
|
|
8237
|
+
background: #2d2030;
|
|
8238
|
+
color: #f7768e;
|
|
8239
|
+
border: 1px solid #f7768e;
|
|
8240
|
+
z-index: 60;
|
|
8241
|
+
}
|
|
8242
|
+
.speak-menu {
|
|
8243
|
+
position: absolute;
|
|
8244
|
+
top: 100%;
|
|
8245
|
+
right: 0;
|
|
8246
|
+
margin-top: 6px;
|
|
8247
|
+
min-width: 220px;
|
|
8248
|
+
max-width: 280px;
|
|
8249
|
+
padding: 6px;
|
|
8250
|
+
background: #16161e;
|
|
8251
|
+
border: 1px solid #3b3d57;
|
|
8252
|
+
border-radius: 8px;
|
|
8253
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
|
|
8254
|
+
z-index: 300;
|
|
8255
|
+
}
|
|
8256
|
+
.speak-menu-head {
|
|
8257
|
+
font-size: 10px;
|
|
8258
|
+
text-transform: uppercase;
|
|
8259
|
+
letter-spacing: 0.04em;
|
|
8260
|
+
color: #565f89;
|
|
8261
|
+
padding: 6px 6px 4px;
|
|
8262
|
+
}
|
|
8263
|
+
.speak-menu-item {
|
|
8264
|
+
display: flex;
|
|
8265
|
+
align-items: center;
|
|
8266
|
+
gap: 6px;
|
|
8267
|
+
width: 100%;
|
|
8268
|
+
padding: 5px 6px;
|
|
8269
|
+
border: none;
|
|
8270
|
+
border-radius: 5px;
|
|
8271
|
+
background: transparent;
|
|
8272
|
+
color: #a9b1d6;
|
|
8273
|
+
font-size: 12px;
|
|
8274
|
+
text-align: left;
|
|
8275
|
+
cursor: pointer;
|
|
8276
|
+
}
|
|
8277
|
+
.speak-menu-item:hover { background: #1f2335; }
|
|
8278
|
+
.speak-menu-item.active { color: #7aa2f7; }
|
|
8279
|
+
.speak-menu-check {
|
|
8280
|
+
flex: 0 0 12px;
|
|
8281
|
+
width: 12px;
|
|
8282
|
+
color: #7aa2f7;
|
|
8283
|
+
font-size: 11px;
|
|
8284
|
+
}
|
|
8285
|
+
.speak-select {
|
|
8286
|
+
width: 100%;
|
|
8287
|
+
margin: 2px 0 4px;
|
|
8288
|
+
padding: 4px 6px;
|
|
8289
|
+
background: #1f2335;
|
|
8290
|
+
color: #a9b1d6;
|
|
8291
|
+
border: 1px solid #3b3d57;
|
|
8292
|
+
border-radius: 5px;
|
|
8293
|
+
font-size: 12px;
|
|
8294
|
+
}
|
|
8295
|
+
.speak-rate {
|
|
8296
|
+
width: 100%;
|
|
8297
|
+
margin: 2px 0 6px;
|
|
8298
|
+
accent-color: #7aa2f7;
|
|
8299
|
+
}
|
|
8300
|
+
.speak-menu-row {
|
|
8301
|
+
display: flex;
|
|
8302
|
+
gap: 6px;
|
|
8303
|
+
padding: 2px 0 0;
|
|
8304
|
+
}
|
|
8305
|
+
.speak-menu-btn {
|
|
8306
|
+
flex: 1;
|
|
8307
|
+
padding: 5px 6px;
|
|
8308
|
+
background: #2a2b3d;
|
|
8309
|
+
color: #a9b1d6;
|
|
8310
|
+
border: 1px solid #3b3d57;
|
|
8311
|
+
border-radius: 5px;
|
|
8312
|
+
font-size: 12px;
|
|
8313
|
+
cursor: pointer;
|
|
8314
|
+
}
|
|
8315
|
+
.speak-menu-btn:hover { border-color: #7aa2f7; color: #c0caf5; }
|
|
8316
|
+
|
|
8122
8317
|
/* Editor panel */
|
|
8123
8318
|
.editor-panel {
|
|
8124
8319
|
display: flex;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-BiJRCUcL.js";
|
|
2
|
+
import "./index---H6cxNl.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index---H6cxNl.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-CHWJx_Tl.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-CHWJx_Tl.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-DqzMqkRk.js → lspLanguageFeatures-CHWJx_Tl.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index---H6cxNl.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index---H6cxNl.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ctlsurf-worker</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index---H6cxNl.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B-iM7dFC.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
package/src/main/index.ts
CHANGED
|
@@ -106,6 +106,7 @@ const orchestrator = new Orchestrator(
|
|
|
106
106
|
onWorkerMessage: (message) => mainWindow?.webContents.send('worker:message', message),
|
|
107
107
|
onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
|
|
108
108
|
onProjectChanged: (name) => mainWindow?.webContents.send('app:projectChanged', name),
|
|
109
|
+
onAgentMessage: (text) => mainWindow?.webContents.send('agent:message', text),
|
|
109
110
|
onCwdChanged: () => {
|
|
110
111
|
mainWindow?.webContents.send('app:cwdChanged')
|
|
111
112
|
updateProjectBadge(orchestrator.cwd)
|
|
@@ -414,6 +415,12 @@ ipcMain.handle('logchat:set', (_event, enabled: boolean) => {
|
|
|
414
415
|
return { enabled: orchestrator.logChatEnabled }
|
|
415
416
|
})
|
|
416
417
|
|
|
418
|
+
ipcMain.handle('speak:get', () => ({ enabled: orchestrator.speakRepliesEnabled }))
|
|
419
|
+
ipcMain.handle('speak:set', (_event, enabled: boolean) => {
|
|
420
|
+
orchestrator.setSpeakReplies(!!enabled)
|
|
421
|
+
return { enabled: orchestrator.speakRepliesEnabled }
|
|
422
|
+
})
|
|
423
|
+
|
|
417
424
|
// ─── Tracking IPC ─────────────────────────────────
|
|
418
425
|
|
|
419
426
|
ipcMain.handle('tracking:get', () => ({ active: orchestrator.isActiveTabTracking() }))
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface SettingsData {
|
|
|
32
32
|
ctlsurfBaseUrl?: string
|
|
33
33
|
ctlsurfDataspacePageId?: string
|
|
34
34
|
logChat?: boolean
|
|
35
|
+
// Speak agent replies aloud (Electron desktop only). Drives the transcript
|
|
36
|
+
// tailer the same way logChat does, independent of chat logging.
|
|
37
|
+
speakReplies?: boolean
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export interface OrchestratorEvents {
|
|
@@ -44,6 +47,10 @@ export interface OrchestratorEvents {
|
|
|
44
47
|
// Human-readable name of the connected ctlsurf project (folder), or null
|
|
45
48
|
// when no project is connected. Optional — only the desktop header uses it.
|
|
46
49
|
onProjectChanged?: (name: string | null) => void
|
|
50
|
+
// A clean assistant reply from the active agent's transcript, forwarded for
|
|
51
|
+
// text-to-speech. Optional and wired only by the Electron entry — the
|
|
52
|
+
// headless build leaves it unset, so replies are never spoken in a terminal.
|
|
53
|
+
onAgentMessage?: (text: string) => void
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
interface TabState {
|
|
@@ -148,16 +155,30 @@ export class Orchestrator {
|
|
|
148
155
|
return !!this.settings.logChat
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
get speakRepliesEnabled(): boolean {
|
|
159
|
+
return !!this.settings.speakReplies
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
setLogChat(enabled: boolean): void {
|
|
152
163
|
this.settings.logChat = enabled
|
|
153
164
|
this.saveSettings()
|
|
154
165
|
this.bridge.setLoggingEnabled(enabled)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
this.restartActiveChatLogging()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setSpeakReplies(enabled: boolean): void {
|
|
170
|
+
this.settings.speakReplies = enabled
|
|
171
|
+
this.saveSettings()
|
|
172
|
+
this.restartActiveChatLogging()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Re-evaluate the transcript tailer for the active tab against the current
|
|
176
|
+
// logChat / speakReplies flags. Both setters funnel through here.
|
|
177
|
+
private restartActiveChatLogging(): void {
|
|
178
|
+
this.stopChatLogging()
|
|
179
|
+
if (!this.activeTabId) return
|
|
180
|
+
const tab = this.tabs.get(this.activeTabId)
|
|
181
|
+
if (tab) this.startChatLogging(tab.agent, tab.cwd)
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
// Agents with native session transcripts (Claude Code, Codex) get exact
|
|
@@ -165,16 +186,24 @@ export class Orchestrator {
|
|
|
165
186
|
// the terminal screen-scraper bridge.
|
|
166
187
|
private startChatLogging(agent: AgentConfig, cwd: string): void {
|
|
167
188
|
this.stopChatLogging()
|
|
168
|
-
if (!this.settings.logChat) return
|
|
189
|
+
if (!this.settings.logChat && !this.settings.speakReplies) return
|
|
169
190
|
|
|
170
191
|
if (supportsTranscriptLogging(agent.id)) {
|
|
171
192
|
this.transcriptTailer = new TranscriptTailer({
|
|
172
193
|
agentId: agent.id,
|
|
173
194
|
cwd,
|
|
174
|
-
sink: (entry) =>
|
|
195
|
+
sink: (entry) => {
|
|
196
|
+
if (this.settings.logChat) this.workerWs.sendChatLog(entry)
|
|
197
|
+
// Only assistant replies are spoken; user_input entries are skipped.
|
|
198
|
+
if (this.settings.speakReplies && entry.type === 'terminal_output') {
|
|
199
|
+
this.events.onAgentMessage?.(entry.content)
|
|
200
|
+
}
|
|
201
|
+
},
|
|
175
202
|
})
|
|
176
203
|
this.transcriptTailer.start()
|
|
177
|
-
} else {
|
|
204
|
+
} else if (this.settings.logChat) {
|
|
205
|
+
// The screen-scraper feeds ctlsurf chat logging only; TTS needs the clean
|
|
206
|
+
// transcript path, so non-transcript agents aren't spoken aloud.
|
|
178
207
|
this.bridge.startSession()
|
|
179
208
|
}
|
|
180
209
|
}
|
package/src/preload/index.ts
CHANGED
|
@@ -109,6 +109,17 @@ const api = {
|
|
|
109
109
|
setLogChat: (enabled: boolean): Promise<{ enabled: boolean }> =>
|
|
110
110
|
ipcRenderer.invoke('logchat:set', enabled),
|
|
111
111
|
|
|
112
|
+
// Speak agent replies (global, Electron-only)
|
|
113
|
+
getSpeakReplies: (): Promise<{ enabled: boolean }> =>
|
|
114
|
+
ipcRenderer.invoke('speak:get'),
|
|
115
|
+
setSpeakReplies: (enabled: boolean): Promise<{ enabled: boolean }> =>
|
|
116
|
+
ipcRenderer.invoke('speak:set', enabled),
|
|
117
|
+
onAgentMessage: (callback: (text: string) => void) => {
|
|
118
|
+
const listener = (_event: Electron.IpcRendererEvent, text: string) => callback(text)
|
|
119
|
+
ipcRenderer.on('agent:message', listener)
|
|
120
|
+
return () => ipcRenderer.removeListener('agent:message', listener)
|
|
121
|
+
},
|
|
122
|
+
|
|
112
123
|
// Filesystem
|
|
113
124
|
readDir: (dirPath: string): Promise<Array<{ name: string; path: string; isDirectory: boolean }>> =>
|
|
114
125
|
ipcRenderer.invoke('fs:readDir', dirPath),
|
package/src/renderer/App.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
2
|
import { TerminalPanel, destroyTerminal, focusTerminal } from './components/TerminalPanel'
|
|
3
3
|
import { FloatingMic } from './components/FloatingMic'
|
|
4
|
+
import { SpeakControls } from './components/SpeakControls'
|
|
4
5
|
import { CtlsurfPanel } from './components/CtlsurfPanel'
|
|
5
6
|
import { EditorPanel } from './components/EditorPanel'
|
|
6
7
|
import { AgentPicker } from './components/AgentPicker'
|
|
@@ -56,6 +57,9 @@ declare global {
|
|
|
56
57
|
setTracking: (enabled: boolean) => Promise<{ active: boolean }>
|
|
57
58
|
getLogChat: () => Promise<{ enabled: boolean }>
|
|
58
59
|
setLogChat: (enabled: boolean) => Promise<{ enabled: boolean }>
|
|
60
|
+
getSpeakReplies: () => Promise<{ enabled: boolean }>
|
|
61
|
+
setSpeakReplies: (enabled: boolean) => Promise<{ enabled: boolean }>
|
|
62
|
+
onAgentMessage: (callback: (text: string) => void) => () => void
|
|
59
63
|
createProject: () => Promise<{ ok: boolean; folder_id?: string; error?: string }>
|
|
60
64
|
getWebviewInfo: () => Promise<{
|
|
61
65
|
frontendUrl: string; pageUrl?: string; authenticated: boolean;
|
|
@@ -504,6 +508,7 @@ export default function App() {
|
|
|
504
508
|
<line x1="8" y1="23" x2="16" y2="23" />
|
|
505
509
|
</svg>
|
|
506
510
|
</button>
|
|
511
|
+
<SpeakControls />
|
|
507
512
|
<span className="titlebar-separator" />
|
|
508
513
|
{agents.map(a => {
|
|
509
514
|
const activeTab = tabs.find(t => t.id === activeTabId)
|