mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
storybook: treat work mode as the task type in Jev routing
Drop the separate bug/feature/research type classification. Jev now answers three choice questions: work mode (Auto, Plan, or Ask), assignee, and project. The mode chip leads the row, is always marked as suggested, shows a one-line description and Jev's probability per mode, and supports undo. Owner signals remain internal to the simulation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWn6etZjfGz6p8NWYkCQve
This commit is contained in:
4 files changed
+119
-128
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import type { IssueWorkMode } from "@paperclipai/shared";
|
||||
import { AlertTriangle, Check, ChevronDown, FolderKanban, Pencil, RotateCcw, Sparkles, Tag, Undo2 } from "lucide-react";
|
||||
import { AlertTriangle, Check, ChevronDown, FolderKanban, Pencil, RotateCcw, Sparkles, Undo2 } from "lucide-react";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -15,13 +15,11 @@ import {
|
||||
JEV_NAME,
|
||||
JEV_NO_PROJECT,
|
||||
JEV_PROJECTS,
|
||||
JEV_TASK_TYPES,
|
||||
buildJevDecisionRequest,
|
||||
draftTaskTitle,
|
||||
routeTaskWithJev,
|
||||
type JevField,
|
||||
type JevRouting,
|
||||
type JevTaskType,
|
||||
} from "./jev-classifier";
|
||||
|
||||
export type JevTaskComposerProps = {
|
||||
@@ -47,7 +45,7 @@ export type JevTaskComposerProps = {
|
||||
mobile?: boolean;
|
||||
};
|
||||
|
||||
type Overrides = Partial<{ title: string; type: JevTaskType; assignee: string; project: string | null; workMode: IssueWorkMode }>;
|
||||
type Overrides = Partial<{ title: string; workMode: IssueWorkMode; assignee: string; project: string | null }>;
|
||||
type OverrideField = keyof Overrides;
|
||||
type Status = "idle" | "thinking" | "ready" | "error";
|
||||
|
||||
@@ -157,7 +155,6 @@ export function JevTaskComposer({
|
||||
|
||||
const effective = useMemo(() => ({
|
||||
title: overrides.title ?? suggestedTitle ?? "",
|
||||
type: overrides.type ?? routing?.type ?? null,
|
||||
assignee: overrides.assignee ?? routing?.assigneeId ?? null,
|
||||
project: overrides.project !== undefined ? overrides.project : routing?.projectId ?? null,
|
||||
workMode: overrides.workMode ?? routing?.workMode ?? "standard",
|
||||
@@ -241,7 +238,7 @@ export function JevTaskComposer({
|
||||
}}
|
||||
placeholder={flow === "instant"
|
||||
? "What should get done? It gets a title and an owner once you start."
|
||||
: "What should get done? A title, type, and owner are suggested as you write."}
|
||||
: "What should get done? A title, mode, and owner are suggested as you write."}
|
||||
className="min-h-36 resize-none border-0 bg-transparent px-0 text-base shadow-none focus-visible:ring-0 dark:bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
@@ -379,7 +376,6 @@ function ComposerHeader({
|
||||
|
||||
type Effective = {
|
||||
title: string;
|
||||
type: JevTaskType | null;
|
||||
assignee: string | null;
|
||||
project: string | null;
|
||||
workMode: IssueWorkMode;
|
||||
@@ -427,7 +423,7 @@ function RoutingPanel({
|
||||
const why = fallbackOwner.reportsTo === null ? "who reports to the board" : "the first agent in your org";
|
||||
return `${JEV_NAME} wasn't sure who should own this (${percent(routing!.confidence.assignee)}), so it goes to ${fallbackOwner.name}, ${why}.`;
|
||||
}
|
||||
if (status === "idle" && flow === "instant" && !created) return `${JEV_NAME} picks the type and owner after you start. You can change anything later.`;
|
||||
if (status === "idle" && flow === "instant" && !created) return `${JEV_NAME} picks the mode and owner after you start. You can change anything later.`;
|
||||
if (status === "idle" && promptLength > 0 && promptLength < JEV_MIN_PROMPT_LENGTH) return "Keep going. Suggestions start once there's a bit more to go on.";
|
||||
return null;
|
||||
})();
|
||||
@@ -458,28 +454,14 @@ function RoutingPanel({
|
||||
|
||||
{showChips ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<PropertyChip
|
||||
label="Type"
|
||||
<WorkModeChip
|
||||
loading={loading}
|
||||
suggested={fromJev("type")}
|
||||
onReset={resetFor("type")}
|
||||
display={<><Tag aria-hidden className="size-3.5" />{JEV_TASK_TYPES.find((option) => option.value === effective.type)?.label ?? "Type"}</>}
|
||||
>
|
||||
{(close) => JEV_TASK_TYPES.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === effective.type}
|
||||
probability={probabilities?.type[option.value]}
|
||||
onClick={() => { setField("type", option.value); close(); }}
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span>{option.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{option.hint}</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</PropertyChip>
|
||||
|
||||
mode={effective.workMode}
|
||||
suggested={fromJev("workMode")}
|
||||
probabilities={probabilities?.workMode}
|
||||
onChange={(mode) => setField("workMode", mode)}
|
||||
onReset={resetFor("workMode")}
|
||||
/>
|
||||
<PropertyChip
|
||||
label="Assignee"
|
||||
loading={loading}
|
||||
@@ -534,13 +516,6 @@ function RoutingPanel({
|
||||
]}
|
||||
</PropertyChip>
|
||||
|
||||
<WorkModeChip
|
||||
loading={loading}
|
||||
mode={effective.workMode}
|
||||
suggested={fromJev("workMode") && effective.workMode !== "standard"}
|
||||
probabilities={probabilities?.workMode}
|
||||
onChange={(mode) => setField("workMode", mode)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -569,10 +544,9 @@ function RoutingPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const FIELD_LABELS: Record<JevField, string> = { type: "Type", assignee: "Assignee", project: "Project", workMode: "Mode" };
|
||||
const FIELD_LABELS: Record<JevField, string> = { workMode: "Mode", assignee: "Assignee", project: "Project" };
|
||||
|
||||
function choiceLabel(field: JevField, routing: JevRouting): string {
|
||||
if (field === "type") return JEV_TASK_TYPES.find((option) => option.value === routing.type)?.label ?? routing.type;
|
||||
if (field === "assignee") {
|
||||
const name = (id: string) => JEV_AGENTS.find((agent) => agent.id === id)?.name ?? id;
|
||||
return routing.assigneeSource === "fallback"
|
||||
@@ -584,7 +558,7 @@ function choiceLabel(field: JevField, routing: JevRouting): string {
|
||||
}
|
||||
|
||||
function ConfidenceDetails({ routing, prompt, agents }: { routing: JevRouting; prompt: string; agents: typeof JEV_AGENTS }) {
|
||||
const fields: JevField[] = ["type", "assignee", "project", "workMode"];
|
||||
const fields: JevField[] = ["workMode", "assignee", "project"];
|
||||
return (
|
||||
<div className="mt-2 flex flex-col gap-2 pl-4">
|
||||
<dl className="grid grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-1.5">
|
||||
@@ -699,53 +673,72 @@ function MenuItem({
|
||||
);
|
||||
}
|
||||
|
||||
const WORK_MODE_HINTS: Partial<Record<IssueWorkMode, string>> = {
|
||||
standard: "The agent does the work",
|
||||
planning: "The agent writes a plan for your review first",
|
||||
ask: "The agent answers without changing anything",
|
||||
};
|
||||
|
||||
/** The task's type. Uses the shipped work-mode labels, icons, and colors. */
|
||||
function WorkModeChip({
|
||||
mode,
|
||||
loading,
|
||||
suggested,
|
||||
probabilities,
|
||||
onChange,
|
||||
onReset,
|
||||
}: {
|
||||
mode: IssueWorkMode;
|
||||
loading: boolean;
|
||||
suggested: boolean;
|
||||
probabilities?: Record<string, number>;
|
||||
onChange: (mode: IssueWorkMode) => void;
|
||||
onReset?: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const meta = workModeMetaFor(mode);
|
||||
const Icon = meta.icon;
|
||||
if (loading) return <span aria-label="Mode loading" className="h-7 w-20 animate-pulse rounded-md border border-border bg-muted/60" />;
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Mode${suggested ? ` (suggested by ${JEV_NAME})` : ""}`}
|
||||
className={cn("inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-sm transition-colors", meta.classes.chip)}
|
||||
>
|
||||
{suggested ? <Sparkles aria-hidden className="size-3" /> : null}
|
||||
<Icon aria-hidden className="size-3.5" />
|
||||
{meta.label}
|
||||
<ChevronDown aria-hidden className="size-3 opacity-60" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-56 p-1">
|
||||
{workModeMetaList().map((option) => {
|
||||
const OptionIcon = option.icon;
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === mode}
|
||||
probability={probabilities?.[option.value]}
|
||||
onClick={() => { onChange(option.value); setOpen(false); }}
|
||||
>
|
||||
<OptionIcon aria-hidden className={cn("size-3.5", option.classes.menuItem)} />
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<span className="inline-flex items-center">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Mode${suggested ? ` (suggested by ${JEV_NAME})` : ""}`}
|
||||
className={cn("inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-sm transition-colors", meta.classes.chip)}
|
||||
>
|
||||
{suggested ? <Sparkles aria-hidden className="size-3" /> : null}
|
||||
<Icon aria-hidden className="size-3.5" />
|
||||
{meta.label}
|
||||
<ChevronDown aria-hidden className="size-3 opacity-60" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-72 p-1">
|
||||
{workModeMetaList().map((option) => {
|
||||
const OptionIcon = option.icon;
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === mode}
|
||||
probability={probabilities?.[option.value]}
|
||||
onClick={() => { onChange(option.value); setOpen(false); }}
|
||||
>
|
||||
<OptionIcon aria-hidden className={cn("size-3.5 shrink-0", option.classes.menuItem)} />
|
||||
<span className="flex flex-col">
|
||||
<span>{option.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{WORK_MODE_HINTS[option.value]}</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{onReset ? (
|
||||
<Button variant="ghost" size="icon-xs" className="text-muted-foreground" onClick={onReset} title={`Use ${JEV_NAME}'s suggestion`}>
|
||||
<Undo2 />
|
||||
</Button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -16,8 +16,9 @@ after you start it.
|
||||
Two models do the work, because they are good at different things:
|
||||
|
||||
- **[Jev](https://openrouter.ai/docs/guides/community/jev)** (`typesafe/jev-1.13`,
|
||||
TypeSafe's decision model on OpenRouter) answers typed `choice` questions:
|
||||
task type, assignee, project, and work mode. It returns a choice, a
|
||||
TypeSafe's decision model on OpenRouter) answers typed `choice` questions for
|
||||
the task's **mode**, which is its type (Auto, Plan, or Ask), plus its
|
||||
**assignee** and **project**. It returns a choice, a
|
||||
confidence, and a probability for every option. It is fast and cheap: you pay
|
||||
for input tokens only, and output tokens are free. It does not generate text or
|
||||
explain its answers.
|
||||
@@ -28,8 +29,12 @@ Two models do the work, because they are good at different things:
|
||||
|
||||
- The **title** types into the header breadcrumb (`PAP › Fix login redirect
|
||||
loop…`) when it arrives. Click it to edit.
|
||||
- **Type**, **assignee**, **project**, and **mode** appear as chips under the
|
||||
prompt, usually before the title, because Jev answers faster.
|
||||
- **Mode**, **assignee**, and **project** appear as chips under the prompt,
|
||||
usually before the title, because Jev answers faster. Mode is the task's type
|
||||
and uses the shipped `IssueWorkMode` values and labels:
|
||||
- **Auto** (`standard`): the agent does the work.
|
||||
- **Plan** (`planning`): the agent writes a plan for review first.
|
||||
- **Ask** (`ask`): the agent answers without changing anything.
|
||||
- Suggested values carry a sparkle. When you change a value, the sparkle goes
|
||||
away and the value is never overwritten. The undo icon restores the suggestion.
|
||||
- Every chip menu shows Jev's probability for each option.
|
||||
@@ -83,7 +88,5 @@ too (`draftTitle`). No API is called and no task is persisted.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does task **type** need to be a real field? Today `IssueWorkMode` and labels
|
||||
cover part of it.
|
||||
- Should Jev's criteria for each agent come from agent instructions or role
|
||||
metadata, rather than a hand-written "owns" line?
|
||||
@@ -8,7 +8,8 @@ import type { IssueWorkMode } from "@paperclipai/shared";
|
||||
* does not generate text or explain itself. So routing is split in two:
|
||||
*
|
||||
* - Jev (`POST /api/alpha/decisions`, model `typesafe/jev-1.13`) answers `choice`
|
||||
* questions for task type, assignee, project, and work mode.
|
||||
* questions for the task's mode (its type: Auto, Plan, or Ask), assignee, and
|
||||
* project.
|
||||
* - A small text model drafts the title, the way Claude Code names a session.
|
||||
*
|
||||
* `buildJevDecisionRequest` produces the real request body. The response is
|
||||
@@ -48,17 +49,6 @@ export type JevDecisionResponse = {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Company fixtures (match the Storybook company)
|
||||
|
||||
export type JevTaskType = "bug" | "feature" | "research" | "design" | "qa" | "ops";
|
||||
|
||||
export const JEV_TASK_TYPES: { value: JevTaskType; label: string; hint: string }[] = [
|
||||
{ value: "bug", label: "Bug", hint: "Something is broken or regressed" },
|
||||
{ value: "feature", label: "Feature", hint: "Build or change product behavior" },
|
||||
{ value: "research", label: "Research", hint: "Investigate, compare, or answer a question" },
|
||||
{ value: "design", label: "Design", hint: "UI, UX, or visual work" },
|
||||
{ value: "qa", label: "QA", hint: "Verify, test, or reproduce" },
|
||||
{ value: "ops", label: "Ops", hint: "Admin, release, or coordination work" },
|
||||
];
|
||||
|
||||
export type JevAgent = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -103,10 +93,14 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV
|
||||
projects: JEV_PROJECTS.map(({ id, name, description }) => ({ id, name, description })),
|
||||
},
|
||||
questions: {
|
||||
task_type: {
|
||||
work_mode: {
|
||||
type: "choice",
|
||||
instructions: "What kind of work does the task prompt describe?",
|
||||
criteria: Object.fromEntries(JEV_TASK_TYPES.map((option) => [option.value, option.hint])),
|
||||
instructions: "What type of task is this: should the agent do the work, plan it first, or answer a question?",
|
||||
criteria: {
|
||||
standard: "Auto: a clear, bounded request the agent can carry out directly.",
|
||||
planning: "Plan: large, risky, or multi-step work that needs a reviewed plan before anything changes.",
|
||||
ask: "Ask: a question or investigation to answer without changing anything.",
|
||||
},
|
||||
},
|
||||
assignee: {
|
||||
type: "choice",
|
||||
@@ -121,23 +115,17 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV
|
||||
[JEV_NO_PROJECT]: "None of the projects fit.",
|
||||
},
|
||||
},
|
||||
work_mode: {
|
||||
type: "choice",
|
||||
instructions: "How should the assignee approach this task?",
|
||||
criteria: {
|
||||
standard: "Do the work directly.",
|
||||
planning: "Large or risky scope: write a plan for review before doing the work.",
|
||||
ask: "A question: answer it without changing anything.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local simulation of Jev's answers
|
||||
// Local simulation of Jev's answers. Work areas are internal signals for
|
||||
// picking an owner; they are not shown or stored.
|
||||
|
||||
const TYPE_KEYWORDS: Record<JevTaskType, string[]> = {
|
||||
type WorkArea = "bug" | "feature" | "research" | "design" | "qa" | "ops";
|
||||
|
||||
const AREA_KEYWORDS: Record<WorkArea, string[]> = {
|
||||
bug: ["bug", "broken", "fix", "crash", "error", "regress", "fails", "failing", "500", "stuck", "doesn't", "does not", "not working", "wrong"],
|
||||
feature: ["add", "build", "implement", "support", "create", "new", "allow", "enable", "endpoint", "integrate", "make", "improve", "faster", "performance"],
|
||||
research: ["research", "investigate", "compare", "evaluate", "why", "how does", "figure out", "explore", "options", "should we"],
|
||||
@@ -146,8 +134,8 @@ const TYPE_KEYWORDS: Record<JevTaskType, string[]> = {
|
||||
ops: ["release", "deploy", "rotate", "invoice", "schedule", "onboard", "hire", "budget report", "coordinate", "follow up", "email"],
|
||||
};
|
||||
|
||||
/** Which agent usually owns each type. Assignee probabilities follow type probabilities. */
|
||||
const TYPE_OWNER: Record<JevTaskType, string> = {
|
||||
/** Which agent usually owns each work area. */
|
||||
const AREA_OWNER: Record<WorkArea, string> = {
|
||||
bug: "agent-codex",
|
||||
feature: "agent-codex",
|
||||
research: "agent-cto",
|
||||
@@ -191,17 +179,17 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe
|
||||
const prompt = String(request.state.task_prompt ?? "");
|
||||
const text = prompt.toLowerCase();
|
||||
|
||||
const typeScores = Object.fromEntries(
|
||||
(Object.keys(TYPE_KEYWORDS) as JevTaskType[]).map((type) => [type, score(text, TYPE_KEYWORDS[type])]),
|
||||
) as Record<JevTaskType, number>;
|
||||
const areaScores = Object.fromEntries(
|
||||
(Object.keys(AREA_KEYWORDS) as WorkArea[]).map((area) => [area, score(text, AREA_KEYWORDS[area])]),
|
||||
) as Record<WorkArea, number>;
|
||||
// No signal at all reads as an open question.
|
||||
if (Object.values(typeScores).every((value) => value === 0)) typeScores.research = 0.6;
|
||||
const typeProbabilities = softmax(typeScores);
|
||||
if (Object.values(areaScores).every((value) => value === 0)) areaScores.research = 0.6;
|
||||
const areaProbabilities = softmax(areaScores);
|
||||
|
||||
const assigneeQuestion = request.questions.assignee as JevChoiceQuestion;
|
||||
const assigneeProbabilities: Record<string, number> = Object.fromEntries(Object.keys(assigneeQuestion.criteria).map((id) => [id, 0.02]));
|
||||
for (const [type, probability] of Object.entries(typeProbabilities)) {
|
||||
const owner = TYPE_OWNER[type as JevTaskType];
|
||||
for (const [area, probability] of Object.entries(areaProbabilities)) {
|
||||
const owner = AREA_OWNER[area as WorkArea];
|
||||
if (owner in assigneeProbabilities) assigneeProbabilities[owner]! += probability;
|
||||
}
|
||||
const assigneeTotal = Object.values(assigneeProbabilities).reduce((sum, value) => sum + value, 0);
|
||||
@@ -214,16 +202,16 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe
|
||||
|
||||
const isQuestion = /\?\s*$/.test(prompt.trim()) || (/^(how|why|what|which|should|is|are|can|does)\b/.test(text) && !/^(can|could|would) you\b/.test(text));
|
||||
const isLarge = /\b(plan|roadmap|migrate|migration|redesign|rewrite|overhaul|multi-step|phases?)\b/.test(text);
|
||||
const modeScores = { standard: 1, planning: isLarge ? 3 : 0, ask: isQuestion ? 3 : 0 };
|
||||
const isInvestigation = /^(investigate|research|find out|figure out|look into|compare|evaluate)\b/.test(text);
|
||||
const modeScores = { standard: 1, planning: isLarge ? 3 : 0, ask: isQuestion ? 3 : isInvestigation ? 1.6 : 0 };
|
||||
|
||||
const inputTokens = 380 + Math.round(prompt.length / 4);
|
||||
return {
|
||||
model: request.model,
|
||||
answers: {
|
||||
task_type: choiceAnswer(typeProbabilities),
|
||||
work_mode: choiceAnswer(softmax(modeScores)),
|
||||
assignee: choiceAnswer(assigneeProbabilities),
|
||||
project: choiceAnswer(softmax(projectScores, 2.4)),
|
||||
work_mode: choiceAnswer(softmax(modeScores)),
|
||||
},
|
||||
// Output tokens are free on Jev; $0.042 per million input tokens.
|
||||
usage: { input_tokens: inputTokens, output_tokens: 0, cost: (inputTokens * 0.042) / 1_000_000 },
|
||||
@@ -233,7 +221,7 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe
|
||||
// ---------------------------------------------------------------------------
|
||||
// What the composer consumes
|
||||
|
||||
export type JevField = "type" | "assignee" | "project" | "workMode";
|
||||
export type JevField = "workMode" | "assignee" | "project";
|
||||
|
||||
/**
|
||||
* Who gets a task when Jev isn't confident about the owner: the org's first
|
||||
@@ -245,14 +233,14 @@ export function fallbackAssigneeId(agents: JevAgent[]): string | null {
|
||||
}
|
||||
|
||||
export type JevRouting = {
|
||||
type: JevTaskType;
|
||||
/** The task's type. */
|
||||
workMode: IssueWorkMode;
|
||||
/** The owner to use: Jev's pick, or the fallback when Jev is unsure. */
|
||||
assigneeId: string;
|
||||
assigneeSource: "jev" | "fallback";
|
||||
/** Jev's own top pick, kept even when the fallback is used. */
|
||||
jevAssigneeId: string;
|
||||
projectId: string | null;
|
||||
workMode: IssueWorkMode;
|
||||
confidence: Record<JevField, number>;
|
||||
probabilities: Record<JevField, Record<string, number>>;
|
||||
/** Jev's likely owners other than the one assigned, offered when Jev is unsure. */
|
||||
@@ -261,7 +249,7 @@ export type JevRouting = {
|
||||
};
|
||||
|
||||
export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[] = JEV_AGENTS): JevRouting {
|
||||
const { task_type: type, assignee, project, work_mode: mode } = response.answers as Record<string, JevChoiceAnswer>;
|
||||
const { work_mode: mode, assignee, project } = response.answers as Record<string, JevChoiceAnswer>;
|
||||
const fallbackId = assignee!.confidence < JEV_CONFIDENCE_THRESHOLD ? fallbackAssigneeId(agents) : null;
|
||||
const assigneeId = fallbackId ?? assignee!.choice;
|
||||
const alternateAssigneeIds = Object.entries(assignee!.probabilities)
|
||||
@@ -269,14 +257,13 @@ export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[]
|
||||
.slice(0, 2)
|
||||
.map(([id]) => id);
|
||||
return {
|
||||
type: type!.choice as JevTaskType,
|
||||
workMode: mode!.choice as IssueWorkMode,
|
||||
assigneeId,
|
||||
assigneeSource: fallbackId ? "fallback" : "jev",
|
||||
jevAssigneeId: assignee!.choice,
|
||||
projectId: project!.choice === JEV_NO_PROJECT ? null : project!.choice,
|
||||
workMode: mode!.choice as IssueWorkMode,
|
||||
confidence: { type: type!.confidence, assignee: assignee!.confidence, project: project!.confidence, workMode: mode!.confidence },
|
||||
probabilities: { type: type!.probabilities, assignee: assignee!.probabilities, project: project!.probabilities, workMode: mode!.probabilities },
|
||||
confidence: { workMode: mode!.confidence, assignee: assignee!.confidence, project: project!.confidence },
|
||||
probabilities: { workMode: mode!.probabilities, assignee: assignee!.probabilities, project: project!.probabilities },
|
||||
alternateAssigneeIds,
|
||||
usage: response.usage,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const meta = {
|
||||
description: {
|
||||
component:
|
||||
"Prototype: prompt-first task creation. You write what you need. Jev (`typesafe/jev-1.13`, TypeSafe's decision model on OpenRouter) answers " +
|
||||
"`choice` questions for task type, assignee, project, and work mode, with a confidence and per-option probabilities. Jev doesn't generate text, so a separate small text model drafts the title, like Claude Code naming a session. " +
|
||||
"`choice` questions for the task's mode, which is its type (Auto, Plan, or Ask), plus its assignee and project, each with a confidence and per-option probabilities. Jev doesn't generate text, so a separate small text model drafts the title, like Claude Code naming a session. " +
|
||||
"Suggested values carry a sparkle. Anything you change is yours and is never overwritten; the undo icon restores the suggestion. " +
|
||||
"Below the confidence threshold the task goes to the org's first active agent that reports to the board (else its first active agent), and Jev's best guesses appear as one-click buttons. **Instant** stories create first; the title and routing arrive a moment later. " +
|
||||
"Jev's answers are simulated locally in the documented response shape (`prototypes/jev-task-creation/jev-classifier.ts`); **How sure Jev is → Request sent to Jev** shows the real request body. No API is called and no task is persisted.",
|
||||
@@ -44,12 +44,12 @@ export const WatchItSuggest: Story = {
|
||||
name: "02 · Watch Jev suggest while you type",
|
||||
args: { initialPrompt: PROMPTS.bug, typeOnMount: true },
|
||||
};
|
||||
export const Bug: Story = { name: "03 · Bug → CodexCoder", args: { initialPrompt: PROMPTS.bug } };
|
||||
export const Feature: Story = { name: "04 · Feature → CodexCoder", args: { initialPrompt: PROMPTS.feature } };
|
||||
export const Research: Story = { name: "05 · Question → CTO in Ask mode", args: { initialPrompt: PROMPTS.research } };
|
||||
export const Design: Story = { name: "06 · Design → DesignSystemCoder", args: { initialPrompt: PROMPTS.design } };
|
||||
export const QA: Story = { name: "07 · QA → QAChecker", args: { initialPrompt: PROMPTS.qa } };
|
||||
export const LargeScope: Story = { name: "08 · Large scope → Plan mode", args: { initialPrompt: PROMPTS.planning } };
|
||||
export const Bug: Story = { name: "03 · Login bug → Auto mode, CodexCoder", args: { initialPrompt: PROMPTS.bug } };
|
||||
export const Feature: Story = { name: "04 · CSV export → Auto mode, CodexCoder", args: { initialPrompt: PROMPTS.feature } };
|
||||
export const Research: Story = { name: "05 · Question → Ask mode, CTO", args: { initialPrompt: PROMPTS.research } };
|
||||
export const Design: Story = { name: "06 · Spacing pass → Auto mode, DesignSystemCoder", args: { initialPrompt: PROMPTS.design } };
|
||||
export const QA: Story = { name: "07 · Release checks → Auto mode, QAChecker", args: { initialPrompt: PROMPTS.qa } };
|
||||
export const LargeScope: Story = { name: "08 · Phased migration → Plan mode, CodexCoder", args: { initialPrompt: PROMPTS.planning } };
|
||||
export const LowConfidence: Story = {
|
||||
name: "09 · Unsure · Goes to the agent reporting to the board",
|
||||
args: { initialPrompt: PROMPTS.ambiguous },
|
||||
@@ -78,6 +78,14 @@ export const AssigneeProbabilities: Story = {
|
||||
await userEvent.click(chip);
|
||||
},
|
||||
};
|
||||
export const ModeProbabilities: Story = {
|
||||
name: "11c · Mode menu shows Jev's probabilities",
|
||||
args: { initialPrompt: PROMPTS.planning },
|
||||
play: async ({ canvasElement }) => {
|
||||
const chip = await within(canvasElement).findByRole("button", { name: /^Mode/ }, { timeout: 4000 });
|
||||
await userEvent.click(chip);
|
||||
},
|
||||
};
|
||||
export const InstantEmpty: Story = {
|
||||
name: "12 · Instant · Start first, name and route after",
|
||||
args: { flow: "instant" },
|
||||
|
||||
Reference in new issue
Block a user