ubume 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/README.md +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ProviderId } from "../core/providerLauncher/types.js";
|
|
2
|
+
import type { ProviderBackendKind, ProviderRoute } from "../core/providerRuntime/types.js";
|
|
3
|
+
import type { ConversationMessage, ConversationMetadata } from "../core/workspace/conversationStore.js";
|
|
4
|
+
import type { AssistantEvent, TimelineEvent, UserPromptEvent } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Route restored by /resume. Local conversations must keep their saved backend:
|
|
8
|
+
* without it the route falls back to LM Studio's default endpoint, so an
|
|
9
|
+
* Unsloth-served model fails with a connection error.
|
|
10
|
+
*/
|
|
11
|
+
export function buildResumedProviderRoute(
|
|
12
|
+
metadata: Pick<ConversationMetadata, "modelId" | "backendKind" | "reasoning" | "localBackend">,
|
|
13
|
+
providerId: ProviderId,
|
|
14
|
+
fallbackBackendKind: ProviderBackendKind,
|
|
15
|
+
): ProviderRoute {
|
|
16
|
+
return {
|
|
17
|
+
providerId,
|
|
18
|
+
modelId: metadata.modelId,
|
|
19
|
+
backendKind: metadata.backendKind && metadata.backendKind !== "unavailable"
|
|
20
|
+
? metadata.backendKind as ProviderBackendKind
|
|
21
|
+
: fallbackBackendKind,
|
|
22
|
+
...(metadata.reasoning ? { reasoning: metadata.reasoning } : {}),
|
|
23
|
+
...(providerId === "local" && metadata.localBackend ? { localBackend: metadata.localBackend } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function conversationMessagesToTimeline(
|
|
28
|
+
messages: readonly ConversationMessage[],
|
|
29
|
+
createEventId: () => number,
|
|
30
|
+
createTurnId?: () => number,
|
|
31
|
+
): TimelineEvent[] {
|
|
32
|
+
const events: TimelineEvent[] = [];
|
|
33
|
+
// Restored turns must draw from the app's turn counter: the timeline groups
|
|
34
|
+
// events by turnId, so restored ids that collide with the next live turn
|
|
35
|
+
// would fold the new prompt and run into an old turn.
|
|
36
|
+
let localTurnId = 0;
|
|
37
|
+
const nextTurnId = createTurnId ?? (() => ++localTurnId);
|
|
38
|
+
let turnId = 0;
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
if (message.role === "user") {
|
|
41
|
+
turnId = nextTurnId();
|
|
42
|
+
const user: UserPromptEvent = {
|
|
43
|
+
id: createEventId(),
|
|
44
|
+
type: "user",
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
prompt: message.content,
|
|
47
|
+
turnId,
|
|
48
|
+
};
|
|
49
|
+
events.push(user);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const assistant: AssistantEvent = {
|
|
53
|
+
id: createEventId(),
|
|
54
|
+
type: "assistant",
|
|
55
|
+
createdAt: Date.now(),
|
|
56
|
+
content: message.activitySummary
|
|
57
|
+
? `${message.content}\n\n---\n${message.activitySummary}`
|
|
58
|
+
: message.content,
|
|
59
|
+
contentChunks: [],
|
|
60
|
+
turnId,
|
|
61
|
+
};
|
|
62
|
+
events.push(assistant);
|
|
63
|
+
}
|
|
64
|
+
return events;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Plain role/content history for a provider request. Local Harness session
|
|
69
|
+
* reuse hashes this array, so activity summaries are only folded into content
|
|
70
|
+
* when requested (providers that restart from visible history).
|
|
71
|
+
*/
|
|
72
|
+
export function toProviderConversationHistory(
|
|
73
|
+
messages: readonly ConversationMessage[],
|
|
74
|
+
options: { includeActivitySummaries: boolean },
|
|
75
|
+
): ConversationMessage[] {
|
|
76
|
+
return messages.map((message) => ({
|
|
77
|
+
role: message.role,
|
|
78
|
+
content: options.includeActivitySummaries && message.activitySummary
|
|
79
|
+
? `${message.content}\n\n${message.activitySummary}`
|
|
80
|
+
: message.content,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatConversationHistory(messages: readonly ConversationMessage[]): string {
|
|
85
|
+
return messages.map((message) => {
|
|
86
|
+
const label = message.role === "user" ? "User" : "Assistant";
|
|
87
|
+
return `${label}:\n${message.content}`;
|
|
88
|
+
}).join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function selectConversationContext(
|
|
92
|
+
messages: readonly ConversationMessage[],
|
|
93
|
+
maxCharacters?: number,
|
|
94
|
+
): ConversationMessage[] {
|
|
95
|
+
if (!maxCharacters || maxCharacters <= 0) return [...messages];
|
|
96
|
+
const selected: ConversationMessage[] = [];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
99
|
+
const message = messages[index];
|
|
100
|
+
if (!message) continue;
|
|
101
|
+
const cost = message.content.length;
|
|
102
|
+
if (selected.length > 0 && total + cost > maxCharacters) break;
|
|
103
|
+
selected.unshift(message);
|
|
104
|
+
total += cost;
|
|
105
|
+
}
|
|
106
|
+
return selected;
|
|
107
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { BackendProgressUpdate } from "../core/providers/types.js";
|
|
2
|
+
import type { RunFileActivity } from "../core/workspace/workspaceActivity.js";
|
|
3
|
+
import type { RunToolActivity } from "./types.js";
|
|
4
|
+
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
5
|
+
|
|
6
|
+
export type LiveRenderUpdate =
|
|
7
|
+
| { type: "assistant"; chunk: string }
|
|
8
|
+
| { type: "plan"; chunk: string }
|
|
9
|
+
| { type: "progress"; update: BackendProgressUpdate }
|
|
10
|
+
| { type: "activity"; activity: RunFileActivity[] }
|
|
11
|
+
| { type: "tool"; activity: RunToolActivity };
|
|
12
|
+
|
|
13
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
14
|
+
|
|
15
|
+
export interface LiveRenderSchedulerOptions {
|
|
16
|
+
flush: (updates: LiveRenderUpdate[]) => void;
|
|
17
|
+
assistantFlushMs: number;
|
|
18
|
+
progressOnlyFlushMs: number;
|
|
19
|
+
setTimer?: (callback: () => void, delayMs: number) => TimerHandle;
|
|
20
|
+
clearTimer?: (timer: TimerHandle) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LiveRenderScheduler {
|
|
24
|
+
enqueue: (update: LiveRenderUpdate) => void;
|
|
25
|
+
flushNow: () => boolean;
|
|
26
|
+
cancel: () => void;
|
|
27
|
+
hasPendingUpdates: () => boolean;
|
|
28
|
+
getStats: () => LiveRenderSchedulerStats;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LiveRenderSchedulerStats {
|
|
32
|
+
providerEvents: number;
|
|
33
|
+
flushes: number;
|
|
34
|
+
averageFlushIntervalMs: number;
|
|
35
|
+
maxFlushIntervalMs: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Update coalescing ───────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function findPendingUpdateIndex(
|
|
41
|
+
updates: LiveRenderUpdate[],
|
|
42
|
+
predicate: (update: LiveRenderUpdate) => boolean,
|
|
43
|
+
): number {
|
|
44
|
+
for (let index = updates.length - 1; index >= 0; index -= 1) {
|
|
45
|
+
if (predicate(updates[index]!)) return index;
|
|
46
|
+
}
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mergeLiveRenderUpdate(updates: LiveRenderUpdate[], update: LiveRenderUpdate): boolean {
|
|
51
|
+
const previous = updates[updates.length - 1];
|
|
52
|
+
|
|
53
|
+
if (update.type === "assistant" || update.type === "plan") {
|
|
54
|
+
if (previous?.type === update.type) {
|
|
55
|
+
previous.chunk += update.chunk;
|
|
56
|
+
} else {
|
|
57
|
+
updates.push({ ...update });
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (update.type === "progress") {
|
|
63
|
+
const index = findPendingUpdateIndex(
|
|
64
|
+
updates,
|
|
65
|
+
(pending) => pending.type === "progress" && pending.update.id === update.update.id,
|
|
66
|
+
);
|
|
67
|
+
if (index >= 0 && updates[index]?.type === "progress") {
|
|
68
|
+
updates[index] = update;
|
|
69
|
+
} else {
|
|
70
|
+
updates.push(update);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (update.type === "tool") {
|
|
76
|
+
const index = findPendingUpdateIndex(
|
|
77
|
+
updates,
|
|
78
|
+
(pending) => pending.type === "tool" && pending.activity.id === update.activity.id,
|
|
79
|
+
);
|
|
80
|
+
if (index >= 0 && updates[index]?.type === "tool") {
|
|
81
|
+
const previousTool = updates[index];
|
|
82
|
+
updates[index] = {
|
|
83
|
+
type: "tool",
|
|
84
|
+
activity: { ...previousTool.activity, ...update.activity },
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
updates.push(update);
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const activityIndex = findPendingUpdateIndex(updates, (pending) => pending.type === "activity");
|
|
93
|
+
if (activityIndex >= 0 && updates[activityIndex]?.type === "activity") {
|
|
94
|
+
updates[activityIndex].activity.push(...update.activity);
|
|
95
|
+
} else {
|
|
96
|
+
updates.push({ type: "activity", activity: [...update.activity] });
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Scheduler ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function createLiveRenderScheduler({
|
|
104
|
+
flush,
|
|
105
|
+
assistantFlushMs,
|
|
106
|
+
progressOnlyFlushMs,
|
|
107
|
+
setTimer = setTimeout,
|
|
108
|
+
clearTimer = clearTimeout,
|
|
109
|
+
}: LiveRenderSchedulerOptions): LiveRenderScheduler {
|
|
110
|
+
let pendingUpdates: LiveRenderUpdate[] = [];
|
|
111
|
+
let hasPendingAssistantDelta = false;
|
|
112
|
+
let timer: TimerHandle | null = null;
|
|
113
|
+
let isFlushing = false;
|
|
114
|
+
let flushAgain = false;
|
|
115
|
+
let providerEvents = 0;
|
|
116
|
+
let flushes = 0;
|
|
117
|
+
let totalFlushIntervalMs = 0;
|
|
118
|
+
let maxFlushIntervalMs = 0;
|
|
119
|
+
let lastFlushMonotonicMs: number | null = null;
|
|
120
|
+
|
|
121
|
+
const cancelScheduledFlush = () => {
|
|
122
|
+
if (timer) {
|
|
123
|
+
clearTimer(timer);
|
|
124
|
+
timer = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const drain = (): boolean => {
|
|
129
|
+
if (isFlushing) {
|
|
130
|
+
flushAgain = true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cancelScheduledFlush();
|
|
135
|
+
|
|
136
|
+
if (pendingUpdates.length === 0) {
|
|
137
|
+
hasPendingAssistantDelta = false;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isFlushing = true;
|
|
142
|
+
let flushed = false;
|
|
143
|
+
try {
|
|
144
|
+
do {
|
|
145
|
+
flushAgain = false;
|
|
146
|
+
const updates = pendingUpdates;
|
|
147
|
+
pendingUpdates = [];
|
|
148
|
+
hasPendingAssistantDelta = false;
|
|
149
|
+
if (updates.length > 0) {
|
|
150
|
+
flushed = true;
|
|
151
|
+
const startedAt = performance.now();
|
|
152
|
+
flush(updates);
|
|
153
|
+
flushes += 1;
|
|
154
|
+
if (lastFlushMonotonicMs !== null) {
|
|
155
|
+
const intervalMs = Math.max(0, Math.round(startedAt - lastFlushMonotonicMs));
|
|
156
|
+
totalFlushIntervalMs += intervalMs;
|
|
157
|
+
maxFlushIntervalMs = Math.max(maxFlushIntervalMs, intervalMs);
|
|
158
|
+
}
|
|
159
|
+
lastFlushMonotonicMs = startedAt;
|
|
160
|
+
renderDebug.traceSchedulerFlush({
|
|
161
|
+
reason: updates.some((update) => update.type === "assistant" || update.type === "plan") ? "stream" : "progress",
|
|
162
|
+
updates: updates.length,
|
|
163
|
+
assistantChunks: updates.filter((update) => update.type === "assistant").length,
|
|
164
|
+
progressUpdates: updates.filter((update) => update.type === "progress").length,
|
|
165
|
+
toolUpdates: updates.filter((update) => update.type === "tool").length,
|
|
166
|
+
activityUpdates: updates.filter((update) => update.type === "activity").length,
|
|
167
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} while (flushAgain || pendingUpdates.length > 0);
|
|
171
|
+
} finally {
|
|
172
|
+
isFlushing = false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return flushed;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const schedule = () => {
|
|
179
|
+
if (timer || isFlushing) return;
|
|
180
|
+
|
|
181
|
+
const interval = hasPendingAssistantDelta ? assistantFlushMs : progressOnlyFlushMs;
|
|
182
|
+
timer = setTimer(() => {
|
|
183
|
+
timer = null;
|
|
184
|
+
drain();
|
|
185
|
+
}, interval);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
enqueue(update) {
|
|
190
|
+
providerEvents += 1;
|
|
191
|
+
const addedAssistant = mergeLiveRenderUpdate(pendingUpdates, update);
|
|
192
|
+
hasPendingAssistantDelta ||= addedAssistant;
|
|
193
|
+
schedule();
|
|
194
|
+
},
|
|
195
|
+
flushNow: drain,
|
|
196
|
+
cancel() {
|
|
197
|
+
cancelScheduledFlush();
|
|
198
|
+
pendingUpdates = [];
|
|
199
|
+
hasPendingAssistantDelta = false;
|
|
200
|
+
},
|
|
201
|
+
hasPendingUpdates() {
|
|
202
|
+
return pendingUpdates.length > 0;
|
|
203
|
+
},
|
|
204
|
+
getStats() {
|
|
205
|
+
const intervalCount = Math.max(0, flushes - 1);
|
|
206
|
+
return {
|
|
207
|
+
providerEvents,
|
|
208
|
+
flushes,
|
|
209
|
+
averageFlushIntervalMs: intervalCount > 0 ? Math.round(totalFlushIntervalMs / intervalCount) : 0,
|
|
210
|
+
maxFlushIntervalMs,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ConversationMessage } from "../core/workspace/conversationStore.js";
|
|
2
|
+
import type { RunFileOperation } from "../core/workspace/workspaceActivity.js";
|
|
3
|
+
|
|
4
|
+
export type PersistedRunStatus = "completed" | "canceled" | "failed";
|
|
5
|
+
|
|
6
|
+
export interface PersistedFileActivity {
|
|
7
|
+
path: string;
|
|
8
|
+
operation: RunFileOperation;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_SUMMARY_FILES = 20;
|
|
12
|
+
const MAX_SUMMARY_COMMANDS = 10;
|
|
13
|
+
const MAX_COMMAND_CHARS = 80;
|
|
14
|
+
const MAX_ERROR_CHARS = 200;
|
|
15
|
+
|
|
16
|
+
export function selectPersistedAssistantResponse(
|
|
17
|
+
renderedResponse: string | undefined,
|
|
18
|
+
completeResponse: string | undefined,
|
|
19
|
+
): string | undefined {
|
|
20
|
+
return completeResponse ?? renderedResponse;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncate(value: string, maxChars: number): string {
|
|
24
|
+
return value.length > maxChars ? `${value.slice(0, maxChars - 1).trimEnd()}…` : value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function joinCapped(items: readonly string[], cap: number): string {
|
|
28
|
+
const shown = items.slice(0, cap).join(", ");
|
|
29
|
+
return items.length > cap ? `${shown}, +${items.length - cap} more` : shown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Compact, deterministic record of what a run did, stored with its reply for /resume. */
|
|
33
|
+
export function formatRunActivitySummary(
|
|
34
|
+
toolCommands: readonly string[],
|
|
35
|
+
fileActivity: readonly PersistedFileActivity[],
|
|
36
|
+
): string {
|
|
37
|
+
const files = new Map<string, RunFileOperation>();
|
|
38
|
+
for (const entry of fileActivity) {
|
|
39
|
+
const path = entry.path.trim();
|
|
40
|
+
if (!path) continue;
|
|
41
|
+
// A file created during the run stays "created" even if edited again.
|
|
42
|
+
if (files.get(path) === "created" && entry.operation === "modified") continue;
|
|
43
|
+
files.set(path, entry.operation);
|
|
44
|
+
}
|
|
45
|
+
const commands = [...new Set(
|
|
46
|
+
toolCommands
|
|
47
|
+
.map((command) => command.replace(/\s+/g, " ").trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((command) => truncate(command, MAX_COMMAND_CHARS)),
|
|
50
|
+
)];
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
if (files.size > 0) {
|
|
54
|
+
lines.push(`Files changed: ${joinCapped([...files].map(([path, operation]) => `${path} (${operation})`), MAX_SUMMARY_FILES)}`);
|
|
55
|
+
}
|
|
56
|
+
if (commands.length > 0) {
|
|
57
|
+
lines.push(`Commands run: ${joinCapped(commands, MAX_SUMMARY_COMMANDS)}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the assistant message saved for a finished run. Completed replies keep
|
|
64
|
+
* their content byte-for-byte (Local Harness session reuse hashes it) and carry
|
|
65
|
+
* the activity summary separately; interrupted replies keep whatever streamed.
|
|
66
|
+
*/
|
|
67
|
+
export function buildPersistedAssistantMessage(input: {
|
|
68
|
+
status: PersistedRunStatus;
|
|
69
|
+
renderedResponse?: string;
|
|
70
|
+
completeResponse?: string;
|
|
71
|
+
streamedText: string;
|
|
72
|
+
errorMessage?: string;
|
|
73
|
+
toolCommands: readonly string[];
|
|
74
|
+
fileActivity: readonly PersistedFileActivity[];
|
|
75
|
+
}): ConversationMessage | undefined {
|
|
76
|
+
const summary = formatRunActivitySummary(input.toolCommands, input.fileActivity);
|
|
77
|
+
|
|
78
|
+
if (input.status === "completed") {
|
|
79
|
+
const content = selectPersistedAssistantResponse(input.renderedResponse, input.completeResponse);
|
|
80
|
+
if (content?.trim()) {
|
|
81
|
+
return { role: "assistant", content, ...(summary ? { activitySummary: summary } : {}) };
|
|
82
|
+
}
|
|
83
|
+
return summary ? { role: "assistant", content: summary } : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const partial = input.streamedText.trim();
|
|
87
|
+
if (!partial && !summary) return undefined;
|
|
88
|
+
const firstErrorLine = input.errorMessage?.split("\n").find((line) => line.trim())?.trim();
|
|
89
|
+
const note = input.status === "canceled"
|
|
90
|
+
? "[Run canceled before finishing]"
|
|
91
|
+
: `[Run failed${firstErrorLine ? `: ${truncate(firstErrorLine, MAX_ERROR_CHARS)}` : ""}]`;
|
|
92
|
+
return { role: "assistant", content: [partial, note, summary].filter(Boolean).join("\n\n") };
|
|
93
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { AvailableMode } from "../config/settings.js";
|
|
2
|
+
|
|
3
|
+
export type PlanFeedbackMode = "revise" | "constraints";
|
|
4
|
+
|
|
5
|
+
export interface PlanFeedbackRequest {
|
|
6
|
+
mode: PlanFeedbackMode;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PlanFlowContext {
|
|
11
|
+
originalPrompt: string;
|
|
12
|
+
executionMode: AvailableMode;
|
|
13
|
+
constraints: string[];
|
|
14
|
+
planFilePath: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PlanFlowState =
|
|
18
|
+
| { kind: "idle" }
|
|
19
|
+
| (PlanFlowContext & {
|
|
20
|
+
kind: "generating";
|
|
21
|
+
currentPlan: string | null;
|
|
22
|
+
pendingFeedback: PlanFeedbackRequest | null;
|
|
23
|
+
})
|
|
24
|
+
| (PlanFlowContext & {
|
|
25
|
+
kind: "awaiting_action";
|
|
26
|
+
currentPlan: string;
|
|
27
|
+
})
|
|
28
|
+
| (PlanFlowContext & {
|
|
29
|
+
kind: "collecting_feedback";
|
|
30
|
+
currentPlan: string;
|
|
31
|
+
mode: PlanFeedbackMode;
|
|
32
|
+
})
|
|
33
|
+
| (PlanFlowContext & {
|
|
34
|
+
kind: "executing";
|
|
35
|
+
currentPlan: string;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type PlanGeneratingState = Extract<PlanFlowState, { kind: "generating" }>;
|
|
39
|
+
export type PlanAwaitingActionState = Extract<PlanFlowState, { kind: "awaiting_action" }>;
|
|
40
|
+
export type PlanCollectingFeedbackState = Extract<PlanFlowState, { kind: "collecting_feedback" }>;
|
|
41
|
+
export type PlanExecutingState = Extract<PlanFlowState, { kind: "executing" }>;
|
|
42
|
+
|
|
43
|
+
export function createInitialPlanFlowState(): PlanFlowState {
|
|
44
|
+
return { kind: "idle" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The mode an approved plan executes under. Plan mode is orthogonal to the
|
|
49
|
+
* execution mode, so a session in `suggest` (read-only sandbox) can enter plan
|
|
50
|
+
* mode; approving the plan is an explicit request to write, so lift it to
|
|
51
|
+
* `auto-edit`. Write-capable modes are kept as-is.
|
|
52
|
+
*/
|
|
53
|
+
export function resolvePlanExecutionMode(mode: AvailableMode): AvailableMode {
|
|
54
|
+
return mode === "suggest" ? "auto-edit" : mode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function startPlanGeneration(originalPrompt: string, executionMode: AvailableMode): PlanGeneratingState {
|
|
58
|
+
return {
|
|
59
|
+
kind: "generating",
|
|
60
|
+
originalPrompt,
|
|
61
|
+
executionMode: resolvePlanExecutionMode(executionMode),
|
|
62
|
+
constraints: [],
|
|
63
|
+
planFilePath: null,
|
|
64
|
+
currentPlan: null,
|
|
65
|
+
pendingFeedback: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function finishPlanGeneration(
|
|
70
|
+
state: PlanFlowState,
|
|
71
|
+
currentPlan: string,
|
|
72
|
+
planFilePath: string | null,
|
|
73
|
+
): PlanFlowState {
|
|
74
|
+
if (state.kind !== "generating") {
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
kind: "awaiting_action",
|
|
80
|
+
originalPrompt: state.originalPrompt,
|
|
81
|
+
executionMode: state.executionMode,
|
|
82
|
+
constraints: state.constraints,
|
|
83
|
+
currentPlan,
|
|
84
|
+
planFilePath,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function beginPlanFeedback(state: PlanFlowState, mode: PlanFeedbackMode): PlanFlowState {
|
|
89
|
+
if (state.kind !== "awaiting_action") {
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
kind: "collecting_feedback",
|
|
95
|
+
originalPrompt: state.originalPrompt,
|
|
96
|
+
executionMode: state.executionMode,
|
|
97
|
+
constraints: state.constraints,
|
|
98
|
+
currentPlan: state.currentPlan,
|
|
99
|
+
planFilePath: state.planFilePath,
|
|
100
|
+
mode,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function submitPlanFeedback(state: PlanFlowState, text: string): PlanFlowState {
|
|
105
|
+
if (state.kind !== "collecting_feedback") {
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nextConstraints = state.mode === "constraints"
|
|
110
|
+
? [...state.constraints, text]
|
|
111
|
+
: state.constraints;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
kind: "generating",
|
|
115
|
+
originalPrompt: state.originalPrompt,
|
|
116
|
+
executionMode: state.executionMode,
|
|
117
|
+
constraints: nextConstraints,
|
|
118
|
+
planFilePath: state.planFilePath,
|
|
119
|
+
currentPlan: state.currentPlan,
|
|
120
|
+
pendingFeedback: {
|
|
121
|
+
mode: state.mode,
|
|
122
|
+
text,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function cancelPlanFeedback(state: PlanFlowState): PlanFlowState {
|
|
128
|
+
if (state.kind !== "collecting_feedback") {
|
|
129
|
+
return state;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
kind: "awaiting_action",
|
|
134
|
+
originalPrompt: state.originalPrompt,
|
|
135
|
+
executionMode: state.executionMode,
|
|
136
|
+
constraints: state.constraints,
|
|
137
|
+
currentPlan: state.currentPlan,
|
|
138
|
+
planFilePath: state.planFilePath,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function approvePlanExecution(state: PlanFlowState): PlanFlowState {
|
|
143
|
+
if (state.kind !== "awaiting_action") {
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
kind: "executing",
|
|
149
|
+
originalPrompt: state.originalPrompt,
|
|
150
|
+
executionMode: state.executionMode,
|
|
151
|
+
constraints: state.constraints,
|
|
152
|
+
currentPlan: state.currentPlan,
|
|
153
|
+
planFilePath: state.planFilePath,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function resetPlanFlow(): PlanFlowState {
|
|
158
|
+
return createInitialPlanFlowState();
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TimelineEvent } from "./types.js";
|
|
2
|
+
import { getRunPlanText } from "./types.js";
|
|
3
|
+
|
|
4
|
+
function normalizePlanText(text: string): string {
|
|
5
|
+
return text.replace(/\s+/g, " ").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function hasFinalizedTranscriptPlan(events: readonly TimelineEvent[], planText: string | null | undefined): boolean {
|
|
9
|
+
const expected = normalizePlanText(planText ?? "");
|
|
10
|
+
if (!expected) return false;
|
|
11
|
+
|
|
12
|
+
return events.some((event) => {
|
|
13
|
+
if (event.type !== "run") return false;
|
|
14
|
+
if (event.status !== "completed") return false;
|
|
15
|
+
if (event.plan?.status !== "completed") return false;
|
|
16
|
+
const transcriptPlan = normalizePlanText(getRunPlanText(event.plan));
|
|
17
|
+
return transcriptPlan.length > 0 && transcriptPlan === expected;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CancelScheduledPromptRunStart = () => void;
|
|
2
|
+
|
|
3
|
+
export function schedulePromptRunStartAfterVisibleCommit(
|
|
4
|
+
start: () => void,
|
|
5
|
+
): CancelScheduledPromptRunStart {
|
|
6
|
+
let cancelled = false;
|
|
7
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
8
|
+
|
|
9
|
+
queueMicrotask(() => {
|
|
10
|
+
if (cancelled) return;
|
|
11
|
+
timer = setTimeout(() => {
|
|
12
|
+
timer = null;
|
|
13
|
+
if (!cancelled) {
|
|
14
|
+
start();
|
|
15
|
+
}
|
|
16
|
+
}, 0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
cancelled = true;
|
|
21
|
+
if (timer) {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
timer = null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|