A question to @g1t asked on a page says which page under it, in Chat and in the Ask panel: "Asked on pull request #8 in flagon-io/g1t", linking back, so "summarize this pull request" or "is this done?" reads as the one it was about when the conversation is read later, as g1t was told. A question written in Chat itself, whose page is the direct message, says nothing. The quality bar's questions start with [TEST], so they are told apart from the person's own in the direct message they are asked in. Checked: the site's typecheck and its 890 tests, a production build whose chunks load first without error (scripted in a browser over the build), and the bench's next run.
6 files+53−70/6 viewed
| 11 | 11 | import { Message, MessageContent } from "./ai-elements/message"; | |
| 12 | 12 | import type { ResponseContext } from "./ai-elements/response"; | |
| 13 | 13 | import { Suggestion, Suggestions } from "./ai-elements/suggestion"; | |
| 14 | − | import { G1tAnswer } from "./chat/answer"; | |
| 14 | + | import { AskedFrom, G1tAnswer } from "./chat/answer"; | |
| 15 | 15 | import { useChatLive } from "./chat/live"; | |
| 16 | 16 | import { MarkGlyph } from "./logo"; | |
| 17 | 17 | import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; | |
| ⋯ | |||
| 398 | 398 | ) : ( | |
| 399 | 399 | <Message key={m.id} from="user"> | |
| 400 | 400 | <MessageContent className={m.pending && !m.failed ? "opacity-70" : undefined}>{m.body}</MessageContent> | |
| 401 | + | <AskedFrom page={m.page} /> | |
| 401 | 402 | {m.failed && <span className="text-xs text-danger">Not sent. Ask again.</span>} | |
| 402 | 403 | </Message> | |
| 403 | 404 | ), | |
| 5 | 5 | * as they stream, then the answer with who wrote it and what can be done | |
| 6 | 6 | * with it: copy it, or have g1t answer again. | |
| 7 | 7 | */ | |
| 8 | − | import { Check, CircleAlert, Copy, RefreshCw, Settings2 } from "lucide-react"; | |
| 8 | + | import { Check, CircleAlert, Copy, CornerDownRight, RefreshCw, Settings2 } from "lucide-react"; | |
| 9 | 9 | import { type ReactNode, useState } from "react"; | |
| 10 | 10 | ||
| 11 | − | import type { ChatMessage } from "@g1t/contracts"; | |
| 11 | + | import type { AskPage, ChatMessage } from "@g1t/contracts"; | |
| 12 | 12 | ||
| 13 | − | import { answerLabel, answering, cutOff } from "../../lib/g1t-chat"; | |
| 13 | + | import { answerLabel, answering, askedFrom, cutOff } from "../../lib/g1t-chat"; | |
| 14 | 14 | import { AskTrail } from "../ask-steps"; | |
| 15 | 15 | import { Loader } from "../ai-elements/loader"; | |
| 16 | 16 | import { MessageAction, MessageActions } from "../ai-elements/message"; | |
| ⋯ | |||
| 110 | 110 | </div> | |
| 111 | 111 | ); | |
| 112 | 112 | } | |
| 113 | + | ||
| 114 | + | /** | |
| 115 | + | * Under a question to @g1t asked on a page: which page, so "this pull | |
| 116 | + | * request" reads as the one it was. g1t was told the same. | |
| 117 | + | */ | |
| 118 | + | export function AskedFrom({ page }: { page: AskPage | null | undefined }) { | |
| 119 | + | const from = askedFrom(page); | |
| 120 | + | if (!from) return null; | |
| 121 | + | return ( | |
| 122 | + | <a href={from.href} className="mt-0.5 inline-flex max-w-full items-center gap-1 text-xs text-faint hover:text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"> | |
| 123 | + | <CornerDownRight size={12} aria-hidden="true" className="shrink-0" /> | |
| 124 | + | <span className="truncate">Asked on {from.label}</span> | |
| 125 | + | </a> | |
| 126 | + | ); | |
| 127 | + | } | |
| 47 | 47 | import { BottomSheet, SheetRow, useBack, useSwipeBack } from "../mobile"; | |
| 48 | 48 | import { MessageText, type TextContext } from "./text"; | |
| 49 | 49 | // @g1t's answers and its direct message (lib/g1t-chat.ts, components/chat/answer.tsx). | |
| 50 | − | import { ConversationMark, G1tAnswer } from "./answer"; | |
| 50 | + | import { AskedFrom, ConversationMark, G1tAnswer } from "./answer"; | |
| 51 | 51 | import type { ResponseContext } from "../ai-elements/response"; | |
| 52 | 52 | import { Suggestion, Suggestions } from "../ai-elements/suggestion"; | |
| 53 | 53 | import { anyAnswering, applyLive, fromG1t, suggestions } from "../../lib/g1t-chat"; | |
| ⋯ | |||
| 1334 | 1334 | ) : ( | |
| 1335 | 1335 | message.body && <MessageText body={message.body} context={context} /> | |
| 1336 | 1336 | )} | |
| 1337 | + | {!editor && !answer && message.page && <AskedFrom page={message.page} />} | |
| 1337 | 1338 | {!editor && !answer && message.body && !message.pending && <FolioUnfurls slug={slug} body={message.body} />} | |
| 1338 | 1339 | {message.card && ( | |
| 1339 | 1340 | <CardBox card={message.card} slug={slug} code={code} /> | |
| 3 | 3 | ||
| 4 | 4 | import type { ChatLiveEvent, ChatMessage } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | − | import { answerLabel, anyAnswering, applyLive, cutOff, fromG1t, refTarget, suggestions } from "./g1t-chat.ts"; | |
| 6 | + | import { askedFrom, answerLabel, anyAnswering, applyLive, cutOff, fromG1t, refTarget, suggestions } from "./g1t-chat.ts"; | |
| 7 | 7 | ||
| 8 | 8 | const g1t = { kind: "system" as const, id: "g1t", name: "g1t", display_name: "g1t", avatar: null, role: null }; | |
| 9 | 9 | const ana = { kind: "user" as const, id: "u1", name: "ana", display_name: "Ana", avatar: null, role: null }; | |
| ⋯ | |||
| 76 | 76 | assert.equal(refTarget("//evil.example/x"), null); | |
| 77 | 77 | assert.equal(refTarget("javascript:alert(1)"), null); | |
| 78 | 78 | }); | |
| 79 | + | ||
| 80 | + | test("a question asked on a page says which, and one written in Chat says nothing", () => { | |
| 81 | + | const repo = { namespace: "flagon-io", name: "g1t" }; | |
| 82 | + | assert.deepEqual(askedFrom({ path: "/flagon-io/g1t/pull/8", repo, pull: 8 }), { label: "pull request #8 in flagon-io/g1t", href: "/flagon-io/g1t/pull/8" }); | |
| 83 | + | assert.deepEqual(askedFrom({ repo, issue: 21 }), { label: "issue #21 in flagon-io/g1t", href: "/flagon-io/g1t/issues/21" }); | |
| 84 | + | assert.equal(askedFrom({ path: "/flagon-io/g1t", repo })?.label, "flagon-io/g1t"); | |
| 85 | + | assert.equal(askedFrom({ path: "/flagon-io/-/chat/dm/chn_1" }), null); | |
| 86 | + | assert.equal(askedFrom({ time_zone: "America/Denver" }), null); | |
| 87 | + | assert.equal(askedFrom(null), null); | |
| 88 | + | }); | |
| 73 | 73 | return ["What shipped today?", "What needs my attention?", "Summarize what my team is working on", "How do I set up a deployment?"]; | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | /** | |
| 77 | + | * Where a question to @g1t was asked from, when it was asked on a page | |
| 78 | + | * (the top bar's Ask on a pull request, an issue or a repository): what to | |
| 79 | + | * call it and where it leads. Null for a question written in Chat. | |
| 80 | + | */ | |
| 81 | + | export function askedFrom(page: AskPage | null | undefined): { label: string; href: string } | null { | |
| 82 | + | if (!page) return null; | |
| 83 | + | const repo = page.repo ? `${page.repo.namespace}/${page.repo.name}` : null; | |
| 84 | + | if (repo && page.pull) return { label: `pull request #${page.pull} in ${repo}`, href: page.path ?? `/${repo}/pull/${page.pull}` }; | |
| 85 | + | if (repo && page.issue) return { label: `issue #${page.issue} in ${repo}`, href: page.path ?? `/${repo}/issues/${page.issue}` }; | |
| 86 | + | if (repo) return { label: repo, href: page.path ?? `/${repo}` }; | |
| 87 | + | if (page.path && !/\/-\/chat(\/|$)/.test(page.path)) return { label: page.path, href: page.path }; | |
| 88 | + | return null; | |
| 89 | + | } | |
| 90 | + | ||
| 76 | 91 | /** A link g1t knows how to show as a chip: an issue or pull request, a commit, or another page of the site. */ | |
| 77 | 92 | export type RefTarget = | |
| 78 | 93 | | { kind: "issue"; repo: string; number: number; href: string } |
| 182 | 182 | return found; | |
| 183 | 183 | } | |
| 184 | 184 | ||
| 185 | + | /** What every question the bench posts starts with. */ | |
| 186 | + | export const TEST_PREFIX = "[TEST] "; | |
| 187 | + | ||
| 185 | 188 | /** A chat API call on the site as the user: the `{ ok, value | error }` it answers. */ | |
| 186 | 189 | async function chatApi(workspace, body) { | |
| 187 | 190 | const response = await site(`/${workspace}/-/chat/api`, { | |
| ⋯ | |||
| 239 | 242 | for (const [index, question] of questions.entries()) { | |
| 240 | 243 | const last = index === questions.length - 1; | |
| 241 | 244 | const asked = performance.now(); | |
| 242 | − | const posted = await chatApi(workspace, { intent: "post", channel_id: channel, body: question.content, page: last ? (page ?? null) : null }); | |
| 245 | + | // Marked, so the person whose direct message it is can tell the bench's questions from their own. | |
| 246 | + | const posted = await chatApi(workspace, { intent: "post", channel_id: channel, body: `${TEST_PREFIX}${question.content}`, page: last ? (page ?? null) : null }); | |
| 243 | 247 | result.status = posted.ok ? 200 : 400; | |
| 244 | 248 | if (!posted.ok) throw new Error(posted.error?.message ?? "could not post"); | |
| 245 | 249 | const mine = posted.value.id; | |