pi-ui-extend 0.1.70 → 0.1.71
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/dist/app/app.js +22 -1
- package/dist/app/commands/command-host.d.ts +7 -0
- package/dist/app/commands/command-host.js +10 -1
- package/dist/app/commands/command-model-actions.d.ts +1 -1
- package/dist/app/commands/command-model-actions.js +36 -2
- package/dist/app/commands/command-navigation-actions.d.ts +1 -1
- package/dist/app/commands/command-navigation-actions.js +40 -6
- package/dist/app/commands/command-session-actions.d.ts +1 -1
- package/dist/app/commands/command-session-actions.js +56 -0
- package/dist/app/commands/shell-controller.d.ts +9 -2
- package/dist/app/commands/shell-controller.js +32 -21
- package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
- package/dist/app/extensions/extension-actions-controller.js +25 -7
- package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
- package/dist/app/extensions/extension-ui-controller.js +104 -64
- package/dist/app/input/input-action-controller.d.ts +4 -1
- package/dist/app/input/input-action-controller.js +68 -27
- package/dist/app/input/input-controller.d.ts +3 -0
- package/dist/app/input/input-controller.js +45 -1
- package/dist/app/input/input-paste-handler.d.ts +3 -0
- package/dist/app/input/input-paste-handler.js +21 -12
- package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
- package/dist/app/input/prompt-enhancer-controller.js +11 -5
- package/dist/app/input/voice-controller.d.ts +4 -0
- package/dist/app/input/voice-controller.js +76 -35
- package/dist/app/popup/popup-action-controller.d.ts +3 -0
- package/dist/app/popup/popup-action-controller.js +43 -8
- package/dist/app/session/queued-message-controller.d.ts +5 -4
- package/dist/app/session/queued-message-controller.js +32 -27
- package/dist/app/session/request-history.d.ts +2 -0
- package/dist/app/session/request-history.js +22 -15
- package/dist/app/session/session-event-controller.js +28 -8
- package/dist/app/session/session-history.js +8 -2
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
- package/dist/app/session/session-lifecycle-controller.js +91 -19
- package/dist/app/session/tabs-controller.d.ts +26 -0
- package/dist/app/session/tabs-controller.js +469 -96
- package/dist/app/terminal/terminal-controller.d.ts +4 -0
- package/dist/app/terminal/terminal-controller.js +38 -11
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +37 -9
- package/dist/input-editor.d.ts +2 -0
- package/dist/input-editor.js +17 -0
- package/docs/concurrency.md +76 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
- package/external/pi-tools-suite/src/todo/index.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +4 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1
2
|
import { InputPasteHandler } from "./input-paste-handler.js";
|
|
2
3
|
import { hasTerminalCommandModifier, isNativeCommandPressed, isNativeShiftPressed } from "./native-modifiers.js";
|
|
3
4
|
import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalKeyArrowDirection, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsEscape, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
|
|
@@ -5,13 +6,16 @@ const SHIFT_ENTER_ESCAPE_SEQUENCES = ["\x1b\r", "\x1b\n"];
|
|
|
5
6
|
export class AppInputController {
|
|
6
7
|
host;
|
|
7
8
|
inputBuffer = "";
|
|
9
|
+
inputDecoder = new StringDecoder("utf8");
|
|
8
10
|
pasteHandler;
|
|
9
11
|
constructor(host) {
|
|
10
12
|
this.host = host;
|
|
11
13
|
this.pasteHandler = new InputPasteHandler(host);
|
|
12
14
|
}
|
|
13
15
|
handleChunk(chunk) {
|
|
14
|
-
let data =
|
|
16
|
+
let data = this.inputDecoder.write(chunk);
|
|
17
|
+
if (!data)
|
|
18
|
+
return;
|
|
15
19
|
const bufferedSharedEditorInput = this.consumeBufferedSharedEditorInput(data);
|
|
16
20
|
if (bufferedSharedEditorInput.kind === "consumed" || bufferedSharedEditorInput.kind === "pending")
|
|
17
21
|
return;
|
|
@@ -159,12 +163,52 @@ export class AppInputController {
|
|
|
159
163
|
continue;
|
|
160
164
|
if (this.isPendingEscapeSequence())
|
|
161
165
|
return;
|
|
166
|
+
if (this.consumePrintableRun())
|
|
167
|
+
continue;
|
|
162
168
|
const char = this.inputBuffer[0];
|
|
163
169
|
this.inputBuffer = this.inputBuffer.slice(1);
|
|
164
170
|
if (char)
|
|
165
171
|
this.handleChar(char);
|
|
166
172
|
}
|
|
167
173
|
}
|
|
174
|
+
consumePrintableRun() {
|
|
175
|
+
let length = 0;
|
|
176
|
+
while (length < this.inputBuffer.length) {
|
|
177
|
+
const char = this.inputBuffer[length];
|
|
178
|
+
if (!char || char < " " || char === "\u007f")
|
|
179
|
+
break;
|
|
180
|
+
// Native undo/redo shortcuts must retain their individual key behavior.
|
|
181
|
+
const lower = char.toLowerCase();
|
|
182
|
+
if ((lower === "z" || lower === "y") && isNativeCommandPressed())
|
|
183
|
+
break;
|
|
184
|
+
length += 1;
|
|
185
|
+
}
|
|
186
|
+
if (length === 0)
|
|
187
|
+
return false;
|
|
188
|
+
const text = this.inputBuffer.slice(0, length);
|
|
189
|
+
this.inputBuffer = this.inputBuffer.slice(length);
|
|
190
|
+
let editorChanged = false;
|
|
191
|
+
let pendingText = "";
|
|
192
|
+
const flushPendingText = () => {
|
|
193
|
+
if (!pendingText)
|
|
194
|
+
return;
|
|
195
|
+
this.host.resetRequestHistoryNavigation();
|
|
196
|
+
this.host.inputEditor.insert(pendingText);
|
|
197
|
+
pendingText = "";
|
|
198
|
+
editorChanged = true;
|
|
199
|
+
};
|
|
200
|
+
for (const char of text) {
|
|
201
|
+
if (this.host.handleDirectPopupInput(char)) {
|
|
202
|
+
flushPendingText();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
pendingText += char;
|
|
206
|
+
}
|
|
207
|
+
flushPendingText();
|
|
208
|
+
if (editorChanged)
|
|
209
|
+
this.host.render();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
168
212
|
consumeBracketedPastePayload() {
|
|
169
213
|
if (!this.host.inputEditor.isInBracketedPaste)
|
|
170
214
|
return false;
|
|
@@ -2,6 +2,7 @@ import { InputEditor } from "../../input-editor.js";
|
|
|
2
2
|
export type InputPasteHost = {
|
|
3
3
|
readonly inputEditor: InputEditor;
|
|
4
4
|
readonly cwd: string;
|
|
5
|
+
inputScopeKey?(): string | undefined;
|
|
5
6
|
resetRequestHistoryNavigation(): void;
|
|
6
7
|
render(): void;
|
|
7
8
|
};
|
|
@@ -10,6 +11,7 @@ export declare class InputPasteHandler {
|
|
|
10
11
|
private pasteBufferParts;
|
|
11
12
|
private readonly recentPasteFingerprints;
|
|
12
13
|
private suppressImagePathPasteUntil;
|
|
14
|
+
private bracketedPasteScopeKey;
|
|
13
15
|
constructor(host: InputPasteHost);
|
|
14
16
|
handlePlainData(data: string): boolean;
|
|
15
17
|
beginBracketedPaste(): void;
|
|
@@ -23,6 +25,7 @@ export declare class InputPasteHandler {
|
|
|
23
25
|
private handlePastedFilePath;
|
|
24
26
|
private schedulePastedText;
|
|
25
27
|
private handleFilePaste;
|
|
28
|
+
private scopeIsActive;
|
|
26
29
|
private filePathForInput;
|
|
27
30
|
private displayPathForInput;
|
|
28
31
|
private insertPastedPathText;
|
|
@@ -10,6 +10,7 @@ export class InputPasteHandler {
|
|
|
10
10
|
pasteBufferParts = [];
|
|
11
11
|
recentPasteFingerprints = new Map();
|
|
12
12
|
suppressImagePathPasteUntil = 0;
|
|
13
|
+
bracketedPasteScopeKey;
|
|
13
14
|
constructor(host) {
|
|
14
15
|
this.host = host;
|
|
15
16
|
}
|
|
@@ -25,6 +26,7 @@ export class InputPasteHandler {
|
|
|
25
26
|
beginBracketedPaste() {
|
|
26
27
|
this.host.inputEditor.beginBracketedPaste();
|
|
27
28
|
this.pasteBufferParts = [];
|
|
29
|
+
this.bracketedPasteScopeKey = this.host.inputScopeKey?.();
|
|
28
30
|
}
|
|
29
31
|
appendBracketedPasteText(text) {
|
|
30
32
|
if (text)
|
|
@@ -34,11 +36,16 @@ export class InputPasteHandler {
|
|
|
34
36
|
this.host.inputEditor.endBracketedPaste();
|
|
35
37
|
const text = this.pasteBufferParts.join("");
|
|
36
38
|
this.pasteBufferParts = [];
|
|
39
|
+
const scopeKey = this.bracketedPasteScopeKey;
|
|
40
|
+
this.bracketedPasteScopeKey = undefined;
|
|
41
|
+
if (!this.scopeIsActive(scopeKey))
|
|
42
|
+
return;
|
|
37
43
|
this.handlePasteEnd(text);
|
|
38
44
|
}
|
|
39
45
|
async handleClipboardImagePaste() {
|
|
46
|
+
const scopeKey = this.host.inputScopeKey?.();
|
|
40
47
|
const image = await readClipboardImage();
|
|
41
|
-
if (!image)
|
|
48
|
+
if (!image || !this.scopeIsActive(scopeKey))
|
|
42
49
|
return;
|
|
43
50
|
if (this.isDuplicatePaste(`image:${image.mimeType}`, image.data)) {
|
|
44
51
|
this.host.render();
|
|
@@ -101,26 +108,28 @@ export class InputPasteHandler {
|
|
|
101
108
|
this.host.render();
|
|
102
109
|
return true;
|
|
103
110
|
}
|
|
104
|
-
void this.handleFilePaste(filePath);
|
|
111
|
+
void this.handleFilePaste(filePath, this.host.inputScopeKey?.());
|
|
105
112
|
return true;
|
|
106
113
|
}
|
|
107
114
|
schedulePastedText(text) {
|
|
108
|
-
|
|
109
|
-
if (this.isDuplicatePaste("text", text)) {
|
|
110
|
-
this.host.render();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
this.host.resetRequestHistoryNavigation();
|
|
114
|
-
this.host.inputEditor.attachPastedText(text);
|
|
115
|
+
if (this.isDuplicatePaste("text", text)) {
|
|
115
116
|
this.host.render();
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.host.resetRequestHistoryNavigation();
|
|
120
|
+
this.host.inputEditor.attachPastedText(text);
|
|
121
|
+
this.host.render();
|
|
118
122
|
}
|
|
119
|
-
async handleFilePaste(filePath) {
|
|
123
|
+
async handleFilePaste(filePath, scopeKey = this.host.inputScopeKey?.()) {
|
|
120
124
|
const inputPath = await this.filePathForInput(filePath);
|
|
125
|
+
if (!this.scopeIsActive(scopeKey))
|
|
126
|
+
return;
|
|
121
127
|
this.insertPastedPathText(inputPath);
|
|
122
128
|
this.host.render();
|
|
123
129
|
}
|
|
130
|
+
scopeIsActive(scopeKey) {
|
|
131
|
+
return !this.host.inputScopeKey || this.host.inputScopeKey() === scopeKey;
|
|
132
|
+
}
|
|
124
133
|
async filePathForInput(filePath) {
|
|
125
134
|
const resolved = resolve(this.host.cwd, filePath);
|
|
126
135
|
try {
|
|
@@ -8,6 +8,7 @@ export type AppPromptEnhancerControllerHost = {
|
|
|
8
8
|
runtime(): AgentSessionRuntime | undefined;
|
|
9
9
|
inputEditor(): InputEditor;
|
|
10
10
|
activeInputTabId(): string | undefined;
|
|
11
|
+
isInputTabOwnedByRuntime(tabId: string | undefined, runtime: AgentSessionRuntime, session: AgentSessionRuntime["session"]): boolean;
|
|
11
12
|
inputStateForTab(tabId: string | undefined): TabInputState | undefined;
|
|
12
13
|
setInputStateForTab(tabId: string | undefined, state: TabInputState): void;
|
|
13
14
|
promptEnhancerConfig(): PromptEnhancerConfig;
|
|
@@ -47,6 +47,7 @@ export class AppPromptEnhancerController {
|
|
|
47
47
|
this.host.toast.error("Prompt enhancer unavailable: runtime is not initialized");
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
+
const session = runtime.session;
|
|
50
51
|
const target = this.currentTarget();
|
|
51
52
|
if (!target) {
|
|
52
53
|
this.host.toast.warning(`Type at least ${PROMPT_ENHANCER_MIN_TEXT_LENGTH} characters to enhance`);
|
|
@@ -58,21 +59,26 @@ export class AppPromptEnhancerController {
|
|
|
58
59
|
this.host.render();
|
|
59
60
|
try {
|
|
60
61
|
const enhanced = await this.enhancePromptWithPi(runtime, target.text, this.host.promptEnhancerConfig());
|
|
62
|
+
if (!this.host.isInputTabOwnedByRuntime(target.tabId, runtime, session))
|
|
63
|
+
return;
|
|
61
64
|
const currentInputState = this.host.inputStateForTab(target.tabId);
|
|
62
65
|
if (!currentInputState || currentInputState.text !== target.originalEditorText) {
|
|
63
66
|
this.host.toast.warning("Prompt was changed before enhancement completed; result was not applied");
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
|
-
this.applyEnhancedPrompt(target, enhanced);
|
|
69
|
+
this.applyEnhancedPrompt(target, enhanced, currentInputState);
|
|
67
70
|
this.host.resetInputAfterProgrammaticEdit();
|
|
68
71
|
this.host.toast.success(target.kind === "selection" ? "Selection enhanced" : "Prompt enhanced");
|
|
69
72
|
}
|
|
70
73
|
catch (error) {
|
|
74
|
+
if (!this.host.isInputTabOwnedByRuntime(target.tabId, runtime, session))
|
|
75
|
+
return;
|
|
71
76
|
this.host.toast.error(`Prompt enhance failed: ${stringifyUnknown(error)}`);
|
|
72
77
|
}
|
|
73
78
|
finally {
|
|
74
79
|
this.enhancing = false;
|
|
75
|
-
this.
|
|
80
|
+
if (this.host.runtime() === runtime && runtime.session === session)
|
|
81
|
+
this.restoreSessionState(session);
|
|
76
82
|
this.host.render();
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -106,15 +112,15 @@ export class AppPromptEnhancerController {
|
|
|
106
112
|
end: originalEditorText.length,
|
|
107
113
|
};
|
|
108
114
|
}
|
|
109
|
-
applyEnhancedPrompt(target, enhanced) {
|
|
115
|
+
applyEnhancedPrompt(target, enhanced, currentState) {
|
|
110
116
|
const nextText = `${target.originalEditorText.slice(0, target.start)}${enhanced}${target.originalEditorText.slice(target.end)}`;
|
|
111
117
|
this.host.setInputStateForTab(target.tabId, {
|
|
112
118
|
text: nextText,
|
|
113
119
|
cursor: target.start + enhanced.length,
|
|
120
|
+
...(currentState.attachments ? { attachments: currentState.attachments } : {}),
|
|
114
121
|
});
|
|
115
122
|
}
|
|
116
|
-
restoreSessionState() {
|
|
117
|
-
const session = this.host.runtime()?.session;
|
|
123
|
+
restoreSessionState(session) {
|
|
118
124
|
this.host.setSessionStatus(session);
|
|
119
125
|
this.host.setSessionActivity(session?.isStreaming || session?.isCompacting ? "running" : "idle");
|
|
120
126
|
}
|
|
@@ -3,6 +3,7 @@ import { savePixDictationLanguage, type DictationConfig, type DictationLanguageM
|
|
|
3
3
|
export type VoiceLanguage = string;
|
|
4
4
|
export type VoiceInputState = "idle" | "installing" | "downloading" | "loading" | "listening";
|
|
5
5
|
export type AppVoiceControllerHost = {
|
|
6
|
+
activeInputScope(): string | undefined;
|
|
6
7
|
insertTranscript(text: string): void;
|
|
7
8
|
setPartialTranscript(text: string | undefined): void;
|
|
8
9
|
addSystemMessage(message: string): void;
|
|
@@ -68,6 +69,7 @@ export declare class AppVoiceController {
|
|
|
68
69
|
private partialTranscript;
|
|
69
70
|
private partialTranscriptTimer;
|
|
70
71
|
private startGeneration;
|
|
72
|
+
private recordingScope;
|
|
71
73
|
constructor(host: AppVoiceControllerHost, dictationConfig: DictationConfig);
|
|
72
74
|
updateDictationConfig(dictationConfig: DictationConfig): void;
|
|
73
75
|
statusWidgetText(): string;
|
|
@@ -95,7 +97,9 @@ export declare class AppVoiceController {
|
|
|
95
97
|
private clearPartialTranscript;
|
|
96
98
|
private schedulePartialTranscriptEmit;
|
|
97
99
|
private isCurrentStart;
|
|
100
|
+
private continueStart;
|
|
98
101
|
private isCurrentAudioProcess;
|
|
102
|
+
private isScopeActive;
|
|
99
103
|
}
|
|
100
104
|
declare function ensureModel(language: VoiceLanguage, definition: VoiceModelDefinition): Promise<string>;
|
|
101
105
|
declare function tryLoadVosk(): VoskLoadAttempt;
|
|
@@ -45,6 +45,7 @@ export class AppVoiceController {
|
|
|
45
45
|
partialTranscript;
|
|
46
46
|
partialTranscriptTimer;
|
|
47
47
|
startGeneration = 0;
|
|
48
|
+
recordingScope;
|
|
48
49
|
constructor(host, dictationConfig) {
|
|
49
50
|
this.host = host;
|
|
50
51
|
this.modelDefinitions = dictationConfig.languages;
|
|
@@ -97,9 +98,12 @@ export class AppVoiceController {
|
|
|
97
98
|
async toggleLanguage() {
|
|
98
99
|
if (!this.showLanguageSwitcher())
|
|
99
100
|
return;
|
|
101
|
+
const scope = this.host.activeInputScope();
|
|
100
102
|
const wasActive = this.state !== "idle";
|
|
101
103
|
if (wasActive)
|
|
102
104
|
await this.stopRecording();
|
|
105
|
+
if (!this.isScopeActive(scope))
|
|
106
|
+
return;
|
|
103
107
|
this.language = this.nextLanguage();
|
|
104
108
|
this.saveLanguageSelection(this.language);
|
|
105
109
|
this.host.showToast(`Voice language: ${this.modelDefinition(this.language).label}`, "info");
|
|
@@ -109,21 +113,24 @@ export class AppVoiceController {
|
|
|
109
113
|
}
|
|
110
114
|
async stopRecording() {
|
|
111
115
|
this.startGeneration += 1;
|
|
116
|
+
const scope = this.recordingScope;
|
|
112
117
|
const audioProcess = this.audioProcess;
|
|
113
118
|
const recognizer = this.recognizer;
|
|
114
119
|
this.audioProcess = undefined;
|
|
115
120
|
this.recognizer = undefined;
|
|
121
|
+
this.recordingScope = undefined;
|
|
116
122
|
if (audioProcess && !audioProcess.killed)
|
|
117
123
|
audioProcess.kill("SIGTERM");
|
|
118
124
|
if (recognizer) {
|
|
119
|
-
this.clearPartialTranscript();
|
|
120
|
-
this.emitTranscript(recognizer.finalResult());
|
|
125
|
+
this.clearPartialTranscript(scope);
|
|
126
|
+
this.emitTranscript(recognizer.finalResult(), scope);
|
|
121
127
|
recognizer.free?.();
|
|
122
128
|
}
|
|
123
129
|
if (this.state !== "idle") {
|
|
124
130
|
this.clearProgressMessage();
|
|
125
131
|
this.state = "idle";
|
|
126
|
-
this.
|
|
132
|
+
if (this.isScopeActive(scope))
|
|
133
|
+
this.host.render();
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
async dispose() {
|
|
@@ -134,25 +141,29 @@ export class AppVoiceController {
|
|
|
134
141
|
}
|
|
135
142
|
async startRecording() {
|
|
136
143
|
const language = this.language;
|
|
144
|
+
const scope = this.host.activeInputScope();
|
|
137
145
|
const generation = this.startGeneration + 1;
|
|
138
146
|
this.startGeneration = generation;
|
|
147
|
+
this.recordingScope = scope;
|
|
139
148
|
try {
|
|
140
149
|
const initialVosk = voiceControllerDeps.tryLoadVosk();
|
|
141
150
|
const vosk = initialVosk.ok
|
|
142
151
|
? initialVosk.module
|
|
143
|
-
: await this.installAndLoadVosk(initialVosk.error, generation);
|
|
144
|
-
if (!this.
|
|
152
|
+
: await this.installAndLoadVosk(initialVosk.error, generation, scope);
|
|
153
|
+
if (!this.continueStart(generation, scope))
|
|
145
154
|
return;
|
|
146
155
|
vosk.setLogLevel?.(-1);
|
|
147
156
|
this.state = "downloading";
|
|
148
157
|
this.host.render();
|
|
149
158
|
const modelPath = await voiceControllerDeps.ensureModel(language, this.modelDefinition(language));
|
|
150
|
-
if (!this.
|
|
159
|
+
if (!this.continueStart(generation, scope))
|
|
151
160
|
return;
|
|
152
161
|
this.state = "loading";
|
|
153
162
|
this.host.render();
|
|
154
163
|
const model = this.cachedModel(language, modelPath, vosk);
|
|
155
164
|
const recorder = await voiceControllerDeps.selectRecorderCommand();
|
|
165
|
+
if (!this.continueStart(generation, scope))
|
|
166
|
+
return;
|
|
156
167
|
const recognizer = new vosk.Recognizer({ model, sampleRate: SAMPLE_RATE });
|
|
157
168
|
const audioProcess = voiceControllerDeps.spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
158
169
|
this.recognizer = recognizer;
|
|
@@ -160,10 +171,10 @@ export class AppVoiceController {
|
|
|
160
171
|
this.state = "listening";
|
|
161
172
|
this.host.render();
|
|
162
173
|
this.host.showToast(`Voice input on (${this.modelDefinition(language).label}, ${recorder.description})`, "info");
|
|
163
|
-
this.bindAudioProcess(audioProcess, recognizer, generation);
|
|
174
|
+
this.bindAudioProcess(audioProcess, recognizer, generation, scope);
|
|
164
175
|
}
|
|
165
176
|
catch (error) {
|
|
166
|
-
if (!this.
|
|
177
|
+
if (!this.continueStart(generation, scope))
|
|
167
178
|
return;
|
|
168
179
|
this.clearProgressMessage();
|
|
169
180
|
this.cleanupRecognizer();
|
|
@@ -205,31 +216,31 @@ export class AppVoiceController {
|
|
|
205
216
|
throw new Error(`dictation language is not configured: ${language}`);
|
|
206
217
|
return definition;
|
|
207
218
|
}
|
|
208
|
-
async installAndLoadVosk(initialError, generation) {
|
|
219
|
+
async installAndLoadVosk(initialError, generation, scope) {
|
|
209
220
|
this.state = "installing";
|
|
210
|
-
this.setProgressMessage("Installing Vosk voice bindings...");
|
|
221
|
+
this.setProgressMessage("Installing Vosk voice bindings...", generation, scope);
|
|
211
222
|
const vosk = await loadVoskWithAutoInstall(initialError, (message) => {
|
|
212
|
-
if (this.isCurrentStart(generation))
|
|
213
|
-
this.setProgressMessage(message);
|
|
223
|
+
if (this.isCurrentStart(generation, scope))
|
|
224
|
+
this.setProgressMessage(message, generation, scope);
|
|
214
225
|
});
|
|
215
|
-
if (this.isCurrentStart(generation))
|
|
226
|
+
if (this.isCurrentStart(generation, scope))
|
|
216
227
|
this.addProgressSystemMessage("Vosk voice bindings are ready.");
|
|
217
|
-
if (this.isCurrentStart(generation))
|
|
228
|
+
if (this.isCurrentStart(generation, scope))
|
|
218
229
|
this.clearProgressMessage();
|
|
219
230
|
return vosk;
|
|
220
231
|
}
|
|
221
|
-
bindAudioProcess(audioProcess, recognizer, generation) {
|
|
232
|
+
bindAudioProcess(audioProcess, recognizer, generation, scope = this.recordingScope) {
|
|
222
233
|
let stderr = "";
|
|
223
234
|
audioProcess.stdout.on("data", (chunk) => {
|
|
224
|
-
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation))
|
|
235
|
+
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation, scope))
|
|
225
236
|
return;
|
|
226
237
|
try {
|
|
227
238
|
if (recognizer.acceptWaveform(chunk)) {
|
|
228
|
-
this.clearPartialTranscript();
|
|
229
|
-
this.emitTranscript(recognizer.result());
|
|
239
|
+
this.clearPartialTranscript(scope);
|
|
240
|
+
this.emitTranscript(recognizer.result(), scope);
|
|
230
241
|
}
|
|
231
242
|
else {
|
|
232
|
-
this.emitPartialTranscript(recognizer.partialResult?.());
|
|
243
|
+
this.emitPartialTranscript(recognizer.partialResult?.(), audioProcess, recognizer, generation, scope);
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
catch (error) {
|
|
@@ -241,9 +252,9 @@ export class AppVoiceController {
|
|
|
241
252
|
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-600);
|
|
242
253
|
});
|
|
243
254
|
audioProcess.once("error", (error) => {
|
|
244
|
-
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation))
|
|
255
|
+
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation, scope))
|
|
245
256
|
return;
|
|
246
|
-
this.clearPartialTranscript();
|
|
257
|
+
this.clearPartialTranscript(scope);
|
|
247
258
|
this.cleanupRecognizer();
|
|
248
259
|
this.audioProcess = undefined;
|
|
249
260
|
this.state = "idle";
|
|
@@ -251,10 +262,10 @@ export class AppVoiceController {
|
|
|
251
262
|
this.host.render();
|
|
252
263
|
});
|
|
253
264
|
audioProcess.once("close", (code, signal) => {
|
|
254
|
-
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation))
|
|
265
|
+
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation, scope))
|
|
255
266
|
return;
|
|
256
|
-
this.clearPartialTranscript();
|
|
257
|
-
this.emitTranscript(recognizer.finalResult());
|
|
267
|
+
this.clearPartialTranscript(scope);
|
|
268
|
+
this.emitTranscript(recognizer.finalResult(), scope);
|
|
258
269
|
this.cleanupRecognizer();
|
|
259
270
|
this.audioProcess = undefined;
|
|
260
271
|
this.state = "idle";
|
|
@@ -269,11 +280,13 @@ export class AppVoiceController {
|
|
|
269
280
|
this.recognizer?.free?.();
|
|
270
281
|
this.recognizer = undefined;
|
|
271
282
|
}
|
|
272
|
-
setProgressMessage(message) {
|
|
283
|
+
setProgressMessage(message, generation = this.startGeneration, scope = this.recordingScope) {
|
|
273
284
|
this.progressMessage = message;
|
|
274
285
|
this.addProgressSystemMessage(message);
|
|
275
286
|
if (!this.progressTimer) {
|
|
276
287
|
this.progressTimer = setInterval(() => {
|
|
288
|
+
if (!this.isCurrentStart(generation, scope))
|
|
289
|
+
return;
|
|
277
290
|
this.progressFrame += 1;
|
|
278
291
|
this.host.render();
|
|
279
292
|
}, 120);
|
|
@@ -296,20 +309,26 @@ export class AppVoiceController {
|
|
|
296
309
|
this.lastSystemProgressMessage = text;
|
|
297
310
|
this.host.addSystemMessage(text);
|
|
298
311
|
}
|
|
299
|
-
emitTranscript(result) {
|
|
312
|
+
emitTranscript(result, scope = this.recordingScope) {
|
|
313
|
+
if (!this.isScopeActive(scope))
|
|
314
|
+
return;
|
|
300
315
|
const text = transcriptText(result);
|
|
301
316
|
if (!text)
|
|
302
317
|
return;
|
|
303
318
|
this.host.insertTranscript(text);
|
|
304
319
|
}
|
|
305
|
-
emitPartialTranscript(result) {
|
|
320
|
+
emitPartialTranscript(result, audioProcess = this.audioProcess, recognizer = this.recognizer, generation = this.startGeneration, scope = this.recordingScope) {
|
|
321
|
+
if (audioProcess || recognizer) {
|
|
322
|
+
if (!audioProcess || !recognizer || !this.isCurrentAudioProcess(audioProcess, recognizer, generation, scope))
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
306
325
|
const text = partialTranscriptText(result);
|
|
307
326
|
if (text === this.partialTranscript)
|
|
308
327
|
return;
|
|
309
328
|
this.partialTranscript = text;
|
|
310
|
-
this.schedulePartialTranscriptEmit();
|
|
329
|
+
this.schedulePartialTranscriptEmit(audioProcess, recognizer, generation, scope);
|
|
311
330
|
}
|
|
312
|
-
clearPartialTranscript() {
|
|
331
|
+
clearPartialTranscript(scope = this.recordingScope) {
|
|
313
332
|
if (!this.partialTranscript)
|
|
314
333
|
return;
|
|
315
334
|
this.partialTranscript = undefined;
|
|
@@ -317,22 +336,44 @@ export class AppVoiceController {
|
|
|
317
336
|
clearTimeout(this.partialTranscriptTimer);
|
|
318
337
|
this.partialTranscriptTimer = undefined;
|
|
319
338
|
}
|
|
320
|
-
this.
|
|
339
|
+
if (this.isScopeActive(scope))
|
|
340
|
+
this.host.setPartialTranscript(undefined);
|
|
321
341
|
}
|
|
322
|
-
schedulePartialTranscriptEmit() {
|
|
342
|
+
schedulePartialTranscriptEmit(audioProcess, recognizer, generation, scope) {
|
|
323
343
|
if (this.partialTranscriptTimer)
|
|
324
344
|
return;
|
|
325
345
|
this.partialTranscriptTimer = setTimeout(() => {
|
|
326
346
|
this.partialTranscriptTimer = undefined;
|
|
347
|
+
if (audioProcess && recognizer) {
|
|
348
|
+
if (!this.isCurrentAudioProcess(audioProcess, recognizer, generation, scope))
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
else if (!this.isScopeActive(scope)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
327
354
|
this.host.setPartialTranscript(this.partialTranscript);
|
|
328
355
|
}, VOICE_PARTIAL_TRANSCRIPT_THROTTLE_MS);
|
|
329
356
|
this.partialTranscriptTimer.unref?.();
|
|
330
357
|
}
|
|
331
|
-
isCurrentStart(generation) {
|
|
332
|
-
return this.startGeneration === generation;
|
|
358
|
+
isCurrentStart(generation, scope = this.recordingScope) {
|
|
359
|
+
return this.startGeneration === generation && this.recordingScope === scope && this.isScopeActive(scope);
|
|
360
|
+
}
|
|
361
|
+
continueStart(generation, scope) {
|
|
362
|
+
if (this.startGeneration !== generation || this.recordingScope !== scope)
|
|
363
|
+
return false;
|
|
364
|
+
if (this.isScopeActive(scope))
|
|
365
|
+
return true;
|
|
366
|
+
this.startGeneration += 1;
|
|
367
|
+
this.recordingScope = undefined;
|
|
368
|
+
this.clearProgressMessage();
|
|
369
|
+
this.state = "idle";
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
isCurrentAudioProcess(audioProcess, recognizer, generation, scope = this.recordingScope) {
|
|
373
|
+
return this.isCurrentStart(generation, scope) && this.audioProcess === audioProcess && this.recognizer === recognizer;
|
|
333
374
|
}
|
|
334
|
-
|
|
335
|
-
return this.
|
|
375
|
+
isScopeActive(scope) {
|
|
376
|
+
return this.host.activeInputScope() === scope;
|
|
336
377
|
}
|
|
337
378
|
}
|
|
338
379
|
async function ensureModel(language, definition) {
|
|
@@ -39,6 +39,9 @@ export declare class AppPopupActionController {
|
|
|
39
39
|
private submitSelectedResume;
|
|
40
40
|
private formatSlashCommandLine;
|
|
41
41
|
private executeResourceSlashCommand;
|
|
42
|
+
private captureScope;
|
|
43
|
+
private isScopeActive;
|
|
44
|
+
private isRuntimeActive;
|
|
42
45
|
private getRuntime;
|
|
43
46
|
private getIdleRuntime;
|
|
44
47
|
}
|