Skip to content

feat: add progress spinner for create commands#78

Merged
jo-hnny merged 1 commit into
TencentCloudAgentRuntime:mainfrom
fredaluliu:feature/progress-spinner
Jul 21, 2026
Merged

feat: add progress spinner for create commands#78
jo-hnny merged 1 commit into
TencentCloudAgentRuntime:mainfrom
fredaluliu:feature/progress-spinner

Conversation

@fredaluliu

Copy link
Copy Markdown
Contributor

Summary

Add a lightweight terminal spinner (Braille dot animation, 80ms interval) for instance create and tool create commands. Provides visual feedback during API calls.

Changes

New files

  • internal/progress/spinner.go — Core spinner implementation (no external deps)
  • internal/progress/spinner_test.go — 9 unit tests

Modified

  • internal/cli/exports.go — Export IsJSONOutput()
  • internal/commands/instance/create/command.go — Integrate spinner
  • internal/commands/tool/create/command.go — Integrate spinner
  • internal/commands/instance/create/command_test.go — 10 end-to-end tests
  • internal/commands/tool/create/command_test.go — 11 end-to-end tests

Behavior

Condition Spinner
Interactive terminal + text mode ✅ Animated
-o json ❌ Nop
--non-interactive ❌ Nop
stdout non-TTY (pipe/redirect) ❌ Nop

Demo

⠋ Creating instance...   ← animating
✓ Instance created        ← success
⠋ Creating instance...
✗ Failed to create instance  ← error

Design

  • Writes to stderr (stdout stays pure for JSON/pipe)
  • 80ms Braille frames (matches AgentCore CLI)
  • Zero external dependencies
  • Mutex-based concurrency safety
  • Multi-step support via repeated Start()/Stop()

Closes #75

@KayIter
KayIter self-requested a review July 16, 2026 09:48

@KayIter KayIter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the spinner. The JSON/non-interactive guard and stderr separation are heading in the right direction, but I found one user-visible correctness issue that should be fixed before merge.

Required

instance create prints the success spinner status before all local success validation has completed:

result, err := executor.Execute(ctx, apiReq)
if err != nil {
sp.Stop("✗", "Failed to create instance")
return nil, err
}
sp.Stop("✓", "Instance created")
response, ok := result.Data.(*ags.StartSandboxInstanceResponseParams)
if !ok {
return result, nil
}
if response.Instance == nil {
return nil, output.NewCLIError(&output.Failure{

Right now sp.Stop("✓", "Instance created") runs immediately after executor.Execute returns nil error, but the handler still checks whether the typed response contains response.Instance. If the control plane returns &ags.StartSandboxInstanceResponseParams{} (typed success response, but missing Instance), the command returns INTERNAL_ERROR: no instance returned from API while stderr has already printed ✓ Instance created.

I reproduced this locally with a fake control plane and forced TTY:

err=no instance returned from API
stderr="\r\x1b[K✓ Instance created\n"

Please move the success Stop until after all local response validation succeeds, just before returning the final successful result. It would also be good to restore/add a regression test for the missing-Instance path and assert that success is not printed on that error path.

Suggestion

The spinner writes to ErrOut, but the enable check uses deps.IO.IsStdoutTTY():

// Show spinner for interactive text mode only.
var sp *progress.Spinner
if !cli.IsJSONOutput() && !cli.NonInteractive() && deps.IO != nil && deps.IO.IsStdoutTTY() {
sp = progress.New(deps.IO.ErrOut)

// Show spinner for interactive text mode only.
var sp *progress.Spinner
if !cli.IsJSONOutput() && !cli.NonInteractive() && deps.IO != nil && deps.IO.IsStdoutTTY() {
sp = progress.New(deps.IO.ErrOut)

That means an interactive command with stderr redirected, e.g. agr instance create ... 2>log, can write carriage returns / ANSI clear-line sequences / spinner frames into the log file. A more precise guard would check whether stderr is a TTY, or let the spinner decide from the actual writer.

I also ran the focused packages locally with Go 1.25.0:

go test ./internal/progress ./internal/commands/instance/create ./internal/commands/tool/create

Those tests pass; this is a behavior gap rather than a currently failing CI test.

@fredaluliu
fredaluliu force-pushed the feature/progress-spinner branch 5 times, most recently from 3733b8b to b944a24 Compare July 17, 2026 10:24

@KayIter KayIter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instance create 之前提到的误报已经修掉了,stderr TTY 判断也已经改对;但 tool create 还有同类问题。

[P2] tool create 在没有确认创建结果有效时仍会打印成功 spinner。

现在流程是先执行 applyCreateResultText(result, req),这个函数在 result.Data 不是 *ags.CreateSandboxToolResponseParams 时会直接 return;但外层随后无条件执行:

sp.Stop("✓", "Tool created")
return result, nil

所以如果 API/adapter 返回了非 typed response,或者后续出现 typed response 但 ToolId 为空的情况,交互式 stderr 仍会出现 ✓ Tool created。这和之前 instance create 的问题是同一类:用户会看到成功提示,但 CLI 实际没有足够信息确认工具创建成功。

我用临时回归测试复现了非 typed response 情况,stderr 为:

\r\x1b[K✓ Tool created\n

建议和 instance create 保持一致:只有确认 response 类型正确且关键返回值(至少 ToolId)存在后才 sp.Stop("✓", ...);否则 cleanup,或返回 structured internal error。

本地验证:

  • go test ./internal/progress ./internal/commands/instance/create ./internal/commands/tool/create ./internal/iostreams 通过
  • 临时 repro 测试可稳定复现上述误报,测试文件已清理

@fredaluliu
fredaluliu force-pushed the feature/progress-spinner branch 2 times, most recently from 81b8a2f to 245e013 Compare July 21, 2026 03:12
Add a lightweight terminal spinner (Braille animation, 80ms) for
instance create and tool create commands. Provides visual feedback
during API calls.

Features:
- Spinner writes to stderr, checks IsStderrTTY() for enable guard
- Disabled for JSON output, non-interactive mode, or non-TTY stderr
- sp.Stop only prints success after all response validation passes
- response.Instance == nil prints failure status (not false success)
- NewForCLI() helper eliminates duplicate guard logic across commands

New files:
- internal/progress/spinner.go: core implementation (no external deps)
- internal/progress/spinner_test.go: 9 unit tests

Modified:
- internal/cli/exports.go: export IsJSONOutput()
- internal/iostreams/iostreams.go: add IsStderrTTY/SetStderrTTY
- internal/commands/instance/create/command.go: integrate spinner
- internal/commands/tool/create/command.go: integrate spinner
- internal/commands/instance/create/command_test.go: e2e tests
- internal/commands/tool/create/command_test.go: e2e tests

Closes TencentCloudAgentRuntime#75
@fredaluliu
fredaluliu force-pushed the feature/progress-spinner branch from 245e013 to 64549be Compare July 21, 2026 04:38

@KayIter KayIter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复查了最新提交 64549be,之前 tool create 成功 spinner 误报的问题已经修掉了。

现在 applyCreateResultText 会返回是否确认到了有效 CreateSandboxToolResponseParams 和非空 ToolId;只有返回 true 才打印 ✓ Tool created,否则 cleanup,不会再对非 typed response/缺失 ToolId 的情况报成功。

我也重新跑了上次的临时 repro,非 typed response + stderr TTY 时不会再输出 ✓ Tool created

本地验证通过:

  • go test ./internal/progress ./internal/commands/instance/create ./internal/commands/tool/create ./internal/iostreams

LGTM.

@jo-hnny
jo-hnny merged commit 74b4196 into TencentCloudAgentRuntime:main Jul 21, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ux): add progress spinner for long-running commands

3 participants