kcg-code 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daftar opsi agent (mode) opencode — dipakai di dalam `PopoverContent`
|
|
3
|
+
* (`AgentPicker`, desktop) maupun langsung di `SheetContent` (mobile, tanpa
|
|
4
|
+
* popover bersarang). Setiap baris menampilkan ikon + nama + deskripsi
|
|
5
|
+
* singkat, supaya jelas ini adalah *pemilihan mode*, bukan tombol biasa.
|
|
6
|
+
*
|
|
7
|
+
* Kedua varian: nama dikapitalisasi, ikon dalam kotak, deskripsi 1 baris,
|
|
8
|
+
* dan agent pembantu (tanpa deskripsi / mode `subagent`, mis. compaction /
|
|
9
|
+
* title / summary) dilipat ke "More modes" agar pilihan utama tidak tenggelam.
|
|
10
|
+
* - `popover` (desktop): baris ringkas, centang di kanan.
|
|
11
|
+
* - `sheet` (mobile): baris lebih tinggi untuk jempol, indikator radio.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
CheckIcon,
|
|
15
|
+
ChevronDownIcon,
|
|
16
|
+
CompassIcon,
|
|
17
|
+
HammerIcon,
|
|
18
|
+
ListTreeIcon,
|
|
19
|
+
type LucideIcon,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
import type { AgentOption } from "@/server/services/opencode-client";
|
|
25
|
+
|
|
26
|
+
/** Ikon per agent: khusus dulu (build/plan), sisanya fallback per mode. */
|
|
27
|
+
function agentIcon(name: string, mode: AgentOption["mode"]) {
|
|
28
|
+
const n = name.toLowerCase();
|
|
29
|
+
if (n === "build") return HammerIcon;
|
|
30
|
+
if (n === "plan") return ListTreeIcon;
|
|
31
|
+
return mode === "primary" ? CompassIcon : ListTreeIcon;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Agent utama = primary/all yang punya deskripsi; sisanya pembantu. */
|
|
35
|
+
function isMainAgent(a: AgentOption): boolean {
|
|
36
|
+
return a.mode !== "subagent" && Boolean(a.description);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function titleCase(name: string): string {
|
|
40
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AgentModeListProps {
|
|
44
|
+
agents: AgentOption[] | null;
|
|
45
|
+
loading: boolean;
|
|
46
|
+
error: string | null;
|
|
47
|
+
/** Agent aktif saat ini; null = default opencode (build). */
|
|
48
|
+
activeAgent: string | null;
|
|
49
|
+
saving?: boolean;
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
onPick: (name: string | null) => void;
|
|
52
|
+
className?: string;
|
|
53
|
+
variant?: "popover" | "sheet";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function AgentModeList({
|
|
57
|
+
agents,
|
|
58
|
+
loading,
|
|
59
|
+
error,
|
|
60
|
+
activeAgent,
|
|
61
|
+
saving = false,
|
|
62
|
+
disabled = false,
|
|
63
|
+
onPick,
|
|
64
|
+
className,
|
|
65
|
+
variant = "popover",
|
|
66
|
+
}: AgentModeListProps) {
|
|
67
|
+
const sheet = variant === "sheet";
|
|
68
|
+
const all = agents ?? [];
|
|
69
|
+
const main = all.filter(isMainAgent);
|
|
70
|
+
const extra = all.filter((a) => !isMainAgent(a));
|
|
71
|
+
const [expanded, setExpanded] = useState(false);
|
|
72
|
+
// Agent aktif ada di "More modes" -> selalu terbuka agar pilihan terlihat.
|
|
73
|
+
const activeInExtra = extra.some((a) => a.name === activeAgent);
|
|
74
|
+
const showExtra = expanded || activeInExtra;
|
|
75
|
+
|
|
76
|
+
if (loading) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground">
|
|
79
|
+
<Spinner className="size-3.5" />
|
|
80
|
+
Loading agents…
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
return <div className="px-3 py-2.5 text-sm text-destructive">{error}</div>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const row = (key: string, opts: RowProps) => (
|
|
90
|
+
<ModeRow key={key} {...opts} sheet={sheet} disabled={disabled || saving} />
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={cn("flex flex-col gap-0.5", className)}>
|
|
95
|
+
{row("__default", {
|
|
96
|
+
icon: CompassIcon,
|
|
97
|
+
label: "Default",
|
|
98
|
+
description: "Let KCG Code choose (usually Build)",
|
|
99
|
+
selected: activeAgent === null,
|
|
100
|
+
onClick: () => onPick(null),
|
|
101
|
+
})}
|
|
102
|
+
{main.map((a) =>
|
|
103
|
+
row(a.name, {
|
|
104
|
+
icon: agentIcon(a.name, a.mode),
|
|
105
|
+
label: titleCase(a.name),
|
|
106
|
+
description: a.description,
|
|
107
|
+
selected: a.name === activeAgent,
|
|
108
|
+
accent: a.name.toLowerCase() === "plan",
|
|
109
|
+
onClick: () => onPick(a.name),
|
|
110
|
+
}),
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{extra.length > 0 && (
|
|
114
|
+
<>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => !activeInExtra && setExpanded((v) => !v)}
|
|
118
|
+
aria-expanded={showExtra}
|
|
119
|
+
className="mt-1 flex w-full items-center gap-1.5 rounded-lg px-3 py-2.5 text-left text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
120
|
+
>
|
|
121
|
+
<ChevronDownIcon
|
|
122
|
+
className={cn("size-3.5 transition-transform", !showExtra && "-rotate-90")}
|
|
123
|
+
aria-hidden
|
|
124
|
+
/>
|
|
125
|
+
More modes
|
|
126
|
+
<span className="tabular-nums opacity-70">{extra.length}</span>
|
|
127
|
+
</button>
|
|
128
|
+
{showExtra &&
|
|
129
|
+
extra.map((a) =>
|
|
130
|
+
row(a.name, {
|
|
131
|
+
icon: agentIcon(a.name, a.mode),
|
|
132
|
+
label: titleCase(a.name),
|
|
133
|
+
description: a.description,
|
|
134
|
+
selected: a.name === activeAgent,
|
|
135
|
+
compact: true,
|
|
136
|
+
onClick: () => onPick(a.name),
|
|
137
|
+
}),
|
|
138
|
+
)}
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{all.length === 0 && (
|
|
143
|
+
<div className="px-3 py-2.5 text-sm text-muted-foreground">No agents found.</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface RowProps {
|
|
150
|
+
icon: LucideIcon;
|
|
151
|
+
label: string;
|
|
152
|
+
description: string | null;
|
|
153
|
+
selected: boolean;
|
|
154
|
+
accent?: boolean;
|
|
155
|
+
/** Baris pembantu di "More modes": lebih pendek. */
|
|
156
|
+
compact?: boolean;
|
|
157
|
+
onClick: () => void;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ModeRow({
|
|
161
|
+
icon: Icon,
|
|
162
|
+
label,
|
|
163
|
+
description,
|
|
164
|
+
selected,
|
|
165
|
+
accent,
|
|
166
|
+
compact,
|
|
167
|
+
onClick,
|
|
168
|
+
sheet,
|
|
169
|
+
disabled,
|
|
170
|
+
}: RowProps & { sheet: boolean; disabled: boolean }) {
|
|
171
|
+
if (!sheet) {
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
aria-pressed={selected}
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
onClick={onClick}
|
|
178
|
+
className={cn(
|
|
179
|
+
"group/row flex w-full items-center gap-2.5 rounded-lg px-2 text-left transition-colors outline-none",
|
|
180
|
+
"hover:bg-accent focus-visible:bg-accent disabled:opacity-50",
|
|
181
|
+
compact ? "min-h-9 py-1.5" : "min-h-12 py-2",
|
|
182
|
+
selected && "bg-accent/60",
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<span
|
|
186
|
+
className={cn(
|
|
187
|
+
"flex shrink-0 items-center justify-center rounded-md",
|
|
188
|
+
compact ? "size-6" : "size-8",
|
|
189
|
+
selected
|
|
190
|
+
? "bg-primary/15 text-primary dark:bg-primary/25"
|
|
191
|
+
: accent
|
|
192
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
193
|
+
: "bg-muted text-muted-foreground group-hover/row:bg-background/60",
|
|
194
|
+
)}
|
|
195
|
+
aria-hidden
|
|
196
|
+
>
|
|
197
|
+
<Icon className={compact ? "size-3.5" : "size-4"} />
|
|
198
|
+
</span>
|
|
199
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
200
|
+
<span className={cn("truncate text-sm leading-5", selected && "font-medium")}>
|
|
201
|
+
{label}
|
|
202
|
+
</span>
|
|
203
|
+
{description && !compact && (
|
|
204
|
+
<span className="truncate text-xs text-muted-foreground" title={description}>
|
|
205
|
+
{description}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</span>
|
|
209
|
+
<CheckIcon
|
|
210
|
+
className={cn("size-4 shrink-0 text-primary", !selected && "invisible")}
|
|
211
|
+
aria-hidden
|
|
212
|
+
/>
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
aria-pressed={selected}
|
|
221
|
+
disabled={disabled}
|
|
222
|
+
onClick={onClick}
|
|
223
|
+
className={cn(
|
|
224
|
+
"flex w-full items-center gap-3 rounded-xl px-3 text-left transition-colors outline-none",
|
|
225
|
+
"active:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50",
|
|
226
|
+
compact ? "min-h-11 py-2" : "min-h-14 py-2.5",
|
|
227
|
+
selected ? "bg-primary/10 dark:bg-primary/15" : "hover:bg-muted/60",
|
|
228
|
+
)}
|
|
229
|
+
>
|
|
230
|
+
<span
|
|
231
|
+
className={cn(
|
|
232
|
+
"flex shrink-0 items-center justify-center rounded-lg",
|
|
233
|
+
compact ? "size-7" : "size-9",
|
|
234
|
+
selected
|
|
235
|
+
? "bg-primary/15 text-primary dark:bg-primary/25"
|
|
236
|
+
: accent
|
|
237
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
238
|
+
: "bg-muted text-muted-foreground",
|
|
239
|
+
)}
|
|
240
|
+
aria-hidden
|
|
241
|
+
>
|
|
242
|
+
<Icon className={compact ? "size-3.5" : "size-[18px]"} />
|
|
243
|
+
</span>
|
|
244
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
245
|
+
<span className={cn("text-[15px] leading-5", selected && "font-medium")}>{label}</span>
|
|
246
|
+
{description && !compact && (
|
|
247
|
+
<span className="line-clamp-1 text-[13px] leading-snug text-muted-foreground">
|
|
248
|
+
{description}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
</span>
|
|
252
|
+
<span
|
|
253
|
+
className={cn(
|
|
254
|
+
"flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
|
|
255
|
+
selected
|
|
256
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
257
|
+
: "border-muted-foreground/40",
|
|
258
|
+
)}
|
|
259
|
+
aria-hidden
|
|
260
|
+
>
|
|
261
|
+
{selected && <CheckIcon className="size-3" strokeWidth={3} />}
|
|
262
|
+
</span>
|
|
263
|
+
</button>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pemilih agent (mode) opencode untuk composer Session — padanan tombol
|
|
3
|
+
* ganti mode build/plan di TUI opencode (Tab).
|
|
4
|
+
*
|
|
5
|
+
* - Trigger: tombol kecil di toolbar BAWAH input prompt (ala aplikasi chat);
|
|
6
|
+
* menampilkan nama agent aktif + ikon. `plan` ditandai khusus (ikon +
|
|
7
|
+
* warna berbeda) supaya user sadar sedang "mode aman".
|
|
8
|
+
* - Daftar agent dari `GET /api/projects/:id/agents` (dimuat saat popover
|
|
9
|
+
* pertama dibuka — server headless tidak di-spawn kalau tidak dipakai).
|
|
10
|
+
* - Perubahan via `PUT /api/sessions/:id` (`agent`), berlaku di prompt
|
|
11
|
+
* berikutnya tanpa restart. Opsi "Default" mengirim `agent: null`.
|
|
12
|
+
*
|
|
13
|
+
* Fetch + pick logic ada di `useAgentPicker` dan daftar opsi di
|
|
14
|
+
* `AgentModeList` — keduanya dipakai ulang langsung (tanpa popover) di Sheet
|
|
15
|
+
* aksi mobile (`SessionView`).
|
|
16
|
+
*/
|
|
17
|
+
import { ChevronDownIcon, CompassIcon, HammerIcon, ListTreeIcon } from "lucide-react";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { AgentModeList } from "@/components/sessions/AgentModeList";
|
|
20
|
+
import { Button } from "@/components/ui/button";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
22
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
23
|
+
import { useAgentPicker } from "@/hooks/useAgentPicker";
|
|
24
|
+
import { cn } from "@/lib/utils";
|
|
25
|
+
import type { AgentType } from "@/types";
|
|
26
|
+
|
|
27
|
+
export interface AgentPickerProps {
|
|
28
|
+
projectId: string;
|
|
29
|
+
/** Backend Session — Claude Code menawarkan mode permission. */
|
|
30
|
+
agentType?: AgentType;
|
|
31
|
+
/** null = Session belum ada (composer homepage): pilihan tidak di-PUT. */
|
|
32
|
+
sessionId: string | null;
|
|
33
|
+
/** Agent aktif saat ini; null = default opencode (build). */
|
|
34
|
+
agent: string | null;
|
|
35
|
+
/** Dipanggil setelah server menerima perubahan agent. */
|
|
36
|
+
onChanged: (agent: string | null) => void;
|
|
37
|
+
/** Nonaktifkan picker (mis. Session tidak running / WS sibuk). */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Override tampilan trigger, mis. `w-full justify-start` saat ditaruh di dalam Sheet aksi mobile. */
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function AgentPicker({
|
|
44
|
+
projectId,
|
|
45
|
+
agentType = "opencode",
|
|
46
|
+
sessionId,
|
|
47
|
+
agent,
|
|
48
|
+
onChanged,
|
|
49
|
+
disabled = false,
|
|
50
|
+
className,
|
|
51
|
+
}: AgentPickerProps) {
|
|
52
|
+
const [open, setOpen] = useState(false);
|
|
53
|
+
const { agents, loading, saving, error, load, pick } = useAgentPicker(
|
|
54
|
+
projectId,
|
|
55
|
+
sessionId,
|
|
56
|
+
(name) => {
|
|
57
|
+
onChanged(name);
|
|
58
|
+
setOpen(false);
|
|
59
|
+
},
|
|
60
|
+
agentType,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const isPlan = agent?.toLowerCase() === "plan";
|
|
64
|
+
const label = agent ? agent.charAt(0).toUpperCase() + agent.slice(1) : "Default";
|
|
65
|
+
const ActiveIcon = isPlan
|
|
66
|
+
? ListTreeIcon
|
|
67
|
+
: agent?.toLowerCase() === "build"
|
|
68
|
+
? HammerIcon
|
|
69
|
+
: CompassIcon;
|
|
70
|
+
|
|
71
|
+
// Daftar dimuat saat popover pertama dibuka (lazy, ala ModelPicker).
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (open) load();
|
|
74
|
+
}, [open, load]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Popover open={open} onOpenChange={(o) => setOpen(o)}>
|
|
78
|
+
<PopoverTrigger asChild>
|
|
79
|
+
<Button
|
|
80
|
+
type="button"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="sm"
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
aria-label={`Agent mode: ${label}`}
|
|
85
|
+
title="Change mode"
|
|
86
|
+
className={cn(
|
|
87
|
+
"group h-8 shrink-0 gap-1.5 rounded-full border border-transparent px-2.5 text-[13px] font-medium text-muted-foreground",
|
|
88
|
+
"hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground",
|
|
89
|
+
isPlan &&
|
|
90
|
+
"border-amber-500/30 bg-amber-500/10 text-amber-700 hover:bg-amber-500/15 hover:text-amber-700 data-[state=open]:bg-amber-500/15 data-[state=open]:text-amber-700 dark:text-amber-400 dark:hover:text-amber-400 dark:data-[state=open]:text-amber-400",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{saving ? <Spinner className="size-4" /> : <ActiveIcon className="size-3.5" />}
|
|
95
|
+
<span className="inline max-w-24 truncate sm:max-w-32">{label}</span>
|
|
96
|
+
<ChevronDownIcon
|
|
97
|
+
className="size-3.5 opacity-60 transition-transform group-data-[state=open]:rotate-180"
|
|
98
|
+
aria-hidden
|
|
99
|
+
/>
|
|
100
|
+
</Button>
|
|
101
|
+
</PopoverTrigger>
|
|
102
|
+
<PopoverContent
|
|
103
|
+
align="end"
|
|
104
|
+
side="top"
|
|
105
|
+
sideOffset={8}
|
|
106
|
+
collisionPadding={12}
|
|
107
|
+
// Tinggi SELURUH popover (header + daftar) dibatasi ruang yang
|
|
108
|
+
// tersedia (Radix). Bila hanya daftar yang dibatasi, header membuat
|
|
109
|
+
// popover selalu sedikit melebihi ruang -> floating-ui membalik sisi
|
|
110
|
+
// atas/bawah, tinggi daftar ikut berubah, dan ResizeObserver berputar
|
|
111
|
+
// ("ResizeObserver loop completed…") saat "More modes" dibuka.
|
|
112
|
+
className="flex max-h-[min(26rem,var(--radix-popover-content-available-height))] w-80 flex-col overflow-hidden p-0"
|
|
113
|
+
>
|
|
114
|
+
<div className="shrink-0 border-b px-3 pt-2.5 pb-2">
|
|
115
|
+
<p className="text-sm font-medium">Mode</p>
|
|
116
|
+
<p className="text-xs text-muted-foreground">How the agent handles your next message</p>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:thin]">
|
|
119
|
+
<AgentModeList
|
|
120
|
+
agents={agents}
|
|
121
|
+
loading={loading}
|
|
122
|
+
error={error}
|
|
123
|
+
activeAgent={agent}
|
|
124
|
+
saving={saving}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
onPick={(name) => void pick(name)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</PopoverContent>
|
|
130
|
+
</Popover>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render satu turn assistant (Satu kesatuan respon model).
|
|
3
|
+
*
|
|
4
|
+
* Isi: satu atau lebih blok "Thought process" collapsible (kluster steps
|
|
5
|
+
* reasoning/tool per round — `ThoughtProcessBlock.tsx`), diikuti jawaban
|
|
6
|
+
* teks / error, lalu footer waktu (+ durasi thinking) setelah turn selesai.
|
|
7
|
+
*/
|
|
8
|
+
import { CircleAlertIcon } from "lucide-react";
|
|
9
|
+
import { MarkdownContent } from "@/components/sessions/MarkdownContent";
|
|
10
|
+
import { CopyMessageButton } from "@/components/sessions/MessageActions";
|
|
11
|
+
import { Bubble, BubbleContent } from "@/components/ui/bubble";
|
|
12
|
+
import { Message, MessageContent, MessageFooter } from "@/components/ui/message";
|
|
13
|
+
import { useSmoothText } from "@/hooks/useSmoothText";
|
|
14
|
+
import type { CollapsibleState } from "@/lib/collapsible";
|
|
15
|
+
import { formatDuration, formatTime } from "@/lib/time";
|
|
16
|
+
import { partText, turnBlocks, turnSegments, turnThoughtDuration } from "@/lib/turns";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
import type { AssistantTurnGroup } from "@/types";
|
|
19
|
+
import { ThoughtProcessBlock } from "./ThoughtProcessBlock";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Markdown jawaban yang sedang di-stream: teks dikejar halus per frame
|
|
23
|
+
* (`useSmoothText`) agar tidak melompat per potongan delta. Hanya blok
|
|
24
|
+
* terakhir dari turn yang masih berjalan yang dianimasikan; riwayat & blok
|
|
25
|
+
* yang sudah lewat langsung tampil penuh.
|
|
26
|
+
*/
|
|
27
|
+
function StreamingMarkdown({ text, streaming }: { text: string; streaming: boolean }) {
|
|
28
|
+
const shown = useSmoothText(text, streaming);
|
|
29
|
+
return <MarkdownContent>{shown}</MarkdownContent>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Model kadang membalas "(empty)" saat tidak ada teks untuk ditampilkan. */
|
|
33
|
+
function isEmptyReply(text: string): boolean {
|
|
34
|
+
return text.trim().toLowerCase() === "(empty)";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AssistantTurnProps {
|
|
38
|
+
/** Pesan assistant berurutan dari satu turn balasan model. */
|
|
39
|
+
group: AssistantTurnGroup;
|
|
40
|
+
/** Status toggle blok "Thought process" (key `group.id:blockKey`). */
|
|
41
|
+
collapsible: CollapsibleState;
|
|
42
|
+
/** Toggle satu blok Thought process. */
|
|
43
|
+
onToggle: (key: string) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function AssistantTurn({ group, collapsible, onToggle }: AssistantTurnProps) {
|
|
47
|
+
const m = group.messages[0];
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
const segments = turnSegments(group.messages);
|
|
50
|
+
const streaming = m.streaming === true || group.messages.some((msg) => msg.streaming === true);
|
|
51
|
+
const blocks = turnBlocks(segments);
|
|
52
|
+
// Durasi thinking untuk footer — hanya dihitung setelah turn selesai.
|
|
53
|
+
const thoughtMs = !streaming ? turnThoughtDuration(group.messages) : null;
|
|
54
|
+
// Teks jawaban (tanpa thinking/tool) untuk tombol Copy, dipisah paragraf.
|
|
55
|
+
const answerText = blocks
|
|
56
|
+
.map((b) =>
|
|
57
|
+
b.content?.kind === "text" && !isEmptyReply(b.content.text) ? b.content.text.trim() : "",
|
|
58
|
+
)
|
|
59
|
+
.filter((t) => t !== "")
|
|
60
|
+
.join("\n\n");
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Message align="start">
|
|
64
|
+
<MessageContent>
|
|
65
|
+
{blocks.map((block, bi) => {
|
|
66
|
+
const isLastBlock = bi === blocks.length - 1;
|
|
67
|
+
const hasSteps = block.steps.length > 0;
|
|
68
|
+
return (
|
|
69
|
+
<div key={block.key}>
|
|
70
|
+
{/* Thought process block — pisah per round thinking */}
|
|
71
|
+
<ThoughtProcessBlock
|
|
72
|
+
groupId={group.id}
|
|
73
|
+
blockKey={block.key}
|
|
74
|
+
steps={block.steps}
|
|
75
|
+
collapsible={collapsible}
|
|
76
|
+
onToggle={onToggle}
|
|
77
|
+
streaming={streaming}
|
|
78
|
+
isLastBlock={isLastBlock}
|
|
79
|
+
/>
|
|
80
|
+
{/* Konten teks/error setelah thinking round ini */}
|
|
81
|
+
{block.content?.kind === "error" && (
|
|
82
|
+
<div className={cn(hasSteps && "mt-3")}>
|
|
83
|
+
<Bubble variant="destructive">
|
|
84
|
+
<BubbleContent>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<CircleAlertIcon
|
|
87
|
+
className="mt-0.5 size-4 shrink-0"
|
|
88
|
+
data-icon="inline-start"
|
|
89
|
+
/>
|
|
90
|
+
<span className="whitespace-pre-wrap">
|
|
91
|
+
{partText(block.content.part) ??
|
|
92
|
+
"Something went wrong while processing the prompt."}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
</BubbleContent>
|
|
96
|
+
</Bubble>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
{/* Balasan kosong dari model ("(empty)") — tetap tampil sebagai
|
|
100
|
+
penanda, tapi kecil & redup agar tidak terbaca sebagai jawaban. */}
|
|
101
|
+
{block.content?.kind === "text" && isEmptyReply(block.content.text) && (
|
|
102
|
+
<p className={cn("text-xs text-muted-foreground/70 italic", hasSteps && "mt-2")}>
|
|
103
|
+
{block.content.text.trim()}
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
{block.content?.kind === "text" &&
|
|
107
|
+
block.content.text !== "" &&
|
|
108
|
+
!isEmptyReply(block.content.text) && (
|
|
109
|
+
<div className={cn(hasSteps && "mt-3")}>
|
|
110
|
+
<Bubble variant="ghost" className="max-w-full">
|
|
111
|
+
<BubbleContent className="w-full">
|
|
112
|
+
<StreamingMarkdown
|
|
113
|
+
text={block.content.text}
|
|
114
|
+
streaming={streaming && isLastBlock}
|
|
115
|
+
/>
|
|
116
|
+
</BubbleContent>
|
|
117
|
+
</Bubble>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
{/* Footer: tersembunyi saat streaming, muncul dengan animasi fade +
|
|
124
|
+
slide dari kanan setelah turn selesai. Format:
|
|
125
|
+
HH:MM:SS · ● · thoughts Xs */}
|
|
126
|
+
{!streaming && (
|
|
127
|
+
<MessageFooter className="gap-1 animate-in fade-in-0 slide-in-from-right-4 duration-500">
|
|
128
|
+
<span className="text-muted-foreground/80">
|
|
129
|
+
{formatTime(m.createdAt)}
|
|
130
|
+
{thoughtMs !== null && (
|
|
131
|
+
<>
|
|
132
|
+
<span className="mx-1.5 text-muted-foreground/40" aria-hidden>
|
|
133
|
+
●
|
|
134
|
+
</span>
|
|
135
|
+
<span>thoughts {formatDuration(thoughtMs)}</span>
|
|
136
|
+
</>
|
|
137
|
+
)}
|
|
138
|
+
</span>
|
|
139
|
+
<CopyMessageButton text={answerText} className="ml-1" />
|
|
140
|
+
</MessageFooter>
|
|
141
|
+
)}
|
|
142
|
+
</MessageContent>
|
|
143
|
+
</Message>
|
|
144
|
+
);
|
|
145
|
+
}
|