Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/agent/src/run-agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,11 @@ export async function runAgentLoop(opts: AgentLoopOptions): Promise<void> {
pendingToolCalls.push(chunk.toolCall)
onChunk(chunk)
}
if (chunk.type === 'prompt_progress') {
// Prefill progress arrives before any text/tool delta; forward it so the
// UI can show a processing indicator on the current step.
onChunk(chunk)
}
if (chunk.type === 'usage') {
streamUsage = {
inputTokens: chunk.inputTokens,
Expand Down
7 changes: 7 additions & 0 deletions packages/llm/src/wire-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export type ProviderStreamChunk =
/** Token counts are a local ~4 chars/token estimate, not agent-reported. */
estimated?: boolean
}
/**
* Prefill progress while the model ingests the prompt (the time-to-first-token
* phase). `fraction` is 0–1 when the provider reports it (e.g. LM Studio's
* native `prompt_processing.progress` events); omit it for an indeterminate
* "processing…" state. Cloud OpenAI/Anthropic do not expose this.
*/
| { type: 'prompt_progress'; fraction?: number }
| { type: 'done'; stopReason?: string }

// ── The provider contract ────────────────────────────────────────────────────
Expand Down
21 changes: 21 additions & 0 deletions src/renderer/agent-activity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { promptProgressLabel } from './agent-activity.ts'

describe('promptProgressLabel', () => {
it('is indeterminate when no fraction is reported', () => {
assert.equal(promptProgressLabel(undefined), 'Processing prompt…')
})

it('renders a rounded percentage when a fraction is reported', () => {
assert.equal(promptProgressLabel(0), 'Processing prompt… 0%')
assert.equal(promptProgressLabel(0.47), 'Processing prompt… 47%')
assert.equal(promptProgressLabel(1), 'Processing prompt… 100%')
})

it('clamps out-of-range and ignores non-finite values', () => {
assert.equal(promptProgressLabel(1.5), 'Processing prompt… 100%')
assert.equal(promptProgressLabel(-0.2), 'Processing prompt… 0%')
assert.equal(promptProgressLabel(Number.NaN), 'Processing prompt…')
})
})
12 changes: 12 additions & 0 deletions src/renderer/agent-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import { formatTodoProgress } from '@shared/todos/todo-logic.ts'

export const CONTEXT_TRIM_ACTIVITY = 'Shortened earlier messages'

/**
* Label for the prefill phase shown on the live status line. `fraction` (0–1)
* is rendered as a percentage when the provider reports it (LM Studio's native
* progress events); without it the label is the indeterminate "Processing
* prompt…".
*/
export function promptProgressLabel(fraction: number | undefined): string {
if (fraction === undefined || !Number.isFinite(fraction)) return 'Processing prompt…'
const pct = Math.round(Math.min(1, Math.max(0, fraction)) * 100)
return `Processing prompt… ${pct.toString()}%`
}

export function runningToolName(thread: Thread): string | null {
for (let i = thread.messages.length - 1; i >= 0; i--) {
const m = thread.messages[i]
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/controller/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
finishSubagent,
} from '@shared/store/subagent-helpers.ts'
import { planAgentTextChunk } from '@copse/agent/agent-text-chunk.ts'
import { syncAgentActivity, CONTEXT_TRIM_ACTIVITY } from '../agent-activity.ts'
import { syncAgentActivity, CONTEXT_TRIM_ACTIVITY, promptProgressLabel } from '../agent-activity.ts'
import { drainMessageQueue, enqueueHookMessage, foldBackContinuationUsed } from './message-queue.ts'
import { usageRecordFromAgentDelta } from '@shared/usage/usage-record-input.ts'
import type { UsageDelta } from '@shared/types'
Expand Down Expand Up @@ -227,6 +227,13 @@ export function startAgentController(store: AppStore, api: ApiClient): () => voi
addUsageDelta(store, threadId, delta)
break
}
case 'prompt_progress': {
// Prefill happens before the first token; surface it on the live status
// line. The next text/tool chunk recomputes the label (Writing…/Running…)
// so this naturally clears once generation starts.
store.emit('agent_activity', threadId, promptProgressLabel(chunk.fraction))
break
}
case 'context_pressure': {
updateContextSnapshot(store, threadId, {
contextWindow: chunk.contextWindow,
Expand Down
Loading