Skip to content

Commit f55e4ba

Browse files
authored
fix: improve agent list responsiveness and connector UI (#324)
## Summary - keep agent listing responsive when runtime status probes stall - localize connector controls and keep provider actions usable in narrow layouts
1 parent 248dd06 commit f55e4ba

6 files changed

Lines changed: 143 additions & 3 deletions

File tree

internal/agent/service.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1829,10 +1829,19 @@ func removeAll(path string) error {
18291829
}
18301830

18311831
func (s *Service) List() []Agent {
1832+
return s.ListContext(context.Background())
1833+
}
1834+
1835+
// ListContext returns the persisted agent registry with best-effort live runtime status.
1836+
// Runtime status probes must honor ctx so API callers can remain responsive when a
1837+
// sandbox runtime is unavailable or contended.
1838+
func (s *Service) ListContext(ctx context.Context) []Agent {
1839+
if ctx == nil {
1840+
ctx = context.Background()
1841+
}
18321842
s.mu.RLock()
18331843
agents := sortedAgentsFromMap(s.agents)
18341844
s.mu.RUnlock()
1835-
ctx := context.Background()
18361845
for idx := range agents {
18371846
agents[idx] = s.withRuntimeImageMigrationStatus(ctx, s.hydrateAgentStatus(ctx, agents[idx]))
18381847
}

internal/agent/service_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5014,6 +5014,48 @@ func TestListKeepsLastKnownStatusWhenHydrationFails(t *testing.T) {
50145014
}
50155015
}
50165016

5017+
func TestListContextStopsBlockedRuntimeStatusHydration(t *testing.T) {
5018+
svc, err := NewService(
5019+
config.ModelConfig{},
5020+
config.ServerConfig{},
5021+
"manager-image:test",
5022+
"",
5023+
WithRuntime(fakeAgentRuntime{
5024+
kind: RuntimeKindPicoClawSandbox,
5025+
info: func(ctx context.Context, _ agentruntime.Handle) (agentruntime.Info, error) {
5026+
<-ctx.Done()
5027+
return agentruntime.Info{}, ctx.Err()
5028+
},
5029+
}),
5030+
)
5031+
if err != nil {
5032+
t.Fatalf("NewService() error = %v", err)
5033+
}
5034+
svc.agents["agent-alice"] = Agent{
5035+
ID: "agent-alice",
5036+
Name: "alice",
5037+
Role: RoleWorker,
5038+
RuntimeKind: RuntimeKindPicoClawSandbox,
5039+
Status: string(sandbox.StateRunning),
5040+
CreatedAt: time.Date(2026, 4, 1, 11, 0, 0, 0, time.UTC),
5041+
}
5042+
5043+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
5044+
defer cancel()
5045+
started := time.Now()
5046+
got := svc.ListContext(ctx)
5047+
5048+
if elapsed := time.Since(started); elapsed > time.Second {
5049+
t.Fatalf("ListContext() took %v after cancellation", elapsed)
5050+
}
5051+
if len(got) != 1 {
5052+
t.Fatalf("ListContext() len = %d, want 1", len(got))
5053+
}
5054+
if got[0].Status != string(sandbox.StateRunning) {
5055+
t.Fatalf("ListContext()[0].Status = %q, want running", got[0].Status)
5056+
}
5057+
}
5058+
50175059
func TestIsSandboxRuntimeContentionRecognizesBoxLiteLockErrors(t *testing.T) {
50185060
cases := []struct {
50195061
name string

internal/api/handler.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ type Handler struct {
7777
participantActivityTurns map[string]participantActivityTurn
7878
}
7979

80-
const createOperationTimeout = 10 * time.Minute
80+
const (
81+
createOperationTimeout = 10 * time.Minute
82+
agentListStatusTimeout = 2 * time.Second
83+
)
8184

8285
var sseHeartbeatInterval = 15 * time.Second
8386
var locateCodexCLI = func() (string, error) {
@@ -916,7 +919,9 @@ func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
916919
http.Error(w, err.Error(), http.StatusInternalServerError)
917920
return
918921
}
919-
writeJSON(w, http.StatusOK, h.presentAgentsForRequest(r, h.svc.List()))
922+
ctx, cancel := context.WithTimeout(r.Context(), agentListStatusTimeout)
923+
defer cancel()
924+
writeJSON(w, http.StatusOK, h.presentAgentsForRequest(r, h.svc.ListContext(ctx)))
920925
case http.MethodPost:
921926
h.handleCreateAgentWorker(w, r)
922927
default:

web/app/src/components/business/ConversationPane/ConversationPane.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@
611611

612612
.connector-provider-row {
613613
min-width: 0;
614+
flex-wrap: wrap;
614615
justify-content: space-between;
615616
gap: 12px;
616617
min-height: 54px;
@@ -620,6 +621,7 @@
620621
}
621622

622623
.connector-provider-main {
624+
flex: 1 1 120px;
623625
min-width: 0;
624626
gap: 10px;
625627
}
@@ -674,8 +676,11 @@
674676
}
675677

676678
.connector-provider-actions {
679+
max-width: 100%;
677680
display: inline-flex;
681+
flex-wrap: wrap;
678682
align-items: center;
683+
justify-content: flex-end;
679684
gap: 8px;
680685
}
681686

web/app/src/shared/i18n/messages.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ export const messages = {
9595
csghubLoginCompleted: "用户 {user} 登录完成。",
9696
csghubLoginEnvironmentCompleted: "用户 {user} 登录 {environment} 完成。",
9797
csghubLogoutCompleted: "用户 {user} 已登出。",
98+
connectorManagerTitle: "管理连接器",
99+
connectorGitHub: "GitHub",
100+
connectorConnected: "已连接",
101+
connectorConfigured: "已配置",
102+
connectorSetUp: "设置",
103+
connectorNotConnected: "未连接",
104+
connectorConnect: "连接",
105+
connectorConnectGitHub: "连接 GitHub",
106+
connectorGitHubConnectedAs: "GitHub 已连接:{user}",
107+
connectorManage: "管理",
108+
connectorDisconnect: "断开",
109+
connectorEdit: "编辑",
110+
connectorSave: "保存",
111+
connectorClientID: "Client ID",
112+
connectorClientSecret: "Client Secret",
113+
connectorCallbackURL: "Callback URL",
114+
connectorScopes: "Scopes",
115+
connectorOAuthPending: "等待 GitHub 授权完成...",
116+
connectorClientIDRequired: "请填写 GitHub Client ID。",
117+
connectorClientSecretRequired: "请填写 GitHub Client Secret。",
118+
connectorSaveFailed: "保存 GitHub 连接器失败。",
119+
connectorNotConfigured: "请先配置 GitHub OAuth Client ID 和 Client Secret。",
120+
connectorConnectFailed: "GitHub 授权启动失败。",
121+
connectorManageFailed: "打开 GitHub App 管理失败。",
122+
connectorDisconnectFailed: "断开 GitHub 连接失败。",
123+
connectorStatusFailed: "读取连接器状态失败。",
124+
connectorLoginTimedOut: "GitHub 授权超时,请重新连接。",
125+
connectorOAuthURLMissing: "GitHub 授权地址为空,请重试。",
126+
connectorOAuthPopupBlocked: "浏览器阻止了 GitHub 授权窗口,请允许弹窗后重试。",
127+
connectorManageURLMissing: "GitHub App 管理地址为空,请重试。",
128+
connectorManagePopupBlocked: "浏览器阻止了 GitHub App 管理窗口,请允许弹窗后重试。",
98129
localAgentConsole: "本地 Agent 控制台",
99130
loading: "正在加载 IM 工作区...",
100131
loadingFailed: "加载失败,请稍后重试。",
@@ -1113,6 +1144,37 @@ export const messages = {
11131144
csghubLoginCompleted: "{user} signed in.",
11141145
csghubLoginEnvironmentCompleted: "{user} signed in to {environment}.",
11151146
csghubLogoutCompleted: "Signed out: {user}.",
1147+
connectorManagerTitle: "Manage connectors",
1148+
connectorGitHub: "GitHub",
1149+
connectorConnected: "Connected",
1150+
connectorConfigured: "Configured",
1151+
connectorSetUp: "Set up",
1152+
connectorNotConnected: "Not connected",
1153+
connectorConnect: "Connect",
1154+
connectorConnectGitHub: "Connect GitHub",
1155+
connectorGitHubConnectedAs: "GitHub connected: {user}",
1156+
connectorManage: "Manage",
1157+
connectorDisconnect: "Disconnect",
1158+
connectorEdit: "Edit",
1159+
connectorSave: "Save",
1160+
connectorClientID: "Client ID",
1161+
connectorClientSecret: "Client Secret",
1162+
connectorCallbackURL: "Callback URL",
1163+
connectorScopes: "Scopes",
1164+
connectorOAuthPending: "Waiting for GitHub authorization...",
1165+
connectorClientIDRequired: "Enter a GitHub Client ID.",
1166+
connectorClientSecretRequired: "Enter a GitHub Client Secret.",
1167+
connectorSaveFailed: "Failed to save GitHub connector.",
1168+
connectorNotConfigured: "Configure the GitHub OAuth Client ID and Client Secret before connecting.",
1169+
connectorConnectFailed: "Failed to start GitHub authorization.",
1170+
connectorManageFailed: "Failed to open GitHub App management.",
1171+
connectorDisconnectFailed: "Failed to disconnect GitHub.",
1172+
connectorStatusFailed: "Failed to read connector status.",
1173+
connectorLoginTimedOut: "GitHub authorization timed out. Please connect again.",
1174+
connectorOAuthURLMissing: "The GitHub authorization URL was empty. Please retry.",
1175+
connectorOAuthPopupBlocked: "The browser blocked the GitHub authorization window. Allow pop-ups and retry.",
1176+
connectorManageURLMissing: "The GitHub App management URL was empty. Please retry.",
1177+
connectorManagePopupBlocked: "The browser blocked the GitHub App management window. Allow pop-ups and retry.",
11161178
localAgentConsole: "Local agent console",
11171179
loading: "Loading IM workspace...",
11181180
loadingFailed: "Failed to load the workspace. Please try again.",

web/app/tests/shared/i18n.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ describe("i18n messages", () => {
66
expect(createTranslator("zh")("humanDetailSubtitle")).toBe("你在聊天、提及和协作中的显示方式。");
77
});
88

9+
it("localizes connector controls instead of exposing translation keys", () => {
10+
const connectorLabels = {
11+
en: ["Manage connectors", "Connected", "Manage", "Disconnect"],
12+
zh: ["管理连接器", "已连接", "管理", "断开"],
13+
} as const;
14+
15+
for (const locale of ["en", "zh"] as const) {
16+
const t = createTranslator(locale);
17+
expect([
18+
t("connectorManagerTitle"),
19+
t("connectorConnected"),
20+
t("connectorManage"),
21+
t("connectorDisconnect"),
22+
]).toEqual(connectorLabels[locale]);
23+
}
24+
});
25+
926
it("localizes personal Hub source tags", () => {
1027
expect(localizeTemplateSourceTag("personal", "zh")).toBe("个人");
1128
expect(localizeTemplateSourceTag("personal", "en")).toBe("personal");

0 commit comments

Comments
 (0)