fix(agent): improve chat message rendering (#56)
Refactor ChatMessage to use AI SDK type guards and render parts in natural order. Collapse reasoning by default so thinking tokens don't flood the screen. Cap reasoning content height during streaming. Add tool workflow guidance to system prompt. Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
This commit is contained in:
parent
b24f94e570
commit
a25c8a26bc
@ -103,6 +103,7 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
...githubTools,
|
...githubTools,
|
||||||
...pluginTools,
|
...pluginTools,
|
||||||
},
|
},
|
||||||
|
toolChoice: "auto",
|
||||||
stopWhen: stepCountIs(10),
|
stopWhen: stepCountIs(10),
|
||||||
onError({ error }) {
|
onError({ error }) {
|
||||||
const apiErr = unwrapAPICallError(error)
|
const apiErr = unwrapAPICallError(error)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from "react"
|
import { useState, useCallback, useRef, useEffect, memo } from "react"
|
||||||
import {
|
import {
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
ThumbsUpIcon,
|
ThumbsUpIcon,
|
||||||
@ -20,9 +20,20 @@ import {
|
|||||||
IconAlertCircle,
|
IconAlertCircle,
|
||||||
IconEye,
|
IconEye,
|
||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
import type { ToolUIPart } from "ai"
|
import {
|
||||||
|
isTextUIPart,
|
||||||
|
isToolUIPart,
|
||||||
|
isReasoningUIPart,
|
||||||
|
type UIMessage,
|
||||||
|
type ToolUIPart,
|
||||||
|
type DynamicToolUIPart,
|
||||||
|
} from "ai"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import {
|
||||||
|
Reasoning,
|
||||||
|
ReasoningTrigger,
|
||||||
|
ReasoningContent,
|
||||||
|
} from "@/components/ai/reasoning"
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationContent,
|
ConversationContent,
|
||||||
@ -160,24 +171,37 @@ function friendlyToolName(raw: string): string {
|
|||||||
return TOOL_DISPLAY_NAMES[raw] ?? raw
|
return TOOL_DISPLAY_NAMES[raw] ?? raw
|
||||||
}
|
}
|
||||||
|
|
||||||
// shared message + tool rendering for both variants
|
interface ChatMessageProps {
|
||||||
function ChatMessage({
|
readonly msg: UIMessage
|
||||||
|
readonly copiedId: string | null
|
||||||
|
readonly onCopy: (id: string, text: string) => void
|
||||||
|
readonly onRegenerate: () => void
|
||||||
|
readonly isStreaming?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyToolPart = ToolUIPart | DynamicToolUIPart
|
||||||
|
|
||||||
|
function extractToolName(part: AnyToolPart): string {
|
||||||
|
if (part.type === "dynamic-tool") {
|
||||||
|
return part.toolName ?? ""
|
||||||
|
}
|
||||||
|
return part.type.slice(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders parts in their natural order from the AI SDK
|
||||||
|
const ChatMessage = memo(
|
||||||
|
function ChatMessage({
|
||||||
msg,
|
msg,
|
||||||
copiedId,
|
copiedId,
|
||||||
onCopy,
|
onCopy,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
}: {
|
isStreaming: msgStreaming = false,
|
||||||
readonly msg: {
|
}: ChatMessageProps) {
|
||||||
readonly id: string
|
|
||||||
readonly role: string
|
|
||||||
readonly parts: ReadonlyArray<unknown>
|
|
||||||
}
|
|
||||||
readonly copiedId: string | null
|
|
||||||
onCopy: (id: string, text: string) => void
|
|
||||||
onRegenerate: () => void
|
|
||||||
}) {
|
|
||||||
if (msg.role === "user") {
|
if (msg.role === "user") {
|
||||||
const text = getTextContent(msg.parts)
|
const text = msg.parts
|
||||||
|
.filter(isTextUIPart)
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join("")
|
||||||
return (
|
return (
|
||||||
<Message from="user">
|
<Message from="user">
|
||||||
<MessageContent>{text}</MessageContent>
|
<MessageContent>{text}</MessageContent>
|
||||||
@ -185,53 +209,85 @@ function ChatMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const textParts: string[] = []
|
// walk parts sequentially, flushing text when
|
||||||
const toolParts: Array<{
|
// hitting a tool or reasoning part to preserve
|
||||||
type: string
|
// interleaving. text flushed before the final
|
||||||
state: ToolUIPart["state"]
|
// segment is "thinking" (intermediate chain-of-
|
||||||
toolName: string
|
// thought) and rendered muted + collapsible.
|
||||||
input: unknown
|
const elements: React.ReactNode[] = []
|
||||||
output: unknown
|
let pendingText = ""
|
||||||
errorText?: string
|
let allText = ""
|
||||||
}> = []
|
let pendingReasoning = ""
|
||||||
|
let reasoningStreaming = false
|
||||||
|
|
||||||
for (const part of msg.parts) {
|
let sawToolPart = false
|
||||||
const p = part as Record<string, unknown>
|
|
||||||
if (p.type === "text" && typeof p.text === "string") {
|
const flushThinking = (
|
||||||
textParts.push(p.text)
|
text: string,
|
||||||
}
|
idx: number,
|
||||||
const pType = p.type as string | undefined
|
streaming = false
|
||||||
// handle static (tool-<name>) and dynamic
|
) => {
|
||||||
// (dynamic-tool) tool parts
|
if (!text) return
|
||||||
if (
|
elements.push(
|
||||||
typeof pType === "string" &&
|
<Reasoning
|
||||||
(pType.startsWith("tool-") ||
|
key={`think-${idx}`}
|
||||||
pType === "dynamic-tool")
|
isStreaming={streaming}
|
||||||
) {
|
defaultOpen={false}
|
||||||
// extract tool name from type field or toolName
|
>
|
||||||
const rawName = pType.startsWith("tool-")
|
<ReasoningTrigger />
|
||||||
? pType.slice(5)
|
<ReasoningContent>{text}</ReasoningContent>
|
||||||
: ((p.toolName ?? "") as string)
|
</Reasoning>
|
||||||
toolParts.push({
|
)
|
||||||
type: pType,
|
|
||||||
state: p.state as ToolUIPart["state"],
|
|
||||||
toolName:
|
|
||||||
friendlyToolName(rawName) || "Working",
|
|
||||||
input: p.input,
|
|
||||||
output: p.output,
|
|
||||||
errorText: p.errorText as string | undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = textParts.join("")
|
const flushText = (idx: number, isFinal: boolean) => {
|
||||||
|
if (!pendingText) return
|
||||||
|
if (!isFinal) {
|
||||||
|
// intermediate text before more tools = thinking
|
||||||
|
flushThinking(pendingText, idx)
|
||||||
|
} else {
|
||||||
|
elements.push(
|
||||||
|
<MessageContent key={`text-${idx}`}>
|
||||||
|
<MessageResponse>
|
||||||
|
{pendingText}
|
||||||
|
</MessageResponse>
|
||||||
|
</MessageContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pendingText = ""
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
for (let i = 0; i < msg.parts.length; i++) {
|
||||||
<Message from="assistant">
|
const part = msg.parts[i]
|
||||||
{toolParts.map((tp, i) => (
|
|
||||||
<Tool key={i}>
|
if (isReasoningUIPart(part)) {
|
||||||
|
pendingReasoning += part.text
|
||||||
|
reasoningStreaming = part.state === "streaming"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTextUIPart(part)) {
|
||||||
|
pendingText += part.text
|
||||||
|
allText += part.text
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToolUIPart(part)) {
|
||||||
|
sawToolPart = true
|
||||||
|
// flush reasoning accumulated before this tool
|
||||||
|
flushThinking(pendingReasoning, i, reasoningStreaming)
|
||||||
|
pendingReasoning = ""
|
||||||
|
reasoningStreaming = false
|
||||||
|
// flush text as thinking (not final)
|
||||||
|
flushText(i, false)
|
||||||
|
const tp = part as AnyToolPart
|
||||||
|
const rawName = extractToolName(tp)
|
||||||
|
elements.push(
|
||||||
|
<Tool key={tp.toolCallId}>
|
||||||
<ToolHeader
|
<ToolHeader
|
||||||
title={tp.toolName}
|
title={
|
||||||
|
friendlyToolName(rawName) || "Working"
|
||||||
|
}
|
||||||
type={tp.type as ToolUIPart["type"]}
|
type={tp.type as ToolUIPart["type"]}
|
||||||
state={tp.state}
|
state={tp.state}
|
||||||
/>
|
/>
|
||||||
@ -246,16 +302,44 @@ function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
</ToolContent>
|
</ToolContent>
|
||||||
</Tool>
|
</Tool>
|
||||||
))}
|
)
|
||||||
{text ? (
|
}
|
||||||
<>
|
}
|
||||||
<MessageContent>
|
|
||||||
<MessageResponse>{text}</MessageResponse>
|
// flush remaining reasoning
|
||||||
</MessageContent>
|
flushThinking(
|
||||||
|
pendingReasoning,
|
||||||
|
msg.parts.length,
|
||||||
|
reasoningStreaming
|
||||||
|
)
|
||||||
|
|
||||||
|
// while streaming, if no tool calls have arrived yet
|
||||||
|
// and text is substantial, it's likely chain-of-thought
|
||||||
|
// that'll be reclassified as thinking once tools come in.
|
||||||
|
// render it collapsed so it doesn't flood the screen.
|
||||||
|
const COT_THRESHOLD = 500
|
||||||
|
if (
|
||||||
|
msgStreaming &&
|
||||||
|
!sawToolPart &&
|
||||||
|
pendingText.length > COT_THRESHOLD
|
||||||
|
) {
|
||||||
|
flushThinking(pendingText, msg.parts.length, true)
|
||||||
|
pendingText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush remaining text as the final response
|
||||||
|
flushText(msg.parts.length, true)
|
||||||
|
|
||||||
|
const hasContent = elements.length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Message from="assistant">
|
||||||
|
{hasContent ? elements : <Loader />}
|
||||||
|
{allText && (
|
||||||
<Actions>
|
<Actions>
|
||||||
<Action
|
<Action
|
||||||
tooltip="Copy"
|
tooltip="Copy"
|
||||||
onClick={() => onCopy(msg.id, text)}
|
onClick={() => onCopy(msg.id, allText)}
|
||||||
>
|
>
|
||||||
{copiedId === msg.id ? (
|
{copiedId === msg.id ? (
|
||||||
<Check className="size-4" />
|
<Check className="size-4" />
|
||||||
@ -276,25 +360,23 @@ function ChatMessage({
|
|||||||
<RefreshCcwIcon className="size-4" />
|
<RefreshCcwIcon className="size-4" />
|
||||||
</Action>
|
</Action>
|
||||||
</Actions>
|
</Actions>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Loader />
|
|
||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
(prev, next) => {
|
||||||
function getTextContent(
|
if (prev.msg !== next.msg) return false
|
||||||
parts: ReadonlyArray<unknown>
|
if (prev.onCopy !== next.onCopy) return false
|
||||||
): string {
|
if (prev.onRegenerate !== next.onRegenerate)
|
||||||
return (parts as ReadonlyArray<{ type: string; text?: string }>)
|
return false
|
||||||
.filter(
|
if (prev.isStreaming !== next.isStreaming)
|
||||||
(p): p is { type: "text"; text: string } =>
|
return false
|
||||||
p.type === "text"
|
const prevCopied = prev.copiedId === prev.msg.id
|
||||||
)
|
const nextCopied = next.copiedId === next.msg.id
|
||||||
.map((p) => p.text)
|
if (prevCopied !== nextCopied) return false
|
||||||
.join("")
|
return true
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function ChatInput({
|
function ChatInput({
|
||||||
textareaRef,
|
textareaRef,
|
||||||
@ -533,6 +615,21 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
[isPage, chat.sendMessage]
|
[isPage, chat.sendMessage]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleIdleSend = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setIsActive(true)
|
||||||
|
chat.sendMessage({ text })
|
||||||
|
},
|
||||||
|
[chat.sendMessage]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleActiveSend = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
chat.sendMessage({ text })
|
||||||
|
},
|
||||||
|
[chat.sendMessage]
|
||||||
|
)
|
||||||
|
|
||||||
const suggestions = isPage
|
const suggestions = isPage
|
||||||
? DASHBOARD_SUGGESTIONS
|
? DASHBOARD_SUGGESTIONS
|
||||||
: getSuggestionsForPath(chat.pathname)
|
: getSuggestionsForPath(chat.pathname)
|
||||||
@ -593,10 +690,7 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
recorder={recorder}
|
recorder={recorder}
|
||||||
status={chat.status}
|
status={chat.status}
|
||||||
isGenerating={chat.isGenerating}
|
isGenerating={chat.isGenerating}
|
||||||
onSend={(text) => {
|
onSend={handleIdleSend}
|
||||||
setIsActive(true)
|
|
||||||
chat.sendMessage({ text })
|
|
||||||
}}
|
|
||||||
className="rounded-2xl"
|
className="rounded-2xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -652,13 +746,19 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
{chat.messages.length > 0 ? (
|
{chat.messages.length > 0 ? (
|
||||||
<Conversation className="flex-1">
|
<Conversation className="flex-1">
|
||||||
<ConversationContent className="mx-auto w-full max-w-3xl">
|
<ConversationContent className="mx-auto w-full max-w-3xl">
|
||||||
{chat.messages.map((msg) => (
|
{chat.messages.map((msg, idx) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
msg={msg}
|
msg={msg}
|
||||||
copiedId={copiedId}
|
copiedId={copiedId}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onRegenerate={chat.regenerate}
|
onRegenerate={chat.regenerate}
|
||||||
|
isStreaming={
|
||||||
|
(chat.status === "streaming" ||
|
||||||
|
chat.status === "submitted") &&
|
||||||
|
idx === chat.messages.length - 1 &&
|
||||||
|
msg.role === "assistant"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ConversationContent>
|
</ConversationContent>
|
||||||
@ -698,9 +798,7 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
recorder={recorder}
|
recorder={recorder}
|
||||||
status={chat.status}
|
status={chat.status}
|
||||||
isGenerating={chat.isGenerating}
|
isGenerating={chat.isGenerating}
|
||||||
onSend={(text) =>
|
onSend={handleActiveSend}
|
||||||
chat.sendMessage({ text })
|
|
||||||
}
|
|
||||||
onNewChat={chat.messages.length > 0 ? chat.newChat : undefined}
|
onNewChat={chat.messages.length > 0 ? chat.newChat : undefined}
|
||||||
className="rounded-2xl"
|
className="rounded-2xl"
|
||||||
/>
|
/>
|
||||||
@ -729,13 +827,19 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
</Suggestions>
|
</Suggestions>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
chat.messages.map((msg) => (
|
chat.messages.map((msg, idx) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
msg={msg}
|
msg={msg}
|
||||||
copiedId={copiedId}
|
copiedId={copiedId}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onRegenerate={chat.regenerate}
|
onRegenerate={chat.regenerate}
|
||||||
|
isStreaming={
|
||||||
|
(chat.status === "streaming" ||
|
||||||
|
chat.status === "submitted") &&
|
||||||
|
idx === chat.messages.length - 1 &&
|
||||||
|
msg.role === "assistant"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@ -751,9 +855,7 @@ export function ChatView({ variant }: ChatViewProps) {
|
|||||||
recorder={recorder}
|
recorder={recorder}
|
||||||
status={chat.status}
|
status={chat.status}
|
||||||
isGenerating={chat.isGenerating}
|
isGenerating={chat.isGenerating}
|
||||||
onSend={(text) =>
|
onSend={handleActiveSend}
|
||||||
chat.sendMessage({ text })
|
|
||||||
}
|
|
||||||
onNewChat={chat.messages.length > 0 ? chat.newChat : undefined}
|
onNewChat={chat.messages.length > 0 ? chat.newChat : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react"
|
import { BrainIcon, ChevronDownIcon } from "lucide-react"
|
||||||
import type { ComponentProps, ReactNode } from "react"
|
import type { ComponentProps, ReactNode } from "react"
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react"
|
import { createContext, memo, useContext, useEffect, useRef, useState } from "react"
|
||||||
import { Streamdown } from "streamdown"
|
import { Streamdown } from "streamdown"
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@ -34,7 +34,7 @@ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|||||||
duration?: number
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 1000
|
const CLOSE_GRACE_MS = 300
|
||||||
const MS_IN_S = 1000
|
const MS_IN_S = 1000
|
||||||
|
|
||||||
export const Reasoning = memo(
|
export const Reasoning = memo(
|
||||||
@ -73,14 +73,13 @@ export const Reasoning = memo(
|
|||||||
}
|
}
|
||||||
}, [isStreaming, startTime, setDuration])
|
}, [isStreaming, startTime, setDuration])
|
||||||
|
|
||||||
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
// close gracefully once streaming finishes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
||||||
// Add a small delay before closing to allow user to see the content
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setHasAutoClosed(true)
|
setHasAutoClosed(true)
|
||||||
}, AUTO_CLOSE_DELAY)
|
}, CLOSE_GRACE_MS)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}
|
}
|
||||||
@ -154,7 +153,18 @@ export type ReasoningContentProps = ComponentProps<typeof CollapsibleContent> &
|
|||||||
children: string
|
children: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => (
|
export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => {
|
||||||
|
const { isStreaming } = useReasoning()
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// auto-scroll to show latest tokens during streaming
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
}, [children, isStreaming])
|
||||||
|
|
||||||
|
return (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-4 text-sm",
|
"mt-4 text-sm",
|
||||||
@ -162,10 +172,16 @@ export const ReasoningContent = memo(({ className, children, ...props }: Reasoni
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className={cn(isStreaming && "max-h-[3.75rem] overflow-hidden")}
|
||||||
>
|
>
|
||||||
<Streamdown {...props}>{children}</Streamdown>
|
<Streamdown {...props}>{children}</Streamdown>
|
||||||
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
Reasoning.displayName = "Reasoning"
|
Reasoning.displayName = "Reasoning"
|
||||||
ReasoningTrigger.displayName = "ReasoningTrigger"
|
ReasoningTrigger.displayName = "ReasoningTrigger"
|
||||||
|
|||||||
@ -5,22 +5,6 @@ import type { PromptSection } from "@/lib/agent/plugins/types"
|
|||||||
|
|
||||||
type PromptMode = "full" | "minimal" | "none"
|
type PromptMode = "full" | "minimal" | "none"
|
||||||
|
|
||||||
type ToolCategory =
|
|
||||||
| "data"
|
|
||||||
| "navigation"
|
|
||||||
| "ui"
|
|
||||||
| "memory"
|
|
||||||
| "github"
|
|
||||||
| "skills"
|
|
||||||
| "feedback"
|
|
||||||
|
|
||||||
interface ToolMeta {
|
|
||||||
readonly name: string
|
|
||||||
readonly summary: string
|
|
||||||
readonly category: ToolCategory
|
|
||||||
readonly adminOnly?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardSummary {
|
interface DashboardSummary {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly name: string
|
readonly name: string
|
||||||
@ -38,6 +22,22 @@ interface PromptContext {
|
|||||||
readonly mode?: PromptMode
|
readonly mode?: PromptMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolCategory =
|
||||||
|
| "data"
|
||||||
|
| "navigation"
|
||||||
|
| "ui"
|
||||||
|
| "memory"
|
||||||
|
| "github"
|
||||||
|
| "skills"
|
||||||
|
| "feedback"
|
||||||
|
|
||||||
|
interface ToolMeta {
|
||||||
|
readonly name: string
|
||||||
|
readonly summary: string
|
||||||
|
readonly category: ToolCategory
|
||||||
|
readonly adminOnly?: true
|
||||||
|
}
|
||||||
|
|
||||||
interface DerivedState {
|
interface DerivedState {
|
||||||
readonly mode: PromptMode
|
readonly mode: PromptMode
|
||||||
readonly page: string
|
readonly page: string
|
||||||
@ -665,6 +665,10 @@ function buildGuidelines(
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
...core,
|
...core,
|
||||||
|
"- Tool workflow: data requests -> queryData immediately. " +
|
||||||
|
"Navigation -> navigateTo, brief confirmation. " +
|
||||||
|
"Dashboards -> queryData first, then generateUI. " +
|
||||||
|
"Memories -> save proactively with rememberContext.",
|
||||||
'- "How\'s development going?" means fetch repo_stats and ' +
|
'- "How\'s development going?" means fetch repo_stats and ' +
|
||||||
'recent commits right now, not "Would you like to see ' +
|
'recent commits right now, not "Would you like to see ' +
|
||||||
'commits or PRs?"',
|
'commits or PRs?"',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user