@danypops/papyrus 0.29.2 → 0.29.3
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.
|
@@ -1126,14 +1126,26 @@ export function isLiveAskPending(): boolean {
|
|
|
1126
1126
|
return livePendingCount > 0;
|
|
1127
1127
|
}
|
|
1128
1128
|
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1129
|
+
const DISCUSS_TYPING_COURTESY_DEFAULT_POLL_MS = 100;
|
|
1130
|
+
const DISCUSS_TYPING_COURTESY_DEFAULT_INITIAL_QUIET_MS = 1_500;
|
|
1131
|
+
const DISCUSS_TYPING_COURTESY_DEFAULT_QUIET_FLOOR_MS = 300;
|
|
1132
|
+
const DISCUSS_TYPING_COURTESY_DEFAULT_DECAY_HORIZON_MS = 10_000;
|
|
1133
|
+
|
|
1134
|
+
let typingCourtesyPollMs = DISCUSS_TYPING_COURTESY_DEFAULT_POLL_MS;
|
|
1135
|
+
let typingCourtesyInitialQuietMs = DISCUSS_TYPING_COURTESY_DEFAULT_INITIAL_QUIET_MS;
|
|
1136
|
+
let typingCourtesyQuietFloorMs = DISCUSS_TYPING_COURTESY_DEFAULT_QUIET_FLOOR_MS;
|
|
1137
|
+
let typingCourtesyDecayHorizonMs = DISCUSS_TYPING_COURTESY_DEFAULT_DECAY_HORIZON_MS;
|
|
1138
|
+
|
|
1139
|
+
/** Test-only: the real decay curve runs over seconds, too slow to exercise at its real scale in a unit test. */
|
|
1140
|
+
export function setTypingCourtesyTimingForTests(overrides?: { pollMs?: number; initialQuietMs?: number; floorMs?: number; decayHorizonMs?: number }): void {
|
|
1141
|
+
typingCourtesyPollMs = overrides?.pollMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_POLL_MS;
|
|
1142
|
+
typingCourtesyInitialQuietMs = overrides?.initialQuietMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_INITIAL_QUIET_MS;
|
|
1143
|
+
typingCourtesyQuietFloorMs = overrides?.floorMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_QUIET_FLOOR_MS;
|
|
1144
|
+
typingCourtesyDecayHorizonMs = overrides?.decayHorizonMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_DECAY_HORIZON_MS;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function isTypingCourtesyEnabled(): boolean {
|
|
1148
|
+
return parseBooleanPreference(process.env["PAPYRUS_DISCUSS_TYPING_COURTESY"]) ?? true;
|
|
1137
1149
|
}
|
|
1138
1150
|
|
|
1139
1151
|
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
@@ -1145,31 +1157,65 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1145
1157
|
}
|
|
1146
1158
|
|
|
1147
1159
|
/**
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
|
|
1160
|
+
* Required quiet gap (no keystroke) before a live ask may open, as a function of how long we've
|
|
1161
|
+
* already been waiting. Starts wide (a natural inter-word pause shouldn't count as "done typing")
|
|
1162
|
+
* and decays toward a floor -- someone typing continuously gets pickier treatment over time
|
|
1163
|
+
* rather than never being asked. No outer cap: someone typing with sub-floor gaps forever waits
|
|
1164
|
+
* forever, same as the picker itself already waits indefinitely for a real human answer once open.
|
|
1165
|
+
*/
|
|
1166
|
+
function requiredQuietMsAt(elapsedMs: number): number {
|
|
1167
|
+
const t = Math.min(1, Math.max(0, elapsedMs / typingCourtesyDecayHorizonMs));
|
|
1168
|
+
return typingCourtesyInitialQuietMs - t * (typingCourtesyInitialQuietMs - typingCourtesyQuietFloorMs);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Ambient, session-lifetime keystroke clock -- deliberately NOT scoped per-ask. A per-ask listener
|
|
1173
|
+
* would only see keystrokes from the moment the tool call happens to start, missing typing already
|
|
1174
|
+
* in progress when it began (the exact case this feature exists to protect). Attached once per
|
|
1175
|
+
* distinct ui instance (reference equality; a session's real ui object is stable for its lifetime)
|
|
1176
|
+
* and left attached -- there is no unregister, matching onTerminalInput's own listener-return-value
|
|
1177
|
+
* contract elsewhere in this file.
|
|
1178
|
+
*/
|
|
1179
|
+
let lastKeystrokeAt = 0;
|
|
1180
|
+
let trackedUi: ExtensionContext["ui"] | undefined;
|
|
1181
|
+
|
|
1182
|
+
export function ensureTypingCourtesyTracking(ui: ExtensionContext["ui"]): void {
|
|
1183
|
+
if (typeof ui.onTerminalInput !== "function" || trackedUi === ui) return;
|
|
1184
|
+
trackedUi = ui;
|
|
1185
|
+
ui.onTerminalInput(() => { lastKeystrokeAt = Date.now(); return undefined; });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Test-only: clears the ambient keystroke clock so one test's simulated typing can't bleed into another's. */
|
|
1189
|
+
export function resetTypingCourtesyTrackingForTests(): void {
|
|
1190
|
+
lastKeystrokeAt = 0;
|
|
1191
|
+
trackedUi = undefined;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Whether there is real, recent typing activity to wait out right now -- a plain synchronous read
|
|
1196
|
+
* of the ambient keystroke clock so the common case (nobody typing) never forces the caller
|
|
1197
|
+
* through an extra microtask. Deliberately not folded into waitForTypingCourtesy itself: an
|
|
1198
|
+
* unconditional `await` there -- even one that resolves immediately -- still yields once, which is
|
|
1199
|
+
* enough to let a signal aborted synchronously right after invoking askQuestion race past the
|
|
1200
|
+
* abort listener registered deeper in askQuestionBlocking and get missed entirely.
|
|
1154
1201
|
*/
|
|
1155
|
-
export function
|
|
1156
|
-
return
|
|
1202
|
+
export function isRecentlyTyping(): boolean {
|
|
1203
|
+
return lastKeystrokeAt > 0 && Date.now() - lastKeystrokeAt < typingCourtesyInitialQuietMs;
|
|
1157
1204
|
}
|
|
1158
1205
|
|
|
1159
1206
|
/**
|
|
1160
|
-
* Waits
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
* courtesy. Only call when hasEditorCourtesyDraft(ctx) is already true.
|
|
1207
|
+
* Waits out real keystroke activity (not editor text content -- that can't distinguish "actively
|
|
1208
|
+
* typing" from "a stale draft sitting there", and misses a mid-thought erase-and-resume) before
|
|
1209
|
+
* popping the live ask over it. Only call when isRecentlyTyping() is already true.
|
|
1164
1210
|
*/
|
|
1165
|
-
export async function
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1211
|
+
export async function waitForTypingCourtesy(params: Pick<AskQuestionParams, "onUpdate" | "signal">): Promise<void> {
|
|
1212
|
+
const startedAt = Date.now();
|
|
1213
|
+
let announced = false;
|
|
1214
|
+
while (lastKeystrokeAt > 0 && !params.signal?.aborted) {
|
|
1215
|
+
const elapsed = Date.now() - startedAt;
|
|
1216
|
+
if (Date.now() - lastKeystrokeAt >= requiredQuietMsAt(elapsed)) return;
|
|
1217
|
+
if (!announced) { announced = true; params.onUpdate?.({ content: [{ type: "text", text: "Waiting for you to finish typing before asking..." }], details: undefined }); }
|
|
1218
|
+
await sleep(typingCourtesyPollMs, params.signal);
|
|
1173
1219
|
}
|
|
1174
1220
|
}
|
|
1175
1221
|
|
|
@@ -1194,11 +1240,12 @@ async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionPa
|
|
|
1194
1240
|
const displayMode: AskDisplayMode = params.displayMode ?? envDisplayMode ?? "overlay";
|
|
1195
1241
|
const normalizedContext = params.context?.trim() || undefined;
|
|
1196
1242
|
|
|
1243
|
+
if (isTypingCourtesyEnabled()) ensureTypingCourtesyTracking(ctx.ui);
|
|
1197
1244
|
livePendingCount += 1;
|
|
1198
1245
|
try {
|
|
1199
|
-
// Only actually awaits (yielding a microtask) when there's
|
|
1200
|
-
// see
|
|
1201
|
-
if (
|
|
1246
|
+
// Only actually awaits (yielding a microtask) when there's real typing activity to wait out --
|
|
1247
|
+
// see isRecentlyTyping's own comment for why the common case must stay synchronous.
|
|
1248
|
+
if (isTypingCourtesyEnabled() && isRecentlyTyping()) await waitForTypingCourtesy(params);
|
|
1202
1249
|
params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
|
|
1203
1250
|
return await askQuestionBlocking(ctx, params, options, allowMultiple, allowFreeform, allowComment, displayMode, normalizedContext);
|
|
1204
1251
|
} finally {
|
package/extension/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { GateResult } from "../../src/domain/gate.ts";
|
|
|
22
22
|
import { formatMetadata } from "./artifact-format.ts";
|
|
23
23
|
import { callService } from "./service-client.ts";
|
|
24
24
|
import { registerDomainTools } from "./domain-tools.ts";
|
|
25
|
-
import { isLiveAskPending } from "./discuss-ask-view.ts";
|
|
25
|
+
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
|
|
26
26
|
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
|
|
27
27
|
import type { TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
28
28
|
import { ActiveTaskContinuation, automaticPauseReason, shouldResumeFocusOnHumanInput, type ActiveTaskMarker } from "./active-task-continuation.ts";
|
|
@@ -566,6 +566,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
566
566
|
// intentionally silent -- see comment above
|
|
567
567
|
}
|
|
568
568
|
if (!ctx.hasUI) return;
|
|
569
|
+
// Attached from session start, not lazily on first ask -- a per-ask listener would only see
|
|
570
|
+
// keystrokes from the moment that tool call happens to begin, missing typing already in
|
|
571
|
+
// progress when it started (the exact case Discuss's typing-courtesy wait protects against).
|
|
572
|
+
ensureTypingCourtesyTracking(ctx.ui);
|
|
569
573
|
overlay ??= new TaskOverlay();
|
|
570
574
|
overlay.setUI(ctx.ui);
|
|
571
575
|
overlay.setProjectRoot(ctx.cwd);
|
package/package.json
CHANGED