From 329ee99a1c746d52641d5fa06404670f4f99d8bf Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 3 Aug 2026 16:59:35 -0400 Subject: [PATCH 1/9] fix(simulate): erase the job view a reprint replaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detail view is printed into the scrollback incrementally: flushDetail diffs the render against what it already printed and prints only the growth. A render that is not an append of the previous one cannot be patched in place, so it is reprinted whole — but clearScrollback was only prepended on the first print of a job, leaving the superseded copy above it. Toggling logs with ctrl+L off is exactly that case (the Logs block is the tail of renderDetail, so turning it on appends but turning it off shortens), so every toggle stacked another copy of the job. --- cmd/lk/simulate_tui.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index 0ec3b9bb..98ea28de 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -1696,12 +1696,14 @@ func (m *simulateModel) flushDetail() tea.Cmd { return nil } tail, ok := detailTail(m.detailPrinted, rendered) - first := m.detailPrinted == "" + // a whole-body reprint has to erase what it replaces, or the copy it + // supersedes stays in the scrollback above it + reprint := m.detailPrinted == "" || !strings.HasPrefix(rendered, m.detailPrinted) m.detailPrinted = rendered if !ok { return nil } - if first { + if reprint { tail = clearScrollback + tail } return tea.Println(tail) From 0dc4724c98045f143cae50692bc486fbab0d2d31 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 3 Aug 2026 20:06:59 -0400 Subject: [PATCH 2/9] feat(simulate): t expands full tool calls and outputs in a job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transcript clips tool arguments and outputs to 80 characters, which is right for reading the conversation but hides exactly what a tool was asked for and what came back — the part you want when a job failed on a tool call. t, in the job detail view, toggles the clip off: arguments and outputs render whole, wrapped to the transcript measure with continuations indented under the marker. The hint offers the key only when the open job has something clipped to reveal. Clipping now cuts on a rune boundary; tool payloads carry guest names and quoted speech, and a byte slice could halve a rune. --- cmd/lk/simulate_tui.go | 100 +++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 14 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index 98ea28de..5eab176f 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -23,6 +23,7 @@ import ( "sort" "strings" "time" + "unicode/utf8" "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/textinput" @@ -222,9 +223,12 @@ type simulateModel struct { // been emitted for it, so a re-render only ever appends its new tail. // detailWidth is the width that text was wrapped at: scrollback cannot be // re-wrapped, so a resize rebaselines instead of reprinting. - detailPrinted string - detailWidth int - showLogs bool + detailPrinted string + detailWidth int + showLogs bool + // tool call arguments and outputs are clipped to a preview in the + // transcript; showToolDetail renders them whole instead + showToolDetail bool logScrollOff int logPinned bool logPinnedTotal int @@ -895,6 +899,10 @@ func (m *simulateModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.showLogs = !m.showLogs m.logScrollOff = 0 m.logPinned = false + case "t": + if m.detailJobID != "" { + m.showToolDetail = !m.showToolDetail + } case "d": if m.detailJobID == "" && m.hasDescription() { m.showDescription = !m.showDescription @@ -1866,25 +1874,16 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { } case *agent.ChatContext_ChatItem_FunctionCall: fc := v.FunctionCall - args := fc.Arguments - if len(args) > 80 { - args = args[:80] + "..." - } ensureAgentBlock() - b.WriteString(dimStyle.Render(fmt.Sprintf(" ƒ %s(%s)", fc.Name, args))) - b.WriteString("\n") + m.writeToolItem(&b, fmt.Sprintf("ƒ %s(%s)", fc.Name, m.toolValue(fc.Arguments)), wrapWidth) case *agent.ChatContext_ChatItem_FunctionCallOutput: fco := v.FunctionCallOutput output := strings.TrimSpace(fco.Output) if output == "" { continue } - if len(output) > 80 { - output = output[:80] + "..." - } ensureAgentBlock() - b.WriteString(dimStyle.Render(fmt.Sprintf(" → %s", output))) - b.WriteString("\n") + m.writeToolItem(&b, "→ "+m.toolValue(output), wrapWidth) case *agent.ChatContext_ChatItem_AgentHandoff: h := v.AgentHandoff old := "" @@ -1899,6 +1898,72 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { return b.String() } +// toolPreviewLen is how much of a tool call's arguments or output the +// transcript shows when tool detail is collapsed — enough to tell two calls +// apart without a lookup table's worth of JSON burying the conversation. +const toolPreviewLen = 80 + +// toolValue is a tool argument blob or output as the transcript should carry +// it: clipped to a preview, or whole when tool detail is expanded. +func (m *simulateModel) toolValue(s string) string { + if m.showToolDetail || len(s) <= toolPreviewLen { + return s + } + // clip on a rune boundary; arguments and outputs carry guest names, + // currency symbols, and quoted speech + clipped := s[:toolPreviewLen] + for len(clipped) > 0 && !utf8.ValidString(clipped) { + clipped = clipped[:len(clipped)-1] + } + return clipped + "..." +} + +// writeToolItem appends one tool line to b. Collapsed, it is a single row and +// the terminal deals with any overflow. Expanded, it wraps to the transcript's +// measure with its continuations indented under the marker, so a long output +// stays readable as a block instead of one run-on row. +func (m *simulateModel) writeToolItem(b *strings.Builder, text string, wrapWidth int) { + if !m.showToolDetail { + b.WriteString(dimStyle.Render(" " + text)) + b.WriteString("\n") + return + } + for i, line := range wrapLines(text, wrapWidth-2) { + indent := " " + if i > 0 { + indent = " " + } + b.WriteString(dimStyle.Render(indent + line)) + b.WriteString("\n") + } +} + +// hasToolDetail reports whether the open job's transcript holds anything the +// tool-detail toggle would reveal, so the hint is only offered when it does +// something. +func (m *simulateModel) hasToolDetail(jobID string) bool { + if m.summary == nil || m.summary.ChatHistory == nil { + return false + } + chatCtx, ok := m.summary.ChatHistory[jobID] + if !ok || chatCtx == nil { + return false + } + for _, item := range chatCtx.Items { + switch v := item.Item.(type) { + case *agent.ChatContext_ChatItem_FunctionCall: + if len(v.FunctionCall.Arguments) > toolPreviewLen { + return true + } + case *agent.ChatContext_ChatItem_FunctionCallOutput: + if len(strings.TrimSpace(v.FunctionCallOutput.Output)) > toolPreviewLen { + return true + } + } + } + return false +} + func chatMessageText(msg *agent.ChatMessage) string { if msg == nil || len(msg.Content) == 0 { return "" @@ -2018,6 +2083,13 @@ func (m *simulateModel) renderHint() string { case m.detailJobID != "": // the job view is in the terminal's scrollback, which scrolls itself parts = append(parts, "c copy scenario · ←/ESC back to list") + if m.hasToolDetail(m.detailJobID) { + if m.showToolDetail { + parts = append(parts, "t clip tool detail") + } else { + parts = append(parts, "t full tool detail") + } + } if m.hasLogs() { if m.showLogs { parts = append(parts, "Ctrl+L hide logs") From 5dce3ef03f51a90bbee8af2ede65b057a87ff646 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 4 Aug 2026 07:25:32 -0400 Subject: [PATCH 3/9] fix(simulate): carry --project into the re-open hint --- cmd/lk/simulate.go | 12 ++++++++++-- cmd/lk/simulate_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 51fae425..bab6bbaf 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -47,6 +47,9 @@ func init() { var ( simulateProjectConfig *config.ProjectConfig + // simulateProjectFlag is the explicit --project name, if any; the hints must + // reproduce it or a re-open resolves against a different project. + simulateProjectFlag string ) const ( @@ -68,6 +71,7 @@ var simulateCommand = &cli.Command{ return nil, err } simulateProjectConfig = pc + simulateProjectFlag = cmd.String("project") return nil, nil }, Action: runSimulate, @@ -582,8 +586,9 @@ func dashboardBaseURL() string { } // viewCommandHint returns the command to re-open a simulation run, carrying -// over --server-url when the run lives somewhere other than the default cloud -// API (e.g. staging), so the printed command targets the same environment. +// over --project and --server-url when the run lives somewhere other than the +// default project and cloud API (e.g. staging), so the printed command targets +// the same project and environment. // The binary name comes from argv[0] so a renamed or path-qualified lk is // reproduced verbatim. func viewCommandHint(runID string) string { @@ -592,6 +597,9 @@ func viewCommandHint(runID string) string { binary = os.Args[0] } hint := binary + " agent simulate --view " + runID + if simulateProjectFlag != "" { + hint += " --project " + simulateProjectFlag + } if serverURL != cloudAPIServerURL { hint += " --server-url " + serverURL } diff --git a/cmd/lk/simulate_test.go b/cmd/lk/simulate_test.go index 13b57a53..54e49959 100644 --- a/cmd/lk/simulate_test.go +++ b/cmd/lk/simulate_test.go @@ -76,3 +76,27 @@ func TestViewCommandHintCarriesServerURL(t *testing.T) { "lk agent simulate --view run_123 --server-url https://cloud-api.staging.livekit.io", viewCommandHint("run_123")) } + +func TestViewCommandHintCarriesProject(t *testing.T) { + origArgs := os.Args + origServerURL := serverURL + origProject := simulateProjectFlag + t.Cleanup(func() { + os.Args = origArgs + serverURL = origServerURL + simulateProjectFlag = origProject + }) + os.Args = []string{"lk"} + serverURL = "https://cloud-api.example.com" + simulateProjectFlag = "my-project" + + require.Equal(t, + "lk agent simulate --view run_123 --project my-project"+ + " --server-url https://cloud-api.example.com", + viewCommandHint("run_123")) + + simulateProjectFlag = "" + require.Equal(t, + "lk agent simulate --view run_123 --server-url https://cloud-api.example.com", + viewCommandHint("run_123")) +} From 9031f99fce9f15965d5a0e62d528c464116c49ef Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 4 Aug 2026 07:41:32 -0400 Subject: [PATCH 4/9] feat(simulate): t shows and hides tool output, hidden by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool outputs are payloads written for the model, not for a reader: a policy blob or a lookup table's worth of JSON between two spoken turns buries the conversation the job is about. Keep them off the transcript and put them behind t, whole rather than clipped, since a clipped payload answers nothing about why a job failed on a tool call. The tool call itself stays on the transcript either way — which tool ran, with a preview of its arguments, is part of reading what the agent did. --- cmd/lk/simulate_tui.go | 70 ++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index 5eab176f..15f8dc3e 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -226,9 +226,9 @@ type simulateModel struct { detailPrinted string detailWidth int showLogs bool - // tool call arguments and outputs are clipped to a preview in the - // transcript; showToolDetail renders them whole instead - showToolDetail bool + // tool outputs are off the transcript unless asked for: they are payloads + // written for the model, and at full length they bury the conversation + showToolOutput bool logScrollOff int logPinned bool logPinnedTotal int @@ -901,7 +901,7 @@ func (m *simulateModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.logPinned = false case "t": if m.detailJobID != "" { - m.showToolDetail = !m.showToolDetail + m.showToolOutput = !m.showToolOutput } case "d": if m.detailJobID == "" && m.hasDescription() { @@ -1875,15 +1875,18 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { case *agent.ChatContext_ChatItem_FunctionCall: fc := v.FunctionCall ensureAgentBlock() - m.writeToolItem(&b, fmt.Sprintf("ƒ %s(%s)", fc.Name, m.toolValue(fc.Arguments)), wrapWidth) + writeToolItem(&b, fmt.Sprintf("ƒ %s(%s)", fc.Name, clipToolValue(fc.Arguments)), wrapWidth) case *agent.ChatContext_ChatItem_FunctionCallOutput: + if !m.showToolOutput { + continue + } fco := v.FunctionCallOutput output := strings.TrimSpace(fco.Output) if output == "" { continue } ensureAgentBlock() - m.writeToolItem(&b, "→ "+m.toolValue(output), wrapWidth) + writeToolItem(&b, "→ "+output, wrapWidth) case *agent.ChatContext_ChatItem_AgentHandoff: h := v.AgentHandoff old := "" @@ -1898,19 +1901,18 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { return b.String() } -// toolPreviewLen is how much of a tool call's arguments or output the -// transcript shows when tool detail is collapsed — enough to tell two calls -// apart without a lookup table's worth of JSON burying the conversation. +// toolPreviewLen is how much of a tool call's arguments the transcript shows — +// enough to tell two calls apart without a lookup table's worth of JSON burying +// the conversation. Tool outputs are shown whole, or not at all. const toolPreviewLen = 80 -// toolValue is a tool argument blob or output as the transcript should carry -// it: clipped to a preview, or whole when tool detail is expanded. -func (m *simulateModel) toolValue(s string) string { - if m.showToolDetail || len(s) <= toolPreviewLen { +// clipToolValue is a tool argument blob as the transcript carries it. +func clipToolValue(s string) string { + if len(s) <= toolPreviewLen { return s } - // clip on a rune boundary; arguments and outputs carry guest names, - // currency symbols, and quoted speech + // clip on a rune boundary; arguments carry guest names, currency symbols, + // and quoted speech clipped := s[:toolPreviewLen] for len(clipped) > 0 && !utf8.ValidString(clipped) { clipped = clipped[:len(clipped)-1] @@ -1918,16 +1920,10 @@ func (m *simulateModel) toolValue(s string) string { return clipped + "..." } -// writeToolItem appends one tool line to b. Collapsed, it is a single row and -// the terminal deals with any overflow. Expanded, it wraps to the transcript's -// measure with its continuations indented under the marker, so a long output -// stays readable as a block instead of one run-on row. -func (m *simulateModel) writeToolItem(b *strings.Builder, text string, wrapWidth int) { - if !m.showToolDetail { - b.WriteString(dimStyle.Render(" " + text)) - b.WriteString("\n") - return - } +// writeToolItem appends one tool line to b, wrapped to the transcript's measure +// with its continuations indented under the marker, so a long output stays +// readable as a block instead of one run-on row. +func writeToolItem(b *strings.Builder, text string, wrapWidth int) { for i, line := range wrapLines(text, wrapWidth-2) { indent := " " if i > 0 { @@ -1938,10 +1934,9 @@ func (m *simulateModel) writeToolItem(b *strings.Builder, text string, wrapWidth } } -// hasToolDetail reports whether the open job's transcript holds anything the -// tool-detail toggle would reveal, so the hint is only offered when it does -// something. -func (m *simulateModel) hasToolDetail(jobID string) bool { +// hasToolOutput reports whether the open job's transcript holds a tool output, +// so the hint is only offered when the toggle would show something. +func (m *simulateModel) hasToolOutput(jobID string) bool { if m.summary == nil || m.summary.ChatHistory == nil { return false } @@ -1950,13 +1945,8 @@ func (m *simulateModel) hasToolDetail(jobID string) bool { return false } for _, item := range chatCtx.Items { - switch v := item.Item.(type) { - case *agent.ChatContext_ChatItem_FunctionCall: - if len(v.FunctionCall.Arguments) > toolPreviewLen { - return true - } - case *agent.ChatContext_ChatItem_FunctionCallOutput: - if len(strings.TrimSpace(v.FunctionCallOutput.Output)) > toolPreviewLen { + if v, ok := item.Item.(*agent.ChatContext_ChatItem_FunctionCallOutput); ok { + if strings.TrimSpace(v.FunctionCallOutput.Output) != "" { return true } } @@ -2083,11 +2073,11 @@ func (m *simulateModel) renderHint() string { case m.detailJobID != "": // the job view is in the terminal's scrollback, which scrolls itself parts = append(parts, "c copy scenario · ←/ESC back to list") - if m.hasToolDetail(m.detailJobID) { - if m.showToolDetail { - parts = append(parts, "t clip tool detail") + if m.hasToolOutput(m.detailJobID) { + if m.showToolOutput { + parts = append(parts, "t hide tool output") } else { - parts = append(parts, "t full tool detail") + parts = append(parts, "t tool output") } } if m.hasLogs() { From c32b4e83306ccbc7676b9d1178ee73cfaad2fb65 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 4 Aug 2026 07:46:59 -0400 Subject: [PATCH 5/9] fix(simulate): say show in the tool output hint The verb was implicit on the way in and explicit on the way out. --- cmd/lk/simulate_tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index 15f8dc3e..5e3f7ae7 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -2077,7 +2077,7 @@ func (m *simulateModel) renderHint() string { if m.showToolOutput { parts = append(parts, "t hide tool output") } else { - parts = append(parts, "t tool output") + parts = append(parts, "t show tool output") } } if m.hasLogs() { From 53f6847666a7015f27a9901a26ff47c1df6e9fd7 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 4 Aug 2026 07:51:14 -0400 Subject: [PATCH 6/9] fix(simulate): send the project ID when fetching a simulation run GetSimulationRun accepts an API key or a session token, and the session path rejects the request when project_id is absent. Every caller already has the resolved project, so pass it through. --- cmd/lk/simulate.go | 6 +++++- cmd/lk/simulate_ci.go | 4 ++-- cmd/lk/simulate_tui.go | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index bab6bbaf..ce41e376 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -550,9 +550,13 @@ func uploadSource(ctx context.Context, client *lksdk.AgentSimulationClient, runI return nil } -func getSimulationRun(ctx context.Context, client *lksdk.AgentSimulationClient, runID string) (*livekit.SimulationRun, error) { +// getSimulationRun carries the project ID because the server accepts either an +// API key or a session token, and the session path cannot resolve the run +// without it. +func getSimulationRun(ctx context.Context, client *lksdk.AgentSimulationClient, runID, projectID string) (*livekit.SimulationRun, error) { resp, err := client.GetSimulationRun(ctx, &livekit.SimulationRun_Get_Request{ SimulationRunId: runID, + ProjectId: projectID, }) if err != nil { return nil, err diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index fbbee3c8..1514bde8 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -146,7 +146,7 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { for { pollCtx, pollCancel := context.WithTimeout(ctx, simulationAPITimeout) - run, err = getSimulationRun(pollCtx, config.client, runID) + run, err = getSimulationRun(pollCtx, config.client, runID, config.pc.ProjectId) pollCancel() if err != nil { @@ -254,7 +254,7 @@ func runSimulateCIView(ctx context.Context, config *simulateConfig) error { for { pollCtx, pollCancel := context.WithTimeout(ctx, simulationAPITimeout) var err error - run, err = getSimulationRun(pollCtx, config.client, runID) + run, err = getSimulationRun(pollCtx, config.client, runID, config.pc.ProjectId) pollCancel() if err != nil { if ctx.Err() != nil { diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index 5e3f7ae7..bc506581 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -427,7 +427,7 @@ func (m *simulateModel) runSetup() tea.Cmd { if c.mode == modeView { ctx, cancel := context.WithTimeout(context.Background(), simulationAPITimeout) defer cancel() - run, err := getSimulationRun(ctx, m.config.client, m.config.viewModeRunID) + run, err := getSimulationRun(ctx, m.config.client, m.config.viewModeRunID, m.config.pc.ProjectId) if err != nil { m.err = err } @@ -569,7 +569,7 @@ func (m *simulateModel) pollSimulation() tea.Cmd { return func() tea.Msg { ctx, cancel := context.WithTimeout(context.Background(), simulationAPITimeout) defer cancel() - run, err := getSimulationRun(ctx, m.config.client, m.runID) + run, err := getSimulationRun(ctx, m.config.client, m.runID, m.config.pc.ProjectId) return simulationRunMsg{run: run, err: err} } } From e804aac24ad05676d67e8b752e13497b2f49e1c1 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Wed, 5 Aug 2026 13:27:19 -0400 Subject: [PATCH 7/9] fix(simulate): take the hint's project from the resolved config The re-open hint read --project off the command, so it only reproduced an explicitly passed project. The resolved config already carries the name for every configured-project path, and pinning it means the hint targets the same project even if the default changes. --- cmd/lk/simulate.go | 17 ++++++++--------- cmd/lk/simulate_test.go | 11 +++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index ce41e376..65acb17e 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -47,9 +47,6 @@ func init() { var ( simulateProjectConfig *config.ProjectConfig - // simulateProjectFlag is the explicit --project name, if any; the hints must - // reproduce it or a re-open resolves against a different project. - simulateProjectFlag string ) const ( @@ -71,7 +68,6 @@ var simulateCommand = &cli.Command{ return nil, err } simulateProjectConfig = pc - simulateProjectFlag = cmd.String("project") return nil, nil }, Action: runSimulate, @@ -590,9 +586,12 @@ func dashboardBaseURL() string { } // viewCommandHint returns the command to re-open a simulation run, carrying -// over --project and --server-url when the run lives somewhere other than the -// default project and cloud API (e.g. staging), so the printed command targets -// the same project and environment. +// over the resolved project and --server-url when the run lives somewhere +// other than the default cloud API (e.g. staging), so the printed command +// targets the same project and environment regardless of which project is +// default when it is run. The project name is empty when credentials came from +// flags or the environment rather than a configured project, and no --project +// would resolve those. // The binary name comes from argv[0] so a renamed or path-qualified lk is // reproduced verbatim. func viewCommandHint(runID string) string { @@ -601,8 +600,8 @@ func viewCommandHint(runID string) string { binary = os.Args[0] } hint := binary + " agent simulate --view " + runID - if simulateProjectFlag != "" { - hint += " --project " + simulateProjectFlag + if simulateProjectConfig != nil && simulateProjectConfig.Name != "" { + hint += " --project " + simulateProjectConfig.Name } if serverURL != cloudAPIServerURL { hint += " --server-url " + serverURL diff --git a/cmd/lk/simulate_test.go b/cmd/lk/simulate_test.go index 54e49959..9141194b 100644 --- a/cmd/lk/simulate_test.go +++ b/cmd/lk/simulate_test.go @@ -19,6 +19,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/livekit/livekit-cli/v2/pkg/config" ) func TestSimulateConfigWarnings(t *testing.T) { @@ -80,22 +82,23 @@ func TestViewCommandHintCarriesServerURL(t *testing.T) { func TestViewCommandHintCarriesProject(t *testing.T) { origArgs := os.Args origServerURL := serverURL - origProject := simulateProjectFlag + origProject := simulateProjectConfig t.Cleanup(func() { os.Args = origArgs serverURL = origServerURL - simulateProjectFlag = origProject + simulateProjectConfig = origProject }) os.Args = []string{"lk"} serverURL = "https://cloud-api.example.com" - simulateProjectFlag = "my-project" + simulateProjectConfig = &config.ProjectConfig{Name: "my-project"} require.Equal(t, "lk agent simulate --view run_123 --project my-project"+ " --server-url https://cloud-api.example.com", viewCommandHint("run_123")) - simulateProjectFlag = "" + // credentials from flags or the environment resolve no project name + simulateProjectConfig = &config.ProjectConfig{} require.Equal(t, "lk agent simulate --view run_123 --server-url https://cloud-api.example.com", viewCommandHint("run_123")) From ea2104e4b5ca461e3d416bd64930a4a2bf122ead Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Wed, 5 Aug 2026 13:30:07 -0400 Subject: [PATCH 8/9] test(simulate): drop cmd/lk/simulate_test.go --- cmd/lk/simulate_test.go | 105 ---------------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 cmd/lk/simulate_test.go diff --git a/cmd/lk/simulate_test.go b/cmd/lk/simulate_test.go deleted file mode 100644 index 9141194b..00000000 --- a/cmd/lk/simulate_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2026 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/livekit/livekit-cli/v2/pkg/config" -) - -func TestSimulateConfigWarnings(t *testing.T) { - // -n with a scenarios file: the flag does nothing, warn. - warns := simulateConfigWarnings(modeScenarios, 5) - require.Len(t, warns, 1) - require.Contains(t, warns[0], "--num-simulations has no effect") - require.Contains(t, warns[0], "--concurrency") - - // -n while generating from source: the flag is meaningful, no warning. - require.Empty(t, simulateConfigWarnings(modeGenerateFromSource, 5)) - // no -n at all: no warning. - require.Empty(t, simulateConfigWarnings(modeScenarios, 0)) - // view mode ignores everything silently. - require.Empty(t, simulateConfigWarnings(modeView, 5)) -} - -func TestViewCommandHintUsesArgv0(t *testing.T) { - origArgs := os.Args - origServerURL := serverURL - t.Cleanup(func() { - os.Args = origArgs - serverURL = origServerURL - }) - serverURL = cloudAPIServerURL - - for _, tc := range []struct { - name string - argv0 string - want string - }{ - {"plain lk", "lk", "lk agent simulate --view run_123"}, - {"path-qualified", "/usr/local/bin/lk", "/usr/local/bin/lk agent simulate --view run_123"}, - {"renamed binary", "lk-dev", "lk-dev agent simulate --view run_123"}, - {"empty argv0 falls back", "", "lk agent simulate --view run_123"}, - } { - t.Run(tc.name, func(t *testing.T) { - os.Args = []string{tc.argv0} - require.Equal(t, tc.want, viewCommandHint("run_123")) - }) - } -} - -func TestViewCommandHintCarriesServerURL(t *testing.T) { - origArgs := os.Args - origServerURL := serverURL - t.Cleanup(func() { - os.Args = origArgs - serverURL = origServerURL - }) - os.Args = []string{"lk"} - serverURL = "https://cloud-api.staging.livekit.io" - - require.Equal(t, - "lk agent simulate --view run_123 --server-url https://cloud-api.staging.livekit.io", - viewCommandHint("run_123")) -} - -func TestViewCommandHintCarriesProject(t *testing.T) { - origArgs := os.Args - origServerURL := serverURL - origProject := simulateProjectConfig - t.Cleanup(func() { - os.Args = origArgs - serverURL = origServerURL - simulateProjectConfig = origProject - }) - os.Args = []string{"lk"} - serverURL = "https://cloud-api.example.com" - simulateProjectConfig = &config.ProjectConfig{Name: "my-project"} - - require.Equal(t, - "lk agent simulate --view run_123 --project my-project"+ - " --server-url https://cloud-api.example.com", - viewCommandHint("run_123")) - - // credentials from flags or the environment resolve no project name - simulateProjectConfig = &config.ProjectConfig{} - require.Equal(t, - "lk agent simulate --view run_123 --server-url https://cloud-api.example.com", - viewCommandHint("run_123")) -} From 5937ac59cd0e9cd614b1e3beb0db62e5518fb751 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Wed, 5 Aug 2026 13:38:12 -0400 Subject: [PATCH 9/9] feat(simulate): show tool arguments whole --- cmd/lk/simulate_tui.go | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index bc506581..5e99be21 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -23,7 +23,6 @@ import ( "sort" "strings" "time" - "unicode/utf8" "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/textinput" @@ -1875,7 +1874,7 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { case *agent.ChatContext_ChatItem_FunctionCall: fc := v.FunctionCall ensureAgentBlock() - writeToolItem(&b, fmt.Sprintf("ƒ %s(%s)", fc.Name, clipToolValue(fc.Arguments)), wrapWidth) + writeToolItem(&b, fmt.Sprintf("ƒ %s(%s)", fc.Name, fc.Arguments), wrapWidth) case *agent.ChatContext_ChatItem_FunctionCallOutput: if !m.showToolOutput { continue @@ -1901,25 +1900,6 @@ func (m *simulateModel) renderChatTranscript(jobID string) string { return b.String() } -// toolPreviewLen is how much of a tool call's arguments the transcript shows — -// enough to tell two calls apart without a lookup table's worth of JSON burying -// the conversation. Tool outputs are shown whole, or not at all. -const toolPreviewLen = 80 - -// clipToolValue is a tool argument blob as the transcript carries it. -func clipToolValue(s string) string { - if len(s) <= toolPreviewLen { - return s - } - // clip on a rune boundary; arguments carry guest names, currency symbols, - // and quoted speech - clipped := s[:toolPreviewLen] - for len(clipped) > 0 && !utf8.ValidString(clipped) { - clipped = clipped[:len(clipped)-1] - } - return clipped + "..." -} - // writeToolItem appends one tool line to b, wrapped to the transcript's measure // with its continuations indented under the marker, so a long output stays // readable as a block instead of one run-on row.