Skip to content
Merged
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
12 changes: 6 additions & 6 deletions runner/src/main/scala/orca/runner/DirtyTreePolicy.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ private[runner] object DirtyTreePolicy:
*
* This writes the terminal outside `TerminalOutput`'s prompt transaction,
* which is otherwise the single owner of the cursor. What makes that safe is
* WHEN it runs: setup precedes the first stage, and the status row is only
* ever raised by a `StageStarted`, so nothing is pinned at the bottom and
* the animator's `tick` is a no-op. Routing it through the transaction
* instead would mean handing setup the run's `Interaction` — including every
* embedder's non-terminal one. Residual: the read blocks uninterruptibly, so
* a fork failing elsewhere in the run's scope waits for the answer.
* WHEN it runs: before any stage or setup activity raises the status row, so
* nothing is pinned at the bottom and the animator's `tick` is a no-op.
* Routing it through the transaction instead would mean handing setup the
* run's `Interaction` — including every embedder's non-terminal one.
* Residual: the read blocks uninterruptibly, so a fork failing elsewhere in
* the run's scope waits for the answer.
*/
def promptOnStderr(dirtyCount: Int): DirtyTreeChoice =
Console.err.println(
Expand Down
13 changes: 11 additions & 2 deletions runner/src/main/scala/orca/runner/FlowLifecycle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ object FlowLifecycle:
StackOutcome.Configured(settings)
case SettingsResolution.NeedsDiscovery(existingContent) =>
val (settings, entries) =
StackDiscovery.discover(agent, workDir, emit, existingContent)
withActivity("Discovering format, lint & test commands", emit):
StackDiscovery.discover(agent, workDir, emit, existingContent)
val fileText = existingContent match
case None => SettingsFile.render(entries)
case Some(content) =>
Expand All @@ -563,6 +564,13 @@ object FlowLifecycle:
)
StackOutcome.Discovered(settings)

private def withActivity[T](label: String, emit: OrcaEvent => Unit)(
op: => T
): T =
emit(OrcaEvent.ActivityStarted(label))
try op
finally emit(OrcaEvent.ActivityEnded())

/** The run's stack settings, and whether [[resolveStackSettings]] just wrote
* them to the settings file, which then still needs its own commit.
*/
Expand Down Expand Up @@ -681,7 +689,8 @@ object FlowLifecycle:
emit: OrcaEvent => Unit
)(using InStage, WorkspaceWrite): FeatureBranch =
val strategy = branchNaming.getOrElse(BranchNamingStrategy.shortenPrompt)
val resolvedName = strategy.resolve(userPrompt, agent)
val resolvedName = withActivity("Naming the branch", emit):
strategy.resolve(userPrompt, agent)
// Resolved once, shared by both fallback triggers below (a
// protected-name refusal and a git-level `BranchAlreadyExists`
// collision use the exact same deterministic name).
Expand Down
5 changes: 4 additions & 1 deletion runner/src/main/scala/orca/runner/LoggingListener.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ private[orca] class LoggingListener extends OrcaListener:
log.info("stage start: {}", path.display)
case OrcaEvent.StageEnded(path, outcome) =>
log.info("stage end: {} ({})", path.display, outcome)
case OrcaEvent.Step(message) => log.info("step: {}", message)
case OrcaEvent.ActivityStarted(label) =>
log.info("activity start: {}", label)
case _: OrcaEvent.ActivityEnded => log.info("activity end")
case OrcaEvent.Step(message) => log.info("step: {}", message)
case OrcaEvent.Bookkeeping(message) =>
log.info("bookkeeping: {}", message)
case OrcaEvent.Caveat(message) => log.info("caveat: {}", message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ private[terminal] class TerminalEventRenderer(
stack = stack.drop(1)
stageEmitters = StageEmitters.Silent
output.setStatus(stack.headOption)
case OrcaEvent.ActivityStarted(label) =>
output.setStatus(Some(label))
case _: OrcaEvent.ActivityEnded =>
output.setStatus(stack.headOption)
case OrcaEvent.ToolUse(tool, args, agent) =>
// Recorded before the branch: an agent whose reads go out unnamed is
// still an emitter, so its prose and writes get named once the stage has
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ private[terminal] object TerminalOutputState:
/** Carriage return + ANSI Erase-In-Line-2 (clear entire line). `\u001b` is
* the ESC byte, a Unicode escape so the source stays grep-friendly.
*/
private val ClearLine: String = "\r\u001b[2K"
private[terminal] val ClearLine: String = "\r\u001b[2K"

private val DefaultLabel: String = "Thinking..."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,23 @@ class TerminalEventRendererTest extends munit.FunSuite:
s"outer stage title leaked into the status bar; tail was: '$tail'"
)

test("an activity outside any stage pins its label until it ends"):
val rendered = renderWith(
animated = true,
List(
OrcaEvent.ActivityStarted("Naming the branch"),
OrcaEvent.ActivityEnded()
)
)
assert(
rendered.contains("Naming the branch"),
s"activity label missing from the status row; got: '$rendered'"
)
assert(
rendered.endsWith(TerminalOutputState.ClearLine),
s"status row should be cleared once the activity ends; got: '$rendered'"
)

test("nested stages indent inner content; no ✔ ever appears in the log"):
val output = renderEvents(
List(
Expand Down
10 changes: 10 additions & 0 deletions tools/src/main/scala/orca/events/OrcaEvent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ enum OrcaEvent:
/** The stage at `path` ended; see [[StageStarted]]. */
case StageEnded private[orca] (path: StagePath.Stage, outcome: StageOutcome)

/** Orca began work outside any stage, such as a setup model call; `label`
* names it for display. Always followed by [[ActivityEnded]], before the
* next `ActivityStarted` or [[StageStarted]]; emitted on the flow's owner
* thread.
*/
case ActivityStarted private[orca] (label: String)

/** The work announced by the preceding [[ActivityStarted]] finished. */
case ActivityEnded private[orca] ()

/** One tool invocation by the agent named in `agent`. Backends emit `None` —
* a drain doesn't know which agent it runs for;
* [[OrcaListener.attributedTo]] wraps it and stamps the name on the way out.
Expand Down
Loading