Make Reginald posh + remove em-dashes from user-facing copy
Voice: rewrote REGINALD_SYSTEM and CAPTURE_SYSTEM (core) to an elevated,
dry butler register, and re-voiced his visible lines — chat greetings, the
approve/dismiss/error replies, the standup closer ('The kettle is on. Yours,
Reginald.'), the onboarding welcome, and the capture prose. Both system prompts
now also instruct him never to use an em dash.
Em-dashes: swept every user-facing string in the renderer free of em-dashes
(punctuation only, comments left untouched) via a per-file pass, plus the core
tool descriptions and the memory focus-slot placeholder ('· ' not '— '). Bare
'—' value placeholders became middots ('·'). No em-dash now renders anywhere
in the app or in Reginald's own output.
core 169 tests green (updated the memory placeholder assertion) · core + desktop
tsc clean · verified visually (posh greeting + standup closer render).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -189,9 +189,9 @@ export function CalibrationScreen({ onBack, data }: { onBack: () => void; data?:
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||||
{c.active
|
||||
? 'You are not bad at estimating; you are optimistic in a very stable way. Stable, I can work with.'
|
||||
: 'Not enough closed history yet — I’m forecasting from cold-start priors and widening the cone to stay honest. The curve takes over at 20.'}
|
||||
: 'Not enough closed history yet: I’m forecasting from cold-start priors and widening the cone to stay honest. The curve takes over at 20.'}
|
||||
{!c.active && c.excludedSameDay > 0
|
||||
? ` And ${c.excludedSameDay} closed ${c.excludedSameDay === 1 ? 'issue' : 'issues'} closed the same day they were started — 0 working days can’t calibrate, so they don’t count toward the 20.`
|
||||
? ` And ${c.excludedSameDay} closed ${c.excludedSameDay === 1 ? 'issue' : 'issues'} closed the same day they were started. 0 working days can’t calibrate, so they don’t count toward the 20.`
|
||||
: ''}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -70,10 +70,10 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
setLiveTickets(res.issues)
|
||||
setStage('review')
|
||||
} else {
|
||||
setError(res.ok ? 'I could not find concrete work in that — try a bit more detail.' : `Capture failed: ${res.reason === 'error' ? res.message : 'no model'}`)
|
||||
setError(res.ok ? 'I could not find concrete work in that; try a bit more detail.' : `Capture failed: ${res.reason === 'error' ? res.message : 'no model'}`)
|
||||
}
|
||||
})
|
||||
.catch(() => { setBrewing(false); setError('I could not reach the model.') })
|
||||
.catch(() => { setBrewing(false); setError('I was unable to reach the model, regrettably.') })
|
||||
}
|
||||
|
||||
const fileLive = () => {
|
||||
@@ -84,7 +84,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
.then((res) => {
|
||||
setBrewing(false)
|
||||
if (res.ok) { setFiledCount(res.created.length); setStage('filed') }
|
||||
else setError('Filing failed — gitea is not configured.')
|
||||
else setError('Filing failed; gitea is not configured.')
|
||||
})
|
||||
.catch(() => { setBrewing(false); setError('Filing failed.') })
|
||||
}
|
||||
@@ -99,12 +99,12 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
|
||||
const QUESTIONS: Question[] = [
|
||||
{
|
||||
q: 'The auth work — one ticket, or shall I split token refresh from session storage? They fail differently.',
|
||||
q: 'The auth work: one ticket, or shall I split token refresh from session storage? They fail differently.',
|
||||
chips: ['One ticket', 'Split them'],
|
||||
set: (a) => setSplit(a === 'Split them'),
|
||||
},
|
||||
{
|
||||
q: 'The webhook double-fire — how long? I should mention your "quick" has averaged two days.',
|
||||
q: 'The webhook double-fire, how long? I should mention your "quick" has averaged two days.',
|
||||
chips: ['est/1d', 'est/2d', 'est/3d'],
|
||||
set: (a) => setWebEst(a),
|
||||
},
|
||||
@@ -205,7 +205,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
</header>
|
||||
|
||||
{stage === 'dump' ? (
|
||||
<Card overline="Braindump" title="Tell me what you're planning" jade>
|
||||
<Card overline="Braindump" title="Do tell me what you are planning" jade>
|
||||
<textarea
|
||||
value={dump}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDump(e.target.value)}
|
||||
@@ -217,7 +217,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
}}
|
||||
></textarea>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 14px' }}>
|
||||
Sentences, fragments, grievances — all welcome. I'll sort it into tickets and only ask what I can't infer.
|
||||
Sentences, fragments, grievances; all are welcome. I shall sort them into tickets and enquire only where I cannot infer.
|
||||
</p>
|
||||
<Button icon="sparkles" onClick={() => void onBrew()} disabled={brewing}>
|
||||
{brewing ? 'Brewing…' : 'Brew tickets'}
|
||||
@@ -262,7 +262,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
{live ? (
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
{tickets.length} issue{tickets.length === 1 ? '' : 's'} from your braindump, estimated and prioritized.
|
||||
Adjust the labels, then approve — I'll open them in gitea with only est/* and p/* labels.
|
||||
Adjust the labels, then approve; I'll open them in gitea with only est/* and p/* labels.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -286,10 +286,10 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
<Icon name="circle-check" size={22} /> Filed
|
||||
</span>
|
||||
<p style={{ font: 'var(--text-body)', margin: 0 }}>
|
||||
{live ? filedCount : tickets.length} issues opened in gitea with <span style={{ font: 'var(--text-data)' }}>est/*</span> and <span style={{ font: 'var(--text-data)' }}>p/*</span> labels — nothing else touched.
|
||||
{live ? filedCount : tickets.length} issues opened in gitea with <span style={{ font: 'var(--text-data)' }}>est/*</span> and <span style={{ font: 'var(--text-data)' }}>p/*</span> labels, nothing else touched.
|
||||
</p>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
Elapsed {clock} — under budget. No bot comments, no synthetic issues; your repo remains yours.
|
||||
Elapsed {clock}, under budget. No bot comments, no synthetic issues; your repo remains yours.
|
||||
</p>
|
||||
<Button iconRight="arrow-right" onClick={onDone}>To morning service</Button>
|
||||
</div>
|
||||
|
||||
@@ -28,9 +28,9 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
|
||||
const mapError = (e?: string) =>
|
||||
e === '401' || e === '403'
|
||||
? 'The token was rejected — check it has repo + issue scopes.'
|
||||
? 'The token was rejected, check it has repo + issue scopes.'
|
||||
: e === '404'
|
||||
? "Couldn't reach that Gitea — check the URL."
|
||||
? "Couldn't reach that Gitea, check the URL."
|
||||
: `Discovery failed${e ? ` (${e})` : ''}.`
|
||||
|
||||
// Discover the owners + repos this token can see. Triggered when the token
|
||||
@@ -114,7 +114,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
label="Access token"
|
||||
type="password"
|
||||
value={token}
|
||||
placeholder={existing?.hasToken ? '•••••••• (saved — enter a new one to change repos)' : 'gitea PAT · scopes: repo + issue'}
|
||||
placeholder={existing?.hasToken ? '•••••••• (saved, enter a new one to change repos)' : 'gitea PAT · scopes: repo + issue'}
|
||||
hint={
|
||||
discovery === 'discovering'
|
||||
? 'Discovering your repositories…'
|
||||
@@ -157,7 +157,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
value={repo}
|
||||
disabled={!pickerReady || repoOptions.length === 0}
|
||||
options={
|
||||
repoOptions.length ? repoOptions.map((r) => ({ value: r, label: r })) : placeholder('—')
|
||||
repoOptions.length ? repoOptions.map((r) => ({ value: r, label: r })) : placeholder('·')
|
||||
}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
@@ -167,7 +167,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
label="Model URL"
|
||||
value={modelUrl}
|
||||
placeholder="http://localhost:1234/v1"
|
||||
hint="Optional — an OpenAI-compatible endpoint for Reginald. Leave blank to keep chat off."
|
||||
hint="Optional: an OpenAI-compatible endpoint for Reginald. Leave blank to keep chat off."
|
||||
onChange={(e) => setModelUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ export function DirectivesScreen() {
|
||||
<EmptyState
|
||||
icon="flag"
|
||||
title="No directives yet"
|
||||
line="When you overrule the scheduler in chat, it goes on the record here — who, when, what, why."
|
||||
line="When you overrule the scheduler in chat, it goes on the record here: who, when, what, why."
|
||||
/>
|
||||
) : (
|
||||
<Card overline="The ledger" flush>
|
||||
@@ -117,7 +117,7 @@ export function DirectivesScreen() {
|
||||
)}
|
||||
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
Entries are never edited. Corrections are new entries — the ledger remembers everything, politely.
|
||||
Entries are never edited. Corrections are new entries; the ledger remembers everything, politely.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ export function FocusScreen({
|
||||
<EmptyState
|
||||
icon="coffee"
|
||||
title="Nothing to pour"
|
||||
line="Capture some work, or enjoy the silence — it never lasts."
|
||||
line="Capture some work, or enjoy the silence, it never lasts."
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export function FocusScreen({
|
||||
<BurnUpCone data={forecast.cone} />
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||||
{forecast.coldStart
|
||||
? `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope. Cold-start priors — ${forecast.calibratedN}/20 estimated closes so far; the cone tightens as the team closes work.`
|
||||
? `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope. Cold-start priors: ${forecast.calibratedN}/20 estimated closes so far; the cone tightens as the team closes work.`
|
||||
: `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope, calibrated on ${forecast.calibratedN} closed ${forecast.calibratedN === 1 ? 'issue' : 'issues'} of your own.`}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { type IssueDetail, type IssueRef } from '../../data/view-types.js'
|
||||
import { Badge, Button, Card, Dialog, Icon, Select, Tag } from '../ui/index.js'
|
||||
|
||||
const NONE = '—'
|
||||
const NONE = '·'
|
||||
|
||||
export function IssueScreen({
|
||||
issue,
|
||||
@@ -214,7 +214,7 @@ export function IssueScreen({
|
||||
<div style={{ marginTop: 14, minHeight: 40 }}>
|
||||
{pendingChanges.length === 0 ? (
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
No change yet — pick a different estimate, priority, assignee, or milestone.
|
||||
No change yet, pick a different estimate, priority, assignee, or milestone.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -265,7 +265,7 @@ export function IssueScreen({
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, padding: '14px 20px', alignItems: 'flex-end' }}>
|
||||
<textarea placeholder="Comment — this writes to gitea, as you" rows={2} style={{
|
||||
<textarea placeholder="Comment: this writes to gitea, as you" rows={2} style={{
|
||||
flex: 1, resize: 'none', font: 'var(--text-body)', color: 'var(--ink-1)', lineHeight: 1.5,
|
||||
background: 'var(--paper-0)', border: '1px solid var(--line-2)', borderRadius: 'var(--radius-2)',
|
||||
padding: '8px 11px', outline: 'none',
|
||||
|
||||
@@ -30,7 +30,7 @@ export function MilestoneScreen({
|
||||
|
||||
const groups = data.groups.map((g) => ({ label: g.label, issues: g.issues, muted: g.label === 'Done' }))
|
||||
const name = data.name
|
||||
const dueLine = `milestone · due ${data.due} · ${data.soft ? 'soft — scope may flex' : 'hard deadline'}`
|
||||
const dueLine = `milestone · due ${data.due} · ${data.soft ? 'soft, scope may flex' : 'hard deadline'}`
|
||||
const forecastLabel = data.forecastRange ? `80% ${data.forecastRange}` : 'all shipped'
|
||||
const scopeStat = `${data.scopeCount} issues · est ${data.scopeEstDays}d`
|
||||
const doneStat = `${data.doneCount} · ${data.donePct}%`
|
||||
|
||||
@@ -43,9 +43,9 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
setFound(null)
|
||||
setError(
|
||||
res.error === '401' || res.error === '403'
|
||||
? 'The token was rejected — check it has repo + issue scopes.'
|
||||
? 'The token was rejected: check it has repo + issue scopes.'
|
||||
: res.error === '404'
|
||||
? "Couldn't reach that Gitea — check the URL."
|
||||
? "Couldn't reach that Gitea; check the URL."
|
||||
: `Connection failed${res.error ? ` (${res.error})` : ''}.`,
|
||||
)
|
||||
return
|
||||
@@ -66,7 +66,7 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
.catch(() => ({ ok: false as const, error: 'unreachable' }))
|
||||
if (!res.ok) {
|
||||
setBoot('idle')
|
||||
setError(`Bootstrap failed${res.error ? ` (${res.error})` : ''}. Nothing was half-applied — you can retry.`)
|
||||
setError(`Bootstrap failed${res.error ? ` (${res.error})` : ''}. Nothing was half-applied, you can retry.`)
|
||||
return
|
||||
}
|
||||
// persist the connection now that the repo is prepared
|
||||
@@ -155,11 +155,11 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
<Frame footer={<Button iconRight="arrow-right" onClick={() => setStep(1)}>Begin</Button>}>
|
||||
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>Good morning.</h1>
|
||||
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
I'm Reginald, your project manager. I interview you instead of making you fill in forms, I forecast in
|
||||
honest ranges, and I never do the arithmetic myself — there's a scheduler for that.
|
||||
I am Reginald, your project manager. I interview you rather than trouble you with forms, I forecast in
|
||||
honest ranges, and I never attempt the arithmetic myself; there is a scheduler for that.
|
||||
</p>
|
||||
<p style={{ font: 'var(--text-body)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
Your plans live in your own Gitea as ordinary issues and labels. Delete me and nothing human is lost.
|
||||
Your plans reside in your own Gitea as ordinary issues and labels. Dismiss me, and nothing human is lost.
|
||||
</p>
|
||||
</Frame>
|
||||
) : null}
|
||||
@@ -353,7 +353,7 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
No bot comments, no body frontmatter, no synthetic issues — ever. Labels are the only footprint.
|
||||
No bot comments, no body frontmatter, no synthetic issues, ever. Labels are the only footprint.
|
||||
</p>
|
||||
</Frame>
|
||||
) : null}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function SettingsScreen({
|
||||
<Icon name="layers" size={12} />
|
||||
sidecar: {connection.pmStateRepo ?? `${connection.repo}-pm-state`}
|
||||
</Row>
|
||||
<Note>Your token is stored encrypted on this machine. Delete the sidecar and resync — no truth is lost.</Note>
|
||||
<Note>Your token is stored encrypted on this machine. Delete the sidecar and resync; no truth is lost.</Note>
|
||||
<Row style={{ gap: 8, marginTop: 2 }}>
|
||||
<Button variant="secondary" size="sm" icon="settings-2" onClick={onReconnect}>
|
||||
Reconfigure
|
||||
@@ -106,7 +106,7 @@ export function SettingsScreen({
|
||||
</span>
|
||||
</Row>
|
||||
<Note>
|
||||
The board is a rebuildable index over gitea — the durable truth. When gitea is unreachable the last
|
||||
The board is a rebuildable index over gitea, the durable truth. When gitea is unreachable the last
|
||||
snapshot is served from cache (the rail's dot turns red), and the next successful reconcile supersedes it.
|
||||
No webhooks, no polling: reads are on demand.
|
||||
</Note>
|
||||
@@ -127,7 +127,7 @@ export function SettingsScreen({
|
||||
<Row style={{ gap: 8 }}>
|
||||
<Icon name="sparkles" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||
<span style={{ font: 'var(--text-body)', color: 'var(--ink-2)', flex: 1 }}>
|
||||
Chat is off — no model reachable.
|
||||
Chat is off: no model reachable.
|
||||
</span>
|
||||
<Button variant="secondary" size="sm" onClick={onReconnect}>
|
||||
Set a Model URL
|
||||
@@ -136,7 +136,7 @@ export function SettingsScreen({
|
||||
)}
|
||||
<Note>
|
||||
An OpenAI-compatible endpoint (set it in Reconfigure). Reginald names, negotiates and explains; it never
|
||||
does the arithmetic — the scheduler and Monte Carlo do that.
|
||||
does the arithmetic; the scheduler and Monte Carlo do that.
|
||||
</Note>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -154,7 +154,7 @@ export function SettingsScreen({
|
||||
))}
|
||||
<Tag label="deadline/hard" />
|
||||
</Row>
|
||||
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable — that is rather the point.</Note>
|
||||
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable: that is rather the point.</Note>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export function StandupScreen({
|
||||
<EmptyState
|
||||
icon="sun"
|
||||
title="Nothing to report"
|
||||
line="No overnight drift — I'll have a plan when there's work to pour."
|
||||
line="No overnight drift. I'll have a plan when there's work to pour."
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -228,7 +228,7 @@ export function StandupScreen({
|
||||
}}
|
||||
>
|
||||
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-1)', margin: 0, flex: 1 }}>
|
||||
The kettle’s on. — R.
|
||||
The kettle is on. Yours, Reginald.
|
||||
</p>
|
||||
<Button variant="ghost" onClick={onBegin}>
|
||||
Ask about the drift
|
||||
|
||||
@@ -399,7 +399,7 @@ export function AppShell() {
|
||||
backlog.status === 'loading'
|
||||
? `Connecting to ${hostLabel}…`
|
||||
: offline
|
||||
? `Offline — reading from cache${staleSince ? ` (since ${new Date(staleSince).toLocaleString()})` : ''}`
|
||||
? `Offline, reading from cache${staleSince ? ` (since ${new Date(staleSince).toLocaleString()})` : ''}`
|
||||
: `Connected to ${hostLabel}`
|
||||
}
|
||||
aria-label={
|
||||
|
||||
@@ -200,7 +200,7 @@ export function calibrationData(
|
||||
return {
|
||||
label: `est/${b}d`,
|
||||
n: fit ? fit.n : inBucket.length,
|
||||
median: inBucket.length ? `${(b * Math.exp(mu)).toFixed(1)}d` : '—',
|
||||
median: inBucket.length ? `${(b * Math.exp(mu)).toFixed(1)}d` : '·',
|
||||
bias: fit ? pctFromMu(fit.mu) : null,
|
||||
}
|
||||
})
|
||||
@@ -216,7 +216,7 @@ export function calibrationData(
|
||||
.filter((i) => i.state === 'open')
|
||||
.reduce((sum, i) => sum + (i.facts.estimateDays ?? 2), 0)
|
||||
const effect = model.coldStart
|
||||
? { raw: `${model.n}/${COLD_START_THRESHOLD} estimated closes`, banded: 'cold-start priors', p50: '—' }
|
||||
? { raw: `${model.n}/${COLD_START_THRESHOLD} estimated closes`, banded: 'cold-start priors', p50: '·' }
|
||||
: {
|
||||
raw: `${openEst}d estimated`,
|
||||
banded: `×${Math.exp(model.global.mu).toFixed(2)} median drift`,
|
||||
|
||||
@@ -6,12 +6,12 @@ import { type ChatMessage } from '../data/view-types.js'
|
||||
|
||||
const LIVE_GREETING: ChatMessage = {
|
||||
from: 'agent',
|
||||
text: 'Morning. Ask me anything about the project — I check the real board before I answer.',
|
||||
text: 'Good morning. Do ask me anything about the project; I shall consult the board before I venture an answer.',
|
||||
}
|
||||
|
||||
const NO_MODEL: ChatMessage = {
|
||||
from: 'agent',
|
||||
text: "Reginald is off — I need a model. Add a Model URL in Settings and chat turns on.",
|
||||
text: 'I am afraid I am rather off duty without a model. Kindly add a Model URL in Settings and I shall be at your service.',
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
@@ -116,14 +116,17 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
...c,
|
||||
{
|
||||
from: 'agent',
|
||||
text: res.reason === 'error' ? `I hit a snag: ${res.message ?? 'unknown error'}` : 'No model is configured.',
|
||||
text:
|
||||
res.reason === 'error'
|
||||
? `I have struck a snag, I am afraid: ${res.message ?? 'unknown error'}.`
|
||||
: 'No model is configured.',
|
||||
},
|
||||
])
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
finish()
|
||||
setConvo((c) => [...c, { from: 'agent', text: 'I could not reach the model.' }])
|
||||
setConvo((c) => [...c, { from: 'agent', text: 'I was unable to reach the model, regrettably.' }])
|
||||
})
|
||||
},
|
||||
[live],
|
||||
@@ -139,8 +142,8 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
{
|
||||
from: 'agent',
|
||||
text: res.ok
|
||||
? `Done — #${p.change.issue}: ${p.summary}. The plan's been re-run.`
|
||||
: `That didn't take — #${p.change.issue} is unchanged.`,
|
||||
? `Very good. #${p.change.issue}: ${p.summary}. The plan has been re-run.`
|
||||
: `Regrettably that did not take; #${p.change.issue} remains unchanged.`,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -150,7 +153,7 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
|
||||
const dismiss = useCallback((p: ChangeProposal) => {
|
||||
drop(p)
|
||||
setConvo((c) => [...c, { from: 'agent', text: `Left #${p.change.issue} as it was.` }])
|
||||
setConvo((c) => [...c, { from: 'agent', text: `As you wish. #${p.change.issue} remains as it was.` }])
|
||||
}, [])
|
||||
|
||||
return { msgs: [...seed, ...convo], thinking, live, model, steps, proposals, streaming, send, approve, dismiss }
|
||||
|
||||
@@ -53,7 +53,7 @@ export function depsGraphView(d: ProjectData): DepsData {
|
||||
nodes: [],
|
||||
milestone: fallback
|
||||
? { name: fallback.title, due: fallback.dueOn ? formatShort(new Date(fallback.dueOn)) : 'no date', col: 0, row: 0 }
|
||||
: { name: '—', due: 'no date', col: 0, row: 0 },
|
||||
: { name: '·', due: 'no date', col: 0, row: 0 },
|
||||
edges: [],
|
||||
critical: [],
|
||||
unattached: [],
|
||||
@@ -134,7 +134,7 @@ export function depsGraphView(d: ProjectData): DepsData {
|
||||
let milestone: DepsData['milestone']
|
||||
const medianRow = median(nodes.map((n) => n.row))
|
||||
if (openMilestones.length === 0) {
|
||||
milestone = { name: '—', due: 'no date', col: maxCol + 1, row: medianRow }
|
||||
milestone = { name: '·', due: 'no date', col: maxCol + 1, row: medianRow }
|
||||
} else {
|
||||
let best: { id: number; title: string; dueOn: string | null | undefined; count: number } | null = null
|
||||
for (const m of openMilestones) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const TOTAL_CAP = 12
|
||||
|
||||
/** "HH:MM" from an ISO timestamp; '—' when absent (never fabricated). */
|
||||
function hhmm(iso: string | null | undefined): string {
|
||||
if (!iso) return '—'
|
||||
if (!iso) return '·'
|
||||
const d = new Date(iso)
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
@@ -103,7 +103,7 @@ export function inboxView(d: ProjectData): InboxItem[] {
|
||||
// left at the honest placeholder rather than borrowed from an issue.
|
||||
text: `${m.title} closed`,
|
||||
detail: 'milestone complete',
|
||||
time: '—',
|
||||
time: '·',
|
||||
unread: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function issueDetailView(id: number, d: ProjectData): IssueDetail | null
|
||||
return {
|
||||
state: inf.column === 'review' ? 'review' : inf.column,
|
||||
assignee: issue.assignee ?? 'unassigned',
|
||||
milestone: issue.milestone?.title ?? '—',
|
||||
milestone: issue.milestone?.title ?? '·',
|
||||
body: issue.body,
|
||||
comments: [],
|
||||
lifecycle: lifecycle(inf.stages),
|
||||
|
||||
Reference in New Issue
Block a user