Make Piggy part of the product rather than a guest in it
Piggy arrived as a chat panel bolted onto a CRM and then grew a workspace around it. The layout was already right — the audit found the approval card to be the best-designed object in the repo, and the account page's empty panels less finished than anything in the workspace. What was wrong was vocabulary: nobody had written the small things down, so both halves kept inventing them. Piggy was drawn with five different marks — a pig in the dock, a sparkle in the sidebar and again on the model picker, a speech bubble on the Ask buttons, and a stock robot glyph on every assistant message, which is the one people look at most. There is now one mark. The composer, which is the first control in the product since sign-in lands on /piggy, was the only un-adapted shadcn field left: 6px radius against a 12px Send button it sat 8px from. A stat tile had been reinvented six times at three numeral scales, and the same uppercase micro-label existed in five variants, two of them one tab apart in the same rail. There were 63 hand-written font sizes: not a scale, sixty-three opinions. Underneath that, the focus ring was invisible. The global rule used ring-accent, which Tailwind deliberately aliases onto the hover tint, so the ring measured 1.01:1 against the light canvas — no visible focus indicator anywhere in the product, for any accent, in either theme. It is ring-brand now and measures 17:1. The warning, positive and info tones were darkened until each clears 4.5:1 on a card, on inset and on its own chip, and the light canvas moved to 98% so a card lifts without leaning on its shadow. The mobile work is the part worth reading. A landscape phone gave the transcript 28% of the viewport and a keyboard-up phone 16%, against a 45% floor — and the fixed tab bar painted over the composer, covering the safety sentence and half the Send button, because two source comments asserted the bar stood down on short viewports and it never had. Both fixed and measured by hit-testing rather than by screenshot. The composer itself was 64px tall for a blank second line nobody typed, because the auto-resize effect sizes to scrollHeight and scrollHeight counts rows — a CSS height could not win against an inline style, so the attribute was the honest lever. Verified across both themes driven through the app's own control: no horizontal overflow on 15 routes at four viewports, 672 stat values that fit, 297 labels at exactly 11px/500, Escape returning focus to its opener rather than the body on every overlay, and a rejected write no longer reporting "Succeeded" with a green check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
* link is resolved only against conversations the caller owns.
|
||||
*/
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, sql } from 'drizzle-orm';
|
||||
import type { AgentTaskKind, AgentTaskOutcome } from '@pig/core';
|
||||
import { PIGGY_MODES, type AgentTaskKind, type AgentTaskOutcome, type PiggyMode } from '@pig/core';
|
||||
import type { Database } from '@pig/db';
|
||||
import { agentRuns, agentTasks, piggyConversations, users } from '@pig/db';
|
||||
import type { Principal } from '../lib/auth';
|
||||
@@ -63,6 +63,21 @@ export interface PiggyRunSummary {
|
||||
*/
|
||||
status: string;
|
||||
model: string | null;
|
||||
/**
|
||||
* What this turn was allowed to do — the whole safety argument, per row.
|
||||
*
|
||||
* PIG's claim is that nothing lands until a person presses Apply, and that
|
||||
* claim is only auditable if the ledger records which turns were even offered
|
||||
* write tools. Without it a run that quietly applied five changes under `auto`
|
||||
* is indistinguishable from one that could not have changed a thing.
|
||||
*
|
||||
* Null means the mode was not recorded, which is two real cases and not a
|
||||
* failure: a queued task run, which has no mode because nobody chose one, and
|
||||
* a chat turn from before the relay started stamping it. Reported as null
|
||||
* rather than defaulted to `read_only`, because guessing the safe answer on an
|
||||
* audit surface is the one direction a wrong guess must never go.
|
||||
*/
|
||||
mode: PiggyMode | null;
|
||||
/** The question, for a chat turn; the queued work, for a task run. */
|
||||
label: string;
|
||||
/** The first line of what Piggy answered. Null on a turn that said nothing. */
|
||||
@@ -158,6 +173,23 @@ function readString(bag: Record<string, unknown> | null, key: string): string |
|
||||
return typeof value === 'string' && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode a chat turn ran in, from the run's `input` blob.
|
||||
*
|
||||
* `agent_runs` has no `mode` column; the chat relay writes it into `input`
|
||||
* alongside the message and the conversation id (`chat-server.ts`,
|
||||
* `startChatRun`). That is a free-text bag, so the value is checked against the
|
||||
* ontology rather than cast — a run whose blob says `"mode": "yolo"` must report
|
||||
* no mode at all, not put an invented one in the ledger.
|
||||
*
|
||||
* Exported for the test, which is the only way to exercise a blob the relay
|
||||
* would never write without standing up a database to hold it.
|
||||
*/
|
||||
export function runMode(input: Record<string, unknown> | null): PiggyMode | null {
|
||||
const claimed = readString(input, 'mode');
|
||||
return PIGGY_MODES.find((mode) => mode === claimed) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The conversation a run answered.
|
||||
*
|
||||
@@ -251,6 +283,7 @@ export class PiggyActivityService {
|
||||
agent: row.agent,
|
||||
status: row.status,
|
||||
model: row.model,
|
||||
mode: runMode(row.input),
|
||||
// A run with neither a question nor a task kind is a row written before
|
||||
// the turn got anywhere; naming it after its status beats an empty cell.
|
||||
label:
|
||||
|
||||
Reference in New Issue
Block a user