From 6234b1dec58e9a8da0fd457e5e97688976633a77 Mon Sep 17 00:00:00 2001 From: Robert Crandall Date: Mon, 5 Oct 2026 09:25:24 -0700 Subject: [PATCH] Clean up Task Details layout --- DESIGN.md | 6 +- PRODUCT.md | 8 +- README.md | 14 +-- src/work/AssessmentHistory.tsx | 82 ++++++++++----- src/work/CodeSessionPanel.tsx | 66 +++++++----- src/work/TaskApp.tsx | 139 ++++++++++++++---------- src/work/styles.test.ts | 2 +- src/work/tasks.css | 20 ++++ tests/desktop.spec.ts | 10 +- tests/tasks.spec.ts | 186 +++++++++++++++++++++++++++++++-- 10 files changed, 393 insertions(+), 140 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index fd0795b..cd1f8b7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -25,13 +25,13 @@ Work profile lives in one sidebar footer row above Settings, showing the active Settings is a full settings page, not a large modal. Separate appearance, instructions, source queries, the opt-in schedule, connection guidance and recovery. Appearance applies and saves immediately, independently of the work-settings form. Manual capture uses a small focused dialog and Command/Ctrl+K. Local edits stay available during collection and ranking. -Keep Task assessor and Task prioritizer as plainly labeled fieldsets in Settings, each with a name, model and instructions. Run now is the default split-button action; its dropdown contains Run assessor and Run prioritizer with brief role descriptions. Assessment ratings stay in the compact task-details definition list and history selector, not a dashboard or card grid. +Keep Task assessor and Task prioritizer as plainly labeled fieldsets in Settings, each with a name, model and instructions. Run now is the default split-button action; its dropdown contains Run assessor and Run prioritizer with brief role descriptions. Task notes precede a bounded Why this order area. Overview shows the ranking reason and compact rating/work-style pills; Assessment holds the latest judgments, rating-rationale disclosures and work-style corrections; History holds saved versions, pagination and provenance. Use keyboard-accessible tabs, not a dashboard or card grid. Implementation assessor and PR reviewer use the same Settings fieldset language. Their actions live in task details and the selected-task toolbar beside compact progress/cancel/result/history controls. Show the service-owned PR conclusion and partial coverage even with no findings. Keep revision/configuration detail in a disclosure and code evidence behind explicit links. Results never displace editing, Done or navigation. To do checkboxes are separate from row focus, rank and Done. Keep select/clear visible tasks and the selected count above the list; reveal eligible agent actions only with a selection. Whole-list prioritization stays outside this toolbar. Eligibility and per-task batch outcomes use compact disclosures, not another dashboard. Batch progress lives in the scrolling list pane, in task details on narrow windows, and near the top of scrolling Settings. Its frozen scope and Stop stay discoverable during navigation without consuming the available editing area. -Do not retain a second workspace or disabled placeholders for the retired Inbox, Filtered, Archive or Tasks destinations. Saved conversations and private thread notes appear alongside matching tasks. +Do not retain a second workspace or disabled placeholders for the retired Inbox, Filtered, Archive or Tasks destinations. Private thread notes remain in a collapsed disclosure alongside matching tasks. Saved conversations are preserved but no longer read or loaded in Task Details. ## Palette and type @@ -45,7 +45,7 @@ Use monospace only for raw preserved records or other machine output. Display us ## Task details and conversations -Task details retain editable text, Done, priority reasons, evidence and notes. Matching saved thread notes remain separate from ranking notes. The conversation reader loads cached messages on selection and fetches only after an explicit action. Keep full Markdown messages readable; never execute HTML or automatically fetch remote images. Unsubscribe and historical operation retries require confirmation. +Task details retain editable text, Done, Open source, priority reasons, evidence and notes. Matching saved thread notes remain private and separate from ranking notes. PR reviewer/Implementation assessor and conversation notifications use disclosures below the evidence; code progress and failures stay discoverable and open automatically. Removing the conversation reader from this surface must not delete cached data or trigger cache reads/network calls on selection. Unsubscribe and historical operation retries still require confirmation. Markdown in code results remains untrusted: never execute HTML or automatically fetch remote images. ## Feedback and recovery diff --git a/PRODUCT.md b/PRODUCT.md index 6efa4c9..ccdd646 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -48,7 +48,7 @@ GitHub task identity is the canonical issue/PR URL, regardless of action. Reposi Each run applies results to the latest state so concurrent captures, notes and Done survive. Invalid rankings, source failures and save failures remain explicit. Preserve discoveries and the prior order when ranking fails. Missing query results do not prove completion. -Successful assessments are permanent versions on each task, including manual tasks, in its work profile. Save each assessed subset before ordering or assessing more tasks. Task details show the latest result, evidence, evaluation time and provenance, with earlier versions available in a compact selector. Flag changed task inputs or settings without expiring judgments. Done, restore, reconciliation, duplicate consolidation, profile switching and backups retain versions. Retries reuse immutable result IDs without duplicating history. Concurrent edits retain their text and leave the submitted assessment historical, not current. Old replaceable cache entries do not become invented history. +Successful assessments are permanent versions on each task, including manual tasks, in its work profile. Save each assessed subset before ordering or assessing more tasks. Task details put notes above Why this order: Overview shows the ranking reason and latest rating/work-style pills, Assessment shows the latest saved judgments and editable style assignments, and History shows earlier versions, pagination and provenance. Keep the tabbed area bounded and keyboard-accessible. Selecting a historical assessment must never replace the latest overview or assessment. Flag changed task inputs or settings without expiring judgments. Done, restore, reconciliation, duplicate consolidation, profile switching and backups retain versions. Retries reuse immutable result IDs without duplicating history. Concurrent edits retain their text and leave the submitted assessment historical, not current. Old replaceable cache entries do not become invented history. Keep permanent assessment rows separately paged in the existing local SQLite database, outside the bounded task snapshot. Read versions by logical append order, not wall-clock timestamps. History growth or a failed history append must not exhaust snapshot capacity or block ordinary notes, Done and capture saves. Keep failed append results visible for explicit retry/export. Database backups and bounded JSON export include complete history and profile/task associations; restoring a database backup replaces state and history together, never mixes unrelated rows. Prior-workspace results remain separately exportable without blocking new runs. Each historical task ID belongs to exactly one task per profile, including after consolidation. @@ -64,11 +64,9 @@ Saved thread notes stay private and editable beside matching ranked tasks. Prese ## Reading and external operations -The desktop reader shows real issue/PR descriptions, comments, reviews and inline discussions with stable reply grouping, author, time and source links. Markdown is untrusted: do not execute source HTML or automatically load external images/embeds. Keep long messages readable without clipping. +Task Details does not render a conversation reader, Load conversation or cache-discard controls. Selecting a task must not read or fetch cached conversations. Keep existing conversation data and cache infrastructure intact; removing this UI is not permission to delete data. -Selection reads only the separate local conversation cache. **Load conversation**, **Load older** and page reloads are explicit network actions. Keep bounded pagination, page freshness, inaccessible/partial results and missing reply context visible. Stable IDs deduplicate repeated pages and retain edits; discovered history never becomes new notification evidence. - -Conversation bodies never enter the workspace snapshot, note saves or model requests. The independent cache is limited to 4 MiB per source (repository + type + number) and 64 MiB total. Full/corrupt caches offer explicit cache-only discard, preserving the old cache file and all notes/Tasks. No silent eviction or reset. +Saved thread notes remain in a separate private disclosure, never used for ranking. Code-agent actions and conversation notifications use disclosures; active code work and failures remain visible. Treat Markdown evidence as untrusted: never execute source HTML or automatically load external images. Keep **Open source** visible in task details. Source links and conversation links open only after an explicit action. Private notes never enter external links. diff --git a/README.md b/README.md index 8f038a6..ce918e4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ In **Settings → Work styles**, define names and descriptions for this profile. Styles appear as pills on task rows. **Filters → Work styles** matches any selected style within the selected sources, preserving original ranks across To do, Done and No action now. **All styles** includes unclassified tasks. Filter choices persist per profile; **Ranked Tasks** bypasses both source and style filters. -In task details, change the style checkboxes to save your own assignments, including no styles. Reassessment never overwrites these corrections. **Use Copilot assignments** restores the latest saved automatic assignments. Renaming a style updates its current pills; deleting it hides them without deleting history. Historical assessments retain the original definitions, and details disclose changed definitions. The Implementation assessor remains a separate code-inspection agent. +In **Task details → Why this order → Assessment → Work styles**, change the style checkboxes to save your own assignments, including no styles. Reassessment never overwrites these corrections. **Use Copilot assignments** restores the latest saved automatic assignments. Renaming a style updates its current pills; deleting it hides them without deleting history. Historical assessments retain the original definitions, and details disclose changed definitions. The Implementation assessor remains a separate code-inspection agent. ### Work profiles @@ -74,7 +74,7 @@ Each profile keeps its own agents, sources, collection model, schedule, tasks, n ### Code sessions -**Task details → Assess implementation** inspects a GitHub issue and its pinned default-branch code. **Review PR** inspects a PR at its pinned head and merge base. Both run inside this app and save results automatically. Configure the **Implementation assessor** and **PR reviewer** in the same Settings agent section. Existing assessor/prioritizer customizations stay unchanged. Each role has one stable identity, editable name/instructions/model, and fixed capabilities. +**Task details → Implementation assessor → Assess implementation** inspects a GitHub issue and its pinned default-branch code. **PR reviewer → Review PR** inspects a PR at its pinned head and merge base. Both run inside this app and save results automatically. Configure the **Implementation assessor** and **PR reviewer** in the same Settings agent section. Existing assessor/prioritizer customizations stay unchanged. Each role has one stable identity, editable name/instructions/model, and fixed capabilities. Code jobs run only after an explicit task or batch action, never from selection, focus, Run now or a schedule. They use only bounded `list_code` and `read_code` tools; task notes and private thread notes are excluded. They cannot execute or edit code, delegate, mark Done, or submit/approve a GitHub review. Notes, capture, Done, navigation and profile switching remain usable while a code job runs. Another Copilot run waits. @@ -104,7 +104,7 @@ Copilot saves importance, urgency, blockers, evidence and uncertainty alongside Saved judgments do not expire. An older assessment's reevaluation suggestion remains historical provenance, not an automatic rerun or ordering gate. Comparative ordering can be reused for up to **1 hour**, provided task inputs, observed PR state and prioritizer instructions are unchanged. A new draft/CI state invalidates ordering, not assessment history. Source checks still run; observation timestamps alone do not invalidate an otherwise identical order. The derived order cache remains credential-scoped, but native task history remains usable after cache loss or credential rotation. -**Task details → Assessment** keeps the latest result and a version selector for earlier results. Evidence, evaluation time and provenance remain readable after edits, Done or relaunch. “Outdated” means saved inputs or settings changed; the judgment is still available for prioritization. Urgency and blockers are labeled as observations from assessment time, not fresh source checks. Blank model selection is identified as SDK default, not a guessed resolved model. +**Task details** puts auto-saved task notes above a bounded **Why this order** area. **Overview** shows the ranking reason and latest impact, visibility, effort and work-style pills. **Assessment** contains the latest importance, urgency, blockers, expandable rating rationales, uncertainty, evidence, work-style corrections and Assess task. **History** is a separate audit view with the saved-version selector, older pages and provenance; selecting an old version never changes Overview or the latest Assessment. The tabs support arrow keys, Home and End. Evidence, evaluation time and provenance remain readable after edits, Done or relaunch. “Outdated” means saved inputs or settings changed; the judgment is still available for prioritization. Urgency and blockers are labeled as observations from assessment time, not fresh source checks. Blank model selection is identified as SDK default, not a guessed resolved model. New v4 results include work-style assignments and the definitions used, alongside the agent and its configuration fingerprint. Earlier v2/v3 history stays readable; absent ratings and styles are labeled **Not recorded**, not invented. Changing an agent never rewrites its historical judgments. Roles and result formats are code-defined, with exactly one definition per supported role; instructions cannot grant tools, source access, delegation or GitHub writes. @@ -134,7 +134,7 @@ The first scan covers the last **30 days**. Later scans include both read and un Notifications identify conversations to inspect, not obligations. Actual source requests determine the action and retain their original event IDs and occurrence times. Notification reasons can remain `mention` after unrelated activity, so neither the reason nor the notification's update time can reopen Done. Requests already found through another source join the same issue or PR task. -**Unsubscribe on GitHub** appears in details for tasks discovered through notifications. Confirm it separately from Done. It stops following the conversation without closing the source, deleting the task or changing completion. Direct mentions, team mentions and review requests can still notify you again. The app saves the unsubscribe intent before sending it; unconfirmed writes stay visible for explicit retry and never replay automatically after relaunch. +**Unsubscribe on GitHub** appears under the **Conversation notifications** disclosure in details for tasks discovered through notifications. Confirm it separately from Done. It stops following the conversation without closing the source, deleting the task or changing completion. Direct mentions, team mentions and review requests can still notify you again. The app saves the unsubscribe intent before sending it; unconfirmed writes stay visible for explicit retry and never replay automatically after relaunch. ### Slack and MCP @@ -152,7 +152,7 @@ See the [service contract](service/README.md) for source configuration, intake a Ranked Tasks uses three panels: navigation, the ranked list and task details. Filters adds a locally filtered view of the same tasks with a scrollable, collapsible source tree. Settings replaces Appearance in the sidebar and includes themes, sources, priorities and scheduling. On narrow windows, task details replace the list until closed. -The old Inbox, Filtered, Archive, Tasks and saved-reference views are retired. Waiting on me and filtering rules are removed, including their logic. Source queries control discovery. Saved thread notes remain private and editable beside matching tasks; backups retain all notes and conversations. Existing saved filtering rules and named inboxes are retired only after an original backup succeeds. +The old Inbox, Filtered, Archive, Tasks and saved-reference views are retired. Waiting on me and filtering rules are removed, including their logic. Source queries control discovery. Saved thread notes remain private and editable in a collapsed disclosure beside matching tasks; backups retain all notes and conversations. Task Details no longer renders the conversation reader or offers Load conversation or cache discard. Open source remains available; selecting a task neither reads nor fetches conversation bodies. Existing conversation caches are untouched. Existing saved filtering rules and named inboxes are retired only after an original backup succeeds. ## Run the desktop @@ -218,7 +218,7 @@ Unconfirmed GitHub writes remain visible and explicitly retryable after relaunch Conversation bodies live in a separate checksummed SQLite cache in the same private app directory, outside workspace snapshots and their 8 MiB limit. A source means one repository + issue/PR type + number, independent of notification IDs. Limits are **4 MiB per source** and **64 MiB total**, measured as serialized UTF-8 data; each service page holds at most five messages within 1 MiB. Untransportable pages and full/corrupt caches produce explicit errors, never shortened bodies, silent eviction or lost notes. -**Discard conversation cache** requires confirmation and clears only cached source data. The native app preserves the discarded cache file locally; those recovery copies need manual cleanup. Notes, Tasks, pending notification evidence and workspace backups are unchanged. Navigation during discard remains usable afterward; pending reads cannot restore discarded content. Loading again requires a separate explicit action. Offline startup/navigation reads the remaining cache without contacting GitHub. +The conversation reader and its cache-discard controls are retired from Task Details. This layout change does not delete or migrate cached conversations, notes or backups. Use **Open source** to read the live conversation externally. Legacy routines and native reminder delivery are retired. The native reminder array remains empty; the new collection cadence lives separately in task settings. A legacy snapshot cannot notify while loading, after a failed migration, or after backup recovery. Closing hides the existing window; **Show GitHub Projects** returns it and **Quit GitHub Projects** stops the owned service process group. @@ -263,4 +263,4 @@ These use generated TEST directories, not app data. They check the ranked task h - [Native integration contract](src/platform/README.md) documents SQLite revisions, recovery, retired schedules, destinations, and owned process hosting. - [Service contract](service/README.md) documents auth, exact endpoints, source coverage, SDK isolation, and bounded JSONL transport. -GitHub refresh is a bounded view of notifications and REST timeline evidence, not full repository synchronization. Missing notifications prove nothing. Archive retains a monotonic source timestamp; without a notification baseline, activity must be newer than the local archive time. A newly discovered same-timestamp ID alone cannot prove new activity. The independent conversation reader fetches REST message pages, not every historical revision, file diff, resolved-review status or repository content. The service is capability-restricted but not an operating-system sandbox. External Copilot App launching is separate from SDK previews. +GitHub refresh is a bounded view of notifications and REST timeline evidence, not full repository synchronization. Missing notifications prove nothing. Archive retains a monotonic source timestamp; without a notification baseline, activity must be newer than the local archive time. A newly discovered same-timestamp ID alone cannot prove new activity. The retained conversation infrastructure supports REST message pages, not every historical revision, file diff, resolved-review status or repository content. The service is capability-restricted but not an operating-system sandbox. External Copilot App launching is separate from SDK previews. diff --git a/src/work/AssessmentHistory.tsx b/src/work/AssessmentHistory.tsx index af5b728..3c68707 100644 --- a/src/work/AssessmentHistory.tsx +++ b/src/work/AssessmentHistory.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useSyncExternalStore } from 'react'; +import { useEffect, useState, useSyncExternalStore, type ReactNode } from 'react'; import { identityDigest, type SavedAssessment, type TaskAssessment } from '../../service/src/work-assessment.ts'; import { taskAgent } from '../../service/src/work-agents.ts'; import { assessmentIdentity } from '../../service/src/work-styles.ts'; @@ -10,24 +10,31 @@ import { assessmentFreshness } from './assessments.ts'; import { rankTask } from './engine.ts'; function date(value: string) { return new Date(value).toLocaleString(); } +const ratingFields = ['impact', 'visibility', 'effort'] as const; +function label(field: string) { return field[0]!.toUpperCase() + field.slice(1); } +export type ReasoningView = 'overview' | 'assessment' | 'history'; function AssessmentRatings({ value }: { value: SavedAssessment }) { - return <>{(['impact', 'visibility', 'effort'] as const).map(field =>
-
{field[0]!.toUpperCase() + field.slice(1)}
+ return <>{ratingFields.map(field =>
+
{label(field)}
{value.assessmentVersion !== 'work-assessment-v2' ? `${value.assessment[field].rating} - ${value.assessment[field].rationale}` : 'Not recorded in this assessment format.'}
)}; } -export function TaskAssessmentHistory({ task, controller }: { task: Task; controller: DesktopWorkspace }) { +export function TaskAssessmentHistory({ task, controller, view, overview, overviewStyles, controls }: { + task: Task; controller: DesktopWorkspace; view: ReasoningView; + overview: ReactNode; overviewStyles: ReactNode; controls: ReactNode; +}) { const snapshot = useSyncExternalStore(controller.subscribe, controller.getSnapshot); const profileId = controller.state.activeWorkProfile.id; const [page, setPage] = useState<{ key: string; values: TaskAssessment[]; before: number | null; latest: string }>(); - const [cursor, setCursor] = useState(null); + const [historyCursor, setHistoryCursor] = useState(null); + const cursor = view === 'history' ? historyCursor : null; const [attempt, setAttempt] = useState(0); const [error, setError] = useState(''); - const key = JSON.stringify([profileId, task.id, task.assessmentTaskIds, snapshot.assessmentRevision, cursor, attempt]); + const key = JSON.stringify([profileId, task.id, task.assessmentTaskIds, controller.assessmentGeneration, snapshot.assessmentRevision, cursor, attempt]); useEffect(() => { let cancelled = false; setError(''); @@ -41,11 +48,12 @@ export function TaskAssessmentHistory({ task, controller }: { task: Task; contro }, [controller, key]); const pending = snapshot.assessmentPending.filter(value => value.profileId === profileId && (value.id === task.id || task.assessmentTaskIds?.includes(value.id))); + const latest = page?.key === key ? page.values.at(-1) : undefined; return <> {pending.length > 0 &&

Unsaved assessments

{snapshot.assessmentError || 'Saving assessment history...'}

Task edits save separately. These results remain available for retry or export.

- {pending.map(value =>
{new Date(value.evaluatedAt).toLocaleString()} + {pending.map(value =>
{date(value.evaluatedAt)}

{value.assessment.importance}

{value.assessment.urgency}

{value.assessment.blockers}

{value.assessment.uncertainty}

@@ -55,21 +63,34 @@ export function TaskAssessmentHistory({ task, controller }: { task: Task; contro void controller.retryAssessments().catch(error => controller.report(error)); }}>Retry assessment save
} - {error ?

Assessment

{error}

+ {view === 'overview' && overview} + {error ?

{error}

- : page?.key === key ? <> - -
- {cursor !== null && } - {page.before !== null && } + : page?.key === key ? view === 'overview' ? <> +
+ {latest && ratingFields.map(field => + {label(field)} {latest.assessmentVersion === 'work-assessment-v2' ? 'Not recorded' : latest.assessment[field].rating} + )} + {overviewStyles}
- :

Assessment

Reading saved assessments...

} + {latest ?

Assessed {date(latest.evaluatedAt)}. Latest saved result.

:

No saved assessment yet. Use Assessment to assess this task.

} +

Assessment contains the underlying judgments. History preserves earlier versions.

+ : <> + {view === 'history' &&

Assessment history only. Current ranking remains separate.

} + + {view === 'history' &&
+ {cursor !== null && } + {page.before !== null && } +
} + :

Reading saved assessments...

} + {view === 'assessment' && controls} ; } -export function AssessmentHistory({ task, profileId, settings, latestResultId }: { +export function AssessmentHistory({ task, profileId, settings, latestResultId, mode = 'history' }: { task: Task & { assessments: TaskAssessment[] }; profileId: string; settings: WorkSettings; latestResultId?: string; + mode?: 'assessment' | 'history'; }) { const [selected, setSelected] = useState(''); const [, updateClock] = useState(0); @@ -95,39 +116,46 @@ export function AssessmentHistory({ task, profileId, settings, latestResultId }: }, []); const versions = task.assessments ?? []; const latest = versions.at(-1); - const value = versions.find(value => value.resultId === selected) ?? latest; + const value = mode === 'assessment' ? latest : versions.find(value => value.resultId === selected) ?? latest; if (!value) return

Assessment

-

No saved assessment yet. Run assessor or Run now to assess active tasks; earlier cache results are not task history.

+

No saved assessment yet. Use Assess task, Run assessor or Run now; earlier cache results are not task history.

; const freshness = identity?.key === key ? assessmentFreshness(value, { ...identity, profileId, model: agent.model }, Date.now()) : 'Checking saved input identity...'; return
-

Assessment

- {versions.length > 1 && }

Assessed {date(value.evaluatedAt)}. {value.resultId === (latestResultId ?? latest?.resultId) ? 'Latest saved result.' : 'Historical result.'}

+

Urgency and blockers are historical judgments, not live source status.

{error ?

{error}

:

{freshness}

}
- -
Work styles when assessed
{value.assessmentVersion === 'work-assessment-v4' - ? value.workStyles.filter(style => value.assessment.workStyleIds.includes(style.id)).map(style => style.name).join(', ') || 'No matching styles.' - : 'Not recorded in this assessment format.'}
+ {mode === 'history' && <> + +
Work styles when assessed
{value.assessmentVersion === 'work-assessment-v4' + ? value.workStyles.filter(style => value.assessment.workStyleIds.includes(style.id)).map(style => `${style.name}: ${style.description}`).join('; ') || 'No matching styles.' + : 'Not recorded in this assessment format.'}
+ }
Importance
{value.assessment.importance}
Urgency when assessed
{value.assessment.urgency}
Blockers when assessed
{value.assessment.blockers}
-
Uncertainty
{value.assessment.uncertainty || 'None recorded.'}
+ {mode === 'assessment' && ratingFields.map(field =>
+ {label(field)} - {value.assessmentVersion === 'work-assessment-v2' ? 'Not recorded' : value.assessment[field].rating} +

{value.assessmentVersion === 'work-assessment-v2' ? 'Not recorded in this assessment format.' : value.assessment[field].rationale}

+
)} +

Uncertainty

{value.assessment.uncertainty || 'None recorded.'}

Supporting evidence

    {value.assessment.supportingEvidence.map((item, index) =>
  • {item.summary} ({item.reference})
  • )}

Original reevaluation suggestion: {date(value.assessment.reevaluateAt)}. {' '}This saved judgment remains usable. Assess task explicitly when its scope needs a new judgment.

-
Assessment provenance
+ {mode === 'history' &&
Assessment provenance
Agent
{value.assessmentVersion !== 'work-assessment-v2' ? `${value.agent.name} (${value.agent.id})` : 'Legacy task assessor'}
{value.assessmentVersion !== 'work-assessment-v2' && <>
Agent configuration identity
{value.agent.configurationFingerprint}
@@ -139,6 +167,6 @@ export function AssessmentHistory({ task, profileId, settings, latestResultId }:
Result ID
{value.resultId}
Input identity
{value.fingerprint}
Instruction identity
{value.instructionsFingerprint}
-
+
}
; } diff --git a/src/work/CodeSessionPanel.tsx b/src/work/CodeSessionPanel.tsx index d0288a7..e541f0a 100644 --- a/src/work/CodeSessionPanel.tsx +++ b/src/work/CodeSessionPanel.tsx @@ -87,6 +87,7 @@ export function CodeSessionPanel({ task, controller, sessions, workBusy }: { const [page, setPage] = useState<{ key: string; page: CodeRunPage }>(); const [cursor, setCursor] = useState(null); const [selected, setSelected] = useState(''); + const [expanded, setExpanded] = useState(false); const [attempt, setAttempt] = useState(0); const [error, setError] = useState(''); const key = JSON.stringify([profileId, task.id, task.assessmentTaskIds, saved.codeRevision, state.revision, controller.assessmentGeneration, cursor, attempt]); @@ -104,34 +105,41 @@ export function CodeSessionPanel({ task, controller, sessions, workBusy }: { && (run.intent.input.taskId === task.id || task.assessmentTaskIds?.includes(run.intent.input.taskId))); const versions = page?.key === key ? page.page.runs : []; const result = versions.find(run => run.intent.runId === selected) ?? versions[0]; - return

Code sessions

- {!source && task.work && githubReference(task.work.url) &&

Source kind unknown: this saved issue-form link may identify a PR. Run now to collect its GitHub source before starting a code job. Nothing is fetched on selection.

} - {source &&
- {active && active.phase !== 'saving' && } -
} - {active && state.batch?.running &&

Stop batch cancels the current code job and leaves remaining tasks not started.

} - {active &&

{active.phase === 'preparing' ? 'Saving start before contacting GitHub...' : active.phase === 'cancelling' - ? 'Cancellation requested. Waiting for the actual outcome...' : active.phase === 'saving' ? 'Saving code result...' - : 'Reading pinned code and running the agent (up to three minutes)...'}

} - {state.busy && !active &&

Waiting for the previous code job to finish before starting another Copilot run...

} - {state.error &&

{state.error}

} - {!active && !result && !pending.length && !error &&

No saved code sessions. Only an explicit task action contacts GitHub and Copilot.

} - {error && <>

{error}

} - {pending.map(value =>
-

{saved.codeError || (saved.codeSaving.includes(value.intent.runId) ? 'Saving code result...' : 'This result is not saved. Retry saving or export it.')}

- -
-
-
)} - {result && <>} -
- {cursor !== null && } - {page?.key === key && page.page.before !== null && } -
+ useEffect(() => { + if (active || pending.length || error || state.error || state.busy) setExpanded(true); + }, [active, pending.length, error, state.error, state.busy]); + return
+
setExpanded(event.currentTarget.open)}> + {source?.kind === 'pr' ? 'PR reviewer' : source?.kind === 'issue' ? 'Implementation assessor' : 'Code sessions'} + {active ? ' - In progress' : result ? ` - ${labels[result.outcome.status]}` : ''} + {!source && task.work && githubReference(task.work.url) &&

Source kind unknown: this saved issue-form link may identify a PR. Run now to collect its GitHub source before starting a code job. Nothing is fetched on selection.

} + {source &&
+ {active && active.phase !== 'saving' && } +
} + {active && state.batch?.running &&

Stop batch cancels the current code job and leaves remaining tasks not started.

} + {active &&

{active.phase === 'preparing' ? 'Saving start before contacting GitHub...' : active.phase === 'cancelling' + ? 'Cancellation requested. Waiting for the actual outcome...' : active.phase === 'saving' ? 'Saving code result...' + : 'Reading pinned code and running the agent (up to three minutes)...'}

} + {state.busy && !active &&

Waiting for the previous code job to finish before starting another Copilot run...

} + {state.error &&

{state.error}

} + {!active && !result && !pending.length && !error &&

No saved code sessions. Only an explicit task action contacts GitHub and Copilot.

} + {error && <>

{error}

} + {pending.map(value =>
+

{saved.codeError || (saved.codeSaving.includes(value.intent.runId) ? 'Saving code result...' : 'This result is not saved. Retry saving or export it.')}

+ +
+
+
)} + {result && <>} +
+ {cursor !== null && } + {page?.key === key && page.page.before !== null && } +
+
; } diff --git a/src/work/TaskApp.tsx b/src/work/TaskApp.tsx index 7c73171..d0b46c6 100644 --- a/src/work/TaskApp.tsx +++ b/src/work/TaskApp.tsx @@ -4,9 +4,7 @@ import { Check, ChevronDown, ExternalLink, Github, ListFilter, ListOrdered, Plus import { Modal } from '../Modal.tsx'; import { ConnectionsPanel, RecoveryPanel } from '../runtime/NativePanels.tsx'; import { DestinationPanel } from '../runtime/DestinationPanel.tsx'; -import { ConversationReader } from '../runtime/ConversationReader.tsx'; import { getRow } from '../domain/engine.ts'; -import { referenceSchema } from '../../service/src/schema.ts'; import type { Destination } from '../runtime/view.ts'; import type { DesktopWorkspace } from '../runtime/desktop-workspace.ts'; import type { ServiceWorkspace } from '../runtime/service-workspace.ts'; @@ -25,7 +23,7 @@ import { matchesSources, sourceCounts, taskSources } from './filters.ts'; import { SourceTree } from './SourceTree.tsx'; import { matchesStyles, taskStyles, useStyleAssessments } from './styles.ts'; import type { SavedAssessment } from '../../service/src/work-assessment.ts'; -import { TaskAssessmentHistory } from './AssessmentHistory.tsx'; +import { TaskAssessmentHistory, type ReasoningView } from './AssessmentHistory.tsx'; import { CodeSessionPanel } from './CodeSessionPanel.tsx'; import { codeJob } from './code-sessions.ts'; import { CodeBatchProgress } from './CodeBatchProgress.tsx'; @@ -128,11 +126,14 @@ function NewProfile({ queue, currentName, close, created }: { ; } -function TaskDetail({ task, reason, queue, controller, remote, close, batchProgress, styleAssessment, stylesLoading, stylesError }: { - task: Task; reason?: string; queue: WorkQueue; controller: DesktopWorkspace; remote: ServiceWorkspace; close: () => void; +function TaskDetail({ task, reason, rank, queue, controller, close, batchProgress, styleAssessment, stylesLoading, stylesError }: { + task: Task; reason?: string; rank?: number; queue: WorkQueue; controller: DesktopWorkspace; close: () => void; batchProgress: ReactNode; styleAssessment?: SavedAssessment; stylesLoading: boolean; stylesError: string; }) { + const [reasoningView, setReasoningView] = useState('overview'); + const reasoningPanel = useRef(null); + useEffect(() => { reasoningPanel.current?.scrollTo({ top: 0 }); }, [reasoningView]); const [confirmUnsubscribe, setConfirmUnsubscribe] = useState(false); const [unsubscribeError, setUnsubscribeError] = useState(''); const status = useSyncExternalStore(queue.subscribe, queue.getSnapshot); @@ -142,15 +143,6 @@ function TaskDetail({ task, reason, queue, controller, remote, close, batchProgr const threads = controller.state.threads.filter(thread => thread.id === task.threadId || (task.work && canonicalSource(`https://github.com/${thread.repo}/issues/${thread.number}`) === canonicalSource(task.work.url))); const notes = controller.state.notes.filter(note => threads.some(thread => thread.id === note.threadId)); - const url = task.work ? new URL(task.work.url) : null; - const match = url?.hostname === 'github.com' ? /^\/([^/]+\/[^/]+)\/(pull|pulls|issues)\/(\d+)\/?$/.exec(url.pathname) : null; - const thread = threads[0]; - const parsed = referenceSchema.safeParse(task.work?.notification?.reference ?? (thread && { - repo: thread.repo, kind: thread.kind, number: thread.number, - }) ?? task.work?.reference ?? (match && { - repo: match[1], kind: match[2] === 'issues' ? 'issue' : 'pr', number: Number(match[3]), - })); - const reference = parsed.success ? parsed.data : null; const styles = controller.state.work.settings.workStyles ?? []; const assigned = taskStyles(task, styles, styleAssessment); const run = (operation: () => void | Promise) => { @@ -167,36 +159,7 @@ function TaskDetail({ task, reason, queue, controller, remote, close, batchProgr {task.work && }
{task.status === 'done' &&

Done {date(task.completedAt ?? null)}. Only a fresh actionable request can bring this task back.

} - {task.work?.notification &&

Conversation notifications

-

Done handles the current request. Unsubscribe stops following the conversation without changing this task or closing the source.

- {subscription?.status === 'confirmed' ?

Unsubscribed on GitHub {date(subscription.confirmedAt ?? null)}. Direct mentions, team mentions and review requests can still notify you.

- : <> - {subscription && !unsubscribing &&

- {subscription.error || 'Unsubscribe is not confirmed. Retry explicitly; this app never resends it automatically.'} -

}} -
} {task.work?.availability !== undefined && task.work.availability !== 'actionable' &&

{task.work.availabilityReason}

} -

Why this order

{reason ?? 'Not ranked yet. The next run considers this task alongside all your other work.'}

- {styles.length > 0 &&

Work styles

-

{task.workStyleOverride !== undefined ? 'Your choices. Reassessment will not overwrite them.' - : stylesLoading ? 'Reading saved work styles...' - : stylesError ? 'Automatic styles are unavailable until history can be read.' - : styleAssessment?.assessmentVersion !== 'work-assessment-v4' ? 'Not classified yet. Use Assess task to assign styles.' - : assigned.length ? 'Assigned by Copilot. Change any choice to keep your own assignments.' - : 'Copilot found no matching styles. You can choose styles yourself.'}

- {styleAssessment?.assessmentVersion === 'work-assessment-v4' && task.workStyleOverride === undefined - && JSON.stringify(styleAssessment.workStyles) !== JSON.stringify(styles) - &&

Definitions changed since this assessment. Use Assess task to update automatic matches.

} - {styles.map(style => )} - {task.workStyleOverride !== undefined && } -
} {task.work?.reference?.kind === 'pr' &&

Current PR status

{task.work.pullRequest ? <>

{task.work.pullRequest.draft === null ? 'Draft status unknown' : task.work.pullRequest.draft ? 'Draft' : 'Not a draft'} @@ -205,19 +168,76 @@ function TaskDetail({ task, reason, queue, controller, remote, close, batchProgr

Checked {date(task.work.pullRequest.observedAt)}. Prioritization refreshes this separately from the saved assessment.

:

Not checked yet. Run prioritizer to refresh current PR status without reassessing this task.

}
} - {task.status === 'open' && task.work?.availability !== 'waiting' && } - - -