| author | |
| committer | |
| log | 29366ea29abf4ceb41ab250d06e190a22258bd18 |
| tree | e80db9d0e4bad419537ba4216a13ac9be85c3d3d |
| parent | 9503155da67add4fbd7fbc51c99ec19a0c8c4a2c |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>6 files changed, 374 insertions(+), 11 deletions(-)
apps/web/src/components/Icons.tsx+7| ... | ... | @@ -2,6 +2,13 @@ import React, { type SVGProps, useId } from "react"; |
| 2 | 2 | import { cn } from "~/lib/utils"; |
| 3 | 3 | export type Icon = React.FC<SVGProps<SVGSVGElement>>; |
| 4 | 4 | |
| 5 | export const UltrafastIcon: Icon = (props) => ( | |
| 6 | <svg viewBox="0 0 24 24" fill="currentColor" {...props}> | |
| 7 | <path d="m17 2-10 12h7l-1 8 10-12h-7l1-8Z" opacity="0.4" /> | |
| 8 | <path d="m11 2-10 12h7l-1 8 10-12h-7l1-8Z" /> | |
| 9 | </svg> | |
| 10 | ); | |
| 11 | ||
| 5 | 12 | export const FinderIcon: Icon = (props) => ( |
| 6 | 13 | <svg viewBox="0 0 24 24" fill="none" {...props}> |
| 7 | 14 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> |
apps/web/src/components/chat/SubagentTooltipContent.tsx+75-3| ... | ... | @@ -4,11 +4,15 @@ import type { |
| 4 | 4 | OrchestrationProjectShell, |
| 5 | 5 | ServerProvider, |
| 6 | 6 | ProviderDriverKind, |
| 7 | ProviderInstanceId, | |
| 8 | OrchestrationV2Subagent, | |
| 7 | 9 | } from "@t3tools/contracts"; |
| 8 | 10 | import { |
| 9 | 11 | resolveSubagentMetadata, |
| 10 | 12 | subagentDetailPreview, |
| 11 | 13 | } from "@t3tools/client-runtime/state/subagent-display"; |
| 14 | import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model"; | |
| 15 | import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed"; | |
| 12 | 16 | import type { ReactNode } from "react"; |
| 13 | 17 | import { |
| 14 | 18 | BotIcon, |
| ... | ... | @@ -29,13 +33,17 @@ import { deriveProviderInstanceEntries, shouldShowInstanceBadge } from "../../pr |
| 29 | 33 | export function SubagentTooltipContent(props: { |
| 30 | 34 | title: string; |
| 31 | 35 | model: string | null; |
| 36 | providerInstanceId: ProviderInstanceId; | |
| 37 | origin: OrchestrationV2Subagent["origin"]; | |
| 32 | 38 | provider?: ServerProvider | undefined; |
| 33 | 39 | /** The environment's instances; with several accounts on one provider, the card names this one. */ |
| 34 | 40 | providers?: ReadonlyArray<ServerProvider> | undefined; |
| 35 | 41 | driver?: ProviderDriverKind | undefined; |
| 36 | 42 | elapsed?: ReactNode; |
| 37 | 43 | parentThread?: Pick<OrchestrationV2ThreadShell, "projectId" | "worktreePath"> | undefined; |
| 38 | childThread?: Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath"> | undefined; | |
| 44 | childThread?: | |
| 45 | | Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath" | "modelSelection"> | |
| 46 | | undefined; | |
| 39 | 47 | parentProject?: Pick<OrchestrationProjectShell, "workspaceRoot"> | undefined; |
| 40 | 48 | childProject?: Pick<OrchestrationProjectShell, "id" | "title" | "workspaceRoot"> | undefined; |
| 41 | 49 | status: OrchestrationV2TurnItemStatus; |
| ... | ... | @@ -44,6 +52,59 @@ export function SubagentTooltipContent(props: { |
| 44 | 52 | }) { |
| 45 | 53 | const { modelLabel, workspace: metadata } = resolveSubagentMetadata(props); |
| 46 | 54 | const preview = subagentDetailPreview(props); |
| 55 | const model = props.model?.trim(); | |
| 56 | const modelSlug = props.provider | |
| 57 | ? resolveSelectableModel(props.provider.driver, model, props.provider.models) | |
| 58 | : model; | |
| 59 | const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug); | |
| 60 | const childSelection = props.childThread?.modelSelection; | |
| 61 | const childModel = props.provider | |
| 62 | ? (resolveSelectableModel( | |
| 63 | props.provider.driver, | |
| 64 | childSelection?.model, | |
| 65 | props.provider.models, | |
| 66 | ) ?? childSelection?.model.trim()) | |
| 67 | : childSelection?.model.trim(); | |
| 68 | const provider = props.provider; | |
| 69 | const matchingSelection = | |
| 70 | props.origin === "app_owned" && | |
| 71 | childModel === (modelSlug ?? model) && | |
| 72 | childSelection?.instanceId === props.providerInstanceId | |
| 73 | ? childSelection | |
| 74 | : undefined; | |
| 75 | const effort = ["reasoningEffort", "effort", "reasoning", "variant"] | |
| 76 | .map((id) => getModelSelectionStringOptionValue(matchingSelection, id)) | |
| 77 | .find(Boolean); | |
| 78 | const speed = provider | |
| 79 | ? providerModel?.capabilities?.optionDescriptors | |
| 80 | ?.map((descriptor) => { | |
| 81 | const saved = matchingSelection?.options?.find((option) => option.id === descriptor.id); | |
| 82 | if ( | |
| 83 | descriptor.id === "fastMode" && | |
| 84 | descriptor.type === "boolean" && | |
| 85 | typeof saved?.value === "boolean" | |
| 86 | ) { | |
| 87 | return getTraitsSpeedDisplay(provider.driver, { | |
| 88 | ...descriptor, | |
| 89 | currentValue: saved.value, | |
| 90 | }); | |
| 91 | } | |
| 92 | if ( | |
| 93 | provider.driver === "codex" && | |
| 94 | descriptor.id === "serviceTier" && | |
| 95 | descriptor.type === "select" && | |
| 96 | typeof saved?.value === "string" && | |
| 97 | descriptor.options.some((option) => option.id === saved.value) | |
| 98 | ) { | |
| 99 | return getTraitsSpeedDisplay(provider.driver, { | |
| 100 | ...descriptor, | |
| 101 | currentValue: saved.value, | |
| 102 | }); | |
| 103 | } | |
| 104 | return null; | |
| 105 | }) | |
| 106 | .find(Boolean) | |
| 107 | : undefined; | |
| 47 | 108 | const driver = props.provider?.driver ?? props.driver; |
| 48 | 109 | const entries = deriveProviderInstanceEntries(props.providers ?? []); |
| 49 | 110 | const entry = entries.find((candidate) => candidate.instanceId === props.provider?.instanceId); |
| ... | ... | @@ -75,8 +136,19 @@ export function SubagentTooltipContent(props: { |
| 75 | 136 | ) : ( |
| 76 | 137 | <BotIcon className="size-3 shrink-0" /> |
| 77 | 138 | )} |
| 78 | <span className="min-w-0 truncate text-foreground/75"> | |
| 79 | {showInstanceBadge ? `${modelLabel} · ${entry.displayName}` : modelLabel} | |
| 139 | <span className="inline-flex min-w-0 items-center gap-1 text-foreground/75"> | |
| 140 | <span className="min-w-0 truncate"> | |
| 141 | {showInstanceBadge ? `${modelLabel} · ${entry.displayName}` : modelLabel} | |
| 142 | </span> | |
| 143 | {effort || speed?.speedIcon ? ( | |
| 144 | <span className="inline-flex shrink-0 items-center gap-1"> | |
| 145 | {effort ? " · " : null} | |
| 146 | {speed?.speedIcon && provider ? ( | |
| 147 | <TraitsSpeedIcon provider={provider.driver} speedIcon={speed.speedIcon} size="xs" /> | |
| 148 | ) : null} | |
| 149 | {effort} | |
| 150 | </span> | |
| 151 | ) : null} | |
| 80 | 152 | </span> |
| 81 | 153 | </div> |
| 82 | 154 | <div className="flex min-w-0 items-center justify-between gap-4"> |
apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx+210-8| ... | ... | @@ -1,6 +1,12 @@ |
| 1 | 1 | import { act, cloneElement, type ReactElement, type ReactNode } from "react"; |
| 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; | |
| 3 | import { EnvironmentId, ThreadId, type OrchestrationV2ContextTransfer } from "@t3tools/contracts"; | |
| 2 | import { create, type ReactTestInstance, type ReactTestRenderer } from "react-test-renderer"; | |
| 3 | import { | |
| 4 | EnvironmentId, | |
| 5 | ThreadId, | |
| 6 | type ModelSelection, | |
| 7 | type ProviderOptionDescriptor, | |
| 8 | type OrchestrationV2ContextTransfer, | |
| 9 | } from "@t3tools/contracts"; | |
| 4 | 10 | import * as DateTime from "effect/DateTime"; |
| 5 | 11 | import { afterEach, expect, it, vi } from "vite-plus/test"; |
| 6 | 12 | |
| ... | ... | @@ -196,7 +202,11 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 196 | 202 | worktreePath: null as string | null, |
| 197 | 203 | branch: null as string | null, |
| 198 | 204 | title: "Worker", |
| 199 | modelSelection: { instanceId: "codex", model: "gpt-5.4" }, | |
| 205 | modelSelection: { | |
| 206 | instanceId: "codex", | |
| 207 | model: "gpt-5.4", | |
| 208 | options: [{ id: "reasoningEffort", value: "high" }] as ModelSelection["options"], | |
| 209 | }, | |
| 200 | 210 | lineage: { parentThreadId: "parent", relationshipToParent: "subagent" }, |
| 201 | 211 | }; |
| 202 | 212 | state.projects = [ |
| ... | ... | @@ -231,6 +241,7 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 231 | 241 | { |
| 232 | 242 | id: "agent", |
| 233 | 243 | childThreadId: "child", |
| 244 | origin: "app_owned", | |
| 234 | 245 | driver: "codex", |
| 235 | 246 | providerInstanceId: "codex", |
| 236 | 247 | title: "Worker", |
| ... | ... | @@ -253,12 +264,24 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 253 | 264 | await act(async () => { |
| 254 | 265 | renderer = create(panel); |
| 255 | 266 | }); |
| 256 | const text = () => | |
| 257 | renderer.root | |
| 258 | .findAll((node) => typeof node.type === "string") | |
| 259 | .flatMap((node) => node.children.filter((child) => typeof child === "string")) | |
| 260 | .join(""); | |
| 267 | const text = (visibleOnly = false) => { | |
| 268 | const read = (node: ReactTestInstance | string): string => { | |
| 269 | if (typeof node === "string") return node; | |
| 270 | if (visibleOnly && node.props.className === "sr-only") return ""; | |
| 271 | return node.children.map(read).join(""); | |
| 272 | }; | |
| 273 | return read(renderer.root); | |
| 274 | }; | |
| 275 | expect(text()).toContain("My GPT · high"); | |
| 276 | state.projection = { | |
| 277 | ...projection, | |
| 278 | subagents: [{ ...projection.subagents[0], origin: "provider_native" }], | |
| 279 | }; | |
| 280 | await act(async () => renderer.update(cloneElement(panel))); | |
| 261 | 281 | expect(text()).toContain("My GPT"); |
| 282 | expect(text()).not.toContain("My GPT · high"); | |
| 283 | state.projection = projection; | |
| 284 | await act(async () => renderer.update(cloneElement(panel))); | |
| 262 | 285 | expect(text()).not.toContain("Tokens"); |
| 263 | 286 | expect(text()).not.toContain("Open subagent"); |
| 264 | 287 | expect(text()).not.toContain("Project"); |
| ... | ... | @@ -279,6 +302,11 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 279 | 302 | }; |
| 280 | 303 | await act(async () => renderer.update(cloneElement(panel))); |
| 281 | 304 | expect(text()).toContain(expected); |
| 305 | if (!model?.trim() || model === "gpt-5.5" || model === "custom/model-v1") { | |
| 306 | expect(text()).not.toContain(" · high"); | |
| 307 | } else { | |
| 308 | expect(text()).toContain(`${expected} · high`); | |
| 309 | } | |
| 282 | 310 | expect(text()).not.toContain("Unknown"); |
| 283 | 311 | if (!model?.trim()) expect(text()).not.toContain("My GPT"); |
| 284 | 312 | } |
| ... | ... | @@ -302,6 +330,179 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 302 | 330 | expect(text()).not.toContain("My GPT"); |
| 303 | 331 | } |
| 304 | 332 | |
| 333 | state.projection = projection; | |
| 334 | for (const [options, expected] of [ | |
| 335 | [[{ id: "effort", value: "max" }], " · max"], | |
| 336 | [[{ id: "reasoning", value: "low" }], " · low"], | |
| 337 | [[{ id: "variant", value: "high" }], " · high"], | |
| 338 | [[{ id: "reasoningEffort", value: "none" }], " · none"], | |
| 339 | [[{ id: "reasoningEffort", value: true }], ""], | |
| 340 | [[{ id: "serviceTier", value: "fast" }], ""], | |
| 341 | [[], ""], | |
| 342 | [undefined, ""], | |
| 343 | ] as const) { | |
| 344 | child.modelSelection.options = options; | |
| 345 | state.shells = [{ environmentId: "test", source: { ...child } }]; | |
| 346 | await act(async () => renderer.update(cloneElement(panel))); | |
| 347 | expect(text()).toContain(`My GPT${expected}`); | |
| 348 | if (!expected) expect(text()).not.toContain("My GPT ·"); | |
| 349 | } | |
| 350 | const speedConfig = state.configs.get("test"); | |
| 351 | const serviceTier: ProviderOptionDescriptor = { | |
| 352 | id: "serviceTier", | |
| 353 | label: "Service Tier", | |
| 354 | type: "select", | |
| 355 | currentValue: "priority", | |
| 356 | options: [ | |
| 357 | { id: "default", label: "Standard", isDefault: true }, | |
| 358 | { id: "priority", label: "Fast" }, | |
| 359 | { id: "ultrafast", label: "Ultrafast" }, | |
| 360 | { id: "flex", label: "Flex" }, | |
| 361 | ], | |
| 362 | }; | |
| 363 | const fastMode: ProviderOptionDescriptor = { | |
| 364 | id: "fastMode", | |
| 365 | label: "Fast Mode", | |
| 366 | type: "boolean", | |
| 367 | currentValue: true, | |
| 368 | }; | |
| 369 | for (const [driver, descriptor, value, iconLabel] of [ | |
| 370 | ["codex", serviceTier, "default", ""], | |
| 371 | ["codex", serviceTier, "priority", "Fast mode on"], | |
| 372 | ["codex", serviceTier, "ultrafast", "Ultrafast mode on"], | |
| 373 | ["codex", serviceTier, "flex", ""], | |
| 374 | ["codex", serviceTier, "unknown", ""], | |
| 375 | [ | |
| 376 | "codex", | |
| 377 | { ...serviceTier, options: serviceTier.options.filter(({ id }) => id !== "ultrafast") }, | |
| 378 | "ultrafast", | |
| 379 | "", | |
| 380 | ], | |
| 381 | ["codex", serviceTier, true, ""], | |
| 382 | ["codex", serviceTier, undefined, ""], | |
| 383 | ["claudeAgent", fastMode, true, "Fast mode on"], | |
| 384 | ["claudeAgent", fastMode, false, ""], | |
| 385 | ["cursor", fastMode, true, "Fast mode on"], | |
| 386 | ["cursor", fastMode, false, ""], | |
| 387 | ["opencode", fastMode, true, "Fast mode on"], | |
| 388 | ["cursor", fastMode, "true", ""], | |
| 389 | ["cursor", serviceTier, "priority", ""], | |
| 390 | ] as const) { | |
| 391 | state.configs.set("test", { | |
| 392 | providers: [ | |
| 393 | { | |
| 394 | instanceId: "codex", | |
| 395 | driver, | |
| 396 | models: [ | |
| 397 | { | |
| 398 | slug: "gpt-5.4", | |
| 399 | name: "My GPT", | |
| 400 | capabilities: { optionDescriptors: [descriptor] }, | |
| 401 | }, | |
| 402 | ], | |
| 403 | }, | |
| 404 | ], | |
| 405 | }); | |
| 406 | child.modelSelection.options = [ | |
| 407 | { id: "reasoningEffort", value: "high" }, | |
| 408 | ...(value === undefined ? [] : [{ id: descriptor.id, value }]), | |
| 409 | ]; | |
| 410 | state.shells = [{ environmentId: "test", source: { ...child } }]; | |
| 411 | await act(async () => renderer.update(cloneElement(panel))); | |
| 412 | expect(text(true)).toContain("My GPT · high"); | |
| 413 | expect(text(true)).not.toMatch(/Fast|Ultrafast|Normal|Standard|Flex/); | |
| 414 | expect(text()).toContain(`My GPT · ${iconLabel}high`); | |
| 415 | if (!iconLabel) expect(text()).not.toContain("mode on"); | |
| 416 | child.modelSelection.options = child.modelSelection.options.filter( | |
| 417 | ({ id }) => id !== "reasoningEffort", | |
| 418 | ); | |
| 419 | state.shells = [{ environmentId: "test", source: { ...child } }]; | |
| 420 | await act(async () => renderer.update(cloneElement(panel))); | |
| 421 | expect(text()).not.toContain(" · high"); | |
| 422 | expect(text(true)).not.toMatch(/Fast|Ultrafast|Normal|Standard|Flex/); | |
| 423 | if (iconLabel) expect(text()).toContain(iconLabel); | |
| 424 | else expect(text()).not.toContain("mode on"); | |
| 425 | state.projection = { | |
| 426 | ...projection, | |
| 427 | subagents: [{ ...projection.subagents[0], origin: "provider_native" }], | |
| 428 | }; | |
| 429 | await act(async () => renderer.update(cloneElement(panel))); | |
| 430 | expect(text()).not.toMatch(/Fast|Ultrafast|Normal|Standard|Flex| · high/); | |
| 431 | state.projection = projection; | |
| 432 | for (const modelSelection of [ | |
| 433 | { ...child.modelSelection, instanceId: "other" }, | |
| 434 | { ...child.modelSelection, model: "gpt-5.5" }, | |
| 435 | ]) { | |
| 436 | state.shells = [{ environmentId: "test", source: { ...child, modelSelection } }]; | |
| 437 | await act(async () => renderer.update(cloneElement(panel))); | |
| 438 | expect(text()).not.toMatch(/Fast|Ultrafast|Normal|Standard|Flex| · high/); | |
| 439 | } | |
| 440 | } | |
| 441 | state.configs.set("test", { | |
| 442 | providers: [ | |
| 443 | { | |
| 444 | instanceId: "codex", | |
| 445 | driver: "codex", | |
| 446 | displayName: "Work account", | |
| 447 | models: [ | |
| 448 | { | |
| 449 | slug: "gpt-5.4", | |
| 450 | name: "My GPT", | |
| 451 | capabilities: { optionDescriptors: [serviceTier] }, | |
| 452 | }, | |
| 453 | ], | |
| 454 | }, | |
| 455 | { | |
| 456 | instanceId: "codex_personal", | |
| 457 | driver: "codex", | |
| 458 | displayName: "Personal account", | |
| 459 | models: [], | |
| 460 | }, | |
| 461 | ], | |
| 462 | }); | |
| 463 | child.modelSelection.options = [ | |
| 464 | { id: "reasoningEffort", value: "high" }, | |
| 465 | { id: "serviceTier", value: "priority" }, | |
| 466 | ]; | |
| 467 | state.shells = [{ environmentId: "test", source: { ...child } }]; | |
| 468 | await act(async () => renderer.update(cloneElement(panel))); | |
| 469 | expect(text(true)).toContain("My GPT · Work account · high"); | |
| 470 | expect(text()).toContain("My GPT · Work account · Fast mode onhigh"); | |
| 471 | expect(text()).not.toContain("Personal account"); | |
| 472 | state.configs.set("test", speedConfig); | |
| 473 | child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }]; | |
| 474 | state.shells = [ | |
| 475 | { | |
| 476 | environmentId: "test", | |
| 477 | source: { ...child, modelSelection: { ...child.modelSelection, instanceId: "other" } }, | |
| 478 | }, | |
| 479 | ]; | |
| 480 | await act(async () => renderer.update(cloneElement(panel))); | |
| 481 | expect(text()).not.toContain("My GPT ·"); | |
| 482 | const config = state.configs.get("test"); | |
| 483 | state.configs.clear(); | |
| 484 | await act(async () => renderer.update(cloneElement(panel))); | |
| 485 | expect(text()).toContain("GPT-5.4"); | |
| 486 | expect(text()).not.toContain("GPT-5.4 ·"); | |
| 487 | state.shells = [{ environmentId: "test", source: { ...child } }]; | |
| 488 | await act(async () => renderer.update(cloneElement(panel))); | |
| 489 | expect(text()).toContain("GPT-5.4 · high"); | |
| 490 | state.configs.set("test", config); | |
| 491 | for (const model of ["custom/model-v1", "custom/model-v2"]) { | |
| 492 | state.projection = { | |
| 493 | ...projection, | |
| 494 | subagents: [{ ...projection.subagents[0], model }], | |
| 495 | }; | |
| 496 | state.shells = [ | |
| 497 | { | |
| 498 | environmentId: "test", | |
| 499 | source: { ...child, modelSelection: { ...child.modelSelection, model: "custom/model-v1" } }, | |
| 500 | }, | |
| 501 | ]; | |
| 502 | await act(async () => renderer.update(cloneElement(panel))); | |
| 503 | expect(text().includes(`${model} · high`)).toBe(model === "custom/model-v1"); | |
| 504 | } | |
| 505 | ||
| 305 | 506 | state.projection = { |
| 306 | 507 | ...projection, |
| 307 | 508 | subagents: [ |
| ... | ... | @@ -364,6 +565,7 @@ it("shows readable models and only differing workspace details in agent tooltips |
| 364 | 565 | state.configs.clear(); |
| 365 | 566 | await act(async () => renderer.update(cloneElement(panel))); |
| 366 | 567 | expect(text()).toContain("GPT-5.4"); |
| 568 | expect(text()).not.toContain("GPT-5.4 ·"); | |
| 367 | 569 | expect(text()).not.toContain("gpt-5.4"); |
| 368 | 570 | expect(text()).not.toContain("Project"); |
| 369 | 571 | expect(text()).not.toContain("Workspace"); |
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+3| ... | ... | @@ -208,6 +208,7 @@ export function ThreadRelationshipsPanel(props: { |
| 208 | 208 | ...projectedSubagentsToRuntime([subagent])[0]!, |
| 209 | 209 | driver: subagent.driver, |
| 210 | 210 | providerInstanceId: subagent.providerInstanceId, |
| 211 | origin: subagent.origin, | |
| 211 | 212 | }, |
| 212 | 213 | ]), |
| 213 | 214 | ), |
| ... | ... | @@ -383,6 +384,8 @@ export function ThreadRelationshipsPanel(props: { |
| 383 | 384 | <SubagentTooltipContent |
| 384 | 385 | title={threadTitle} |
| 385 | 386 | model={agent.model} |
| 387 | providerInstanceId={agent.providerInstanceId} | |
| 388 | origin={agent.origin} | |
| 386 | 389 | provider={provider} |
| 387 | 390 | providers={providers} |
| 388 | 391 | driver={providerDriver} |
apps/web/src/components/chat/TraitsSpeed.tsx created+70| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | import type { ProviderDriverKind, ProviderOptionDescriptor } from "@t3tools/contracts"; | |
| 2 | import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; | |
| 3 | import { ZapIcon } from "lucide-react"; | |
| 4 | import { UltrafastIcon } from "../Icons"; | |
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | import { ComposerControlIcon, type ComposerControlSize } from "./ComposerControl"; | |
| 7 | ||
| 8 | export function getTraitsSpeedDisplay( | |
| 9 | provider: ProviderDriverKind, | |
| 10 | descriptor: ProviderOptionDescriptor, | |
| 11 | ): { label: string; speedIcon: "fast" | "ultrafast" | null } | null { | |
| 12 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { | |
| 13 | return { | |
| 14 | label: descriptor.currentValue === true ? "Fast" : "Normal", | |
| 15 | speedIcon: descriptor.currentValue === true ? "fast" : null, | |
| 16 | }; | |
| 17 | } | |
| 18 | if (provider !== "codex" || descriptor.id !== "serviceTier" || descriptor.type !== "select") { | |
| 19 | return null; | |
| 20 | } | |
| 21 | const currentValue = getProviderOptionCurrentValue(descriptor); | |
| 22 | const fastTier = descriptor.options.find(({ label }) => label === "Fast"); | |
| 23 | const ultrafastTier = descriptor.options.find(({ label }) => label === "Ultrafast"); | |
| 24 | if ( | |
| 25 | ((fastTier || ultrafastTier) && currentValue === "default") || | |
| 26 | (fastTier && currentValue === fastTier.id) || | |
| 27 | (ultrafastTier && currentValue === ultrafastTier.id) | |
| 28 | ) { | |
| 29 | return { | |
| 30 | label: descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal", | |
| 31 | speedIcon: | |
| 32 | ultrafastTier && currentValue === ultrafastTier.id | |
| 33 | ? "ultrafast" | |
| 34 | : fastTier && currentValue === fastTier.id | |
| 35 | ? "fast" | |
| 36 | : null, | |
| 37 | }; | |
| 38 | } | |
| 39 | return null; | |
| 40 | } | |
| 41 | ||
| 42 | export function TraitsSpeedIcon({ | |
| 43 | provider, | |
| 44 | speedIcon, | |
| 45 | size = "sm", | |
| 46 | }: { | |
| 47 | provider: ProviderDriverKind; | |
| 48 | speedIcon: "fast" | "ultrafast"; | |
| 49 | size?: ComposerControlSize; | |
| 50 | }) { | |
| 51 | return ( | |
| 52 | <> | |
| 53 | <ComposerControlIcon | |
| 54 | icon={speedIcon === "ultrafast" ? UltrafastIcon : ZapIcon} | |
| 55 | size={size} | |
| 56 | className={cn( | |
| 57 | "fill-current opacity-80", | |
| 58 | size === "xs" | |
| 59 | ? "text-current" | |
| 60 | : provider === "claudeAgent" | |
| 61 | ? "text-[#d97757]" | |
| 62 | : "text-foreground", | |
| 63 | )} | |
| 64 | /> | |
| 65 | <span className="sr-only"> | |
| 66 | {speedIcon === "ultrafast" ? "Ultrafast mode on" : "Fast mode on"} | |
| 67 | </span> | |
| 68 | </> | |
| 69 | ); | |
| 70 | } |
apps/web/src/components/chat/V2LifecycleRow.tsx+9| ... | ... | @@ -17,6 +17,7 @@ import { |
| 17 | 17 | ProviderDriverKind, |
| 18 | 18 | type OrchestrationV2Notification, |
| 19 | 19 | type OrchestrationV2TurnItem, |
| 20 | type OrchestrationV2Subagent, | |
| 20 | 21 | type ProviderInstanceId, |
| 21 | 22 | type ServerProvider, |
| 22 | 23 | type ThreadId, |
| ... | ... | @@ -212,6 +213,8 @@ export function V2LifecycleRow(props: { |
| 212 | 213 | <SubagentTimelineLink |
| 213 | 214 | parentRef={scopeThreadRef(props.environmentId, item.threadId)} |
| 214 | 215 | subagentId={item.subagentId} |
| 216 | providerInstanceId={item.providerInstanceId} | |
| 217 | origin={item.origin} | |
| 215 | 218 | status={item.status} |
| 216 | 219 | driver={item.driver} |
| 217 | 220 | provider={props.providerStatuses.find( |
| ... | ... | @@ -370,6 +373,8 @@ export function SubagentNotificationLink(props: { |
| 370 | 373 | <SubagentTimelineLink |
| 371 | 374 | parentRef={props.parentRef} |
| 372 | 375 | subagentId={agent.id} |
| 376 | providerInstanceId={agent.providerInstanceId} | |
| 377 | origin={agent.origin} | |
| 373 | 378 | status={agent.status} |
| 374 | 379 | driver={agent.driver} |
| 375 | 380 | provider={props.providerStatuses.find( |
| ... | ... | @@ -395,6 +400,8 @@ export function SubagentNotificationLink(props: { |
| 395 | 400 | function SubagentTimelineLink(props: { |
| 396 | 401 | readonly parentRef: ScopedThreadRef; |
| 397 | 402 | readonly subagentId: NodeId; |
| 403 | readonly providerInstanceId: ProviderInstanceId; | |
| 404 | readonly origin: OrchestrationV2Subagent["origin"]; | |
| 398 | 405 | readonly driver: ProviderDriverKind; |
| 399 | 406 | readonly provider: ServerProvider | undefined; |
| 400 | 407 | readonly providers: ReadonlyArray<ServerProvider>; |
| ... | ... | @@ -540,6 +547,8 @@ function SubagentTimelineTooltip( |
| 540 | 547 | <SubagentTooltipContent |
| 541 | 548 | title={formatSubagentDisplayTitle(child?.title ?? props.title)} |
| 542 | 549 | model={props.model} |
| 550 | providerInstanceId={props.providerInstanceId} | |
| 551 | origin={props.origin} | |
| 543 | 552 | provider={props.provider} |
| 544 | 553 | providers={props.providers} |
| 545 | 554 | driver={props.driver} |