feat: enhance dashboard styling and functionality for task changes and package extensions
This commit is contained in:
@@ -116,7 +116,14 @@ vi.mock("@mariozechner/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mockAuthStorage),
|
||||
},
|
||||
DefaultPackageManager: vi.fn().mockImplementation(() => ({
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
})),
|
||||
ModelRegistry: vi.fn().mockImplementation(() => mockModelRegistry),
|
||||
SettingsManager: {
|
||||
create: vi.fn(() => ({})),
|
||||
},
|
||||
getAgentDir: vi.fn(() => "/mock/agent/dir"),
|
||||
discoverAndLoadExtensions: mockDiscoverAndLoadExtensions,
|
||||
createExtensionRuntime: mockCreateExtensionRuntime,
|
||||
}));
|
||||
|
||||
@@ -181,7 +181,14 @@ vi.mock("@mariozechner/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mockAuthStorage),
|
||||
},
|
||||
DefaultPackageManager: vi.fn().mockImplementation(() => ({
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
})),
|
||||
ModelRegistry: vi.fn().mockImplementation(() => mockModelRegistry),
|
||||
SettingsManager: {
|
||||
create: vi.fn(() => ({})),
|
||||
},
|
||||
getAgentDir: vi.fn(() => "/mock/agent/dir"),
|
||||
discoverAndLoadExtensions: mockDiscoverAndLoadExtensions,
|
||||
createExtensionRuntime: mockCreateExtensionRuntime,
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TaskStore, AutomationStore } from "@fusion/core";
|
||||
import type { Settings, TaskDetail, PrInfo } from "@fusion/core";
|
||||
import { createServer, GitHubClient } from "@fusion/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask, UsageLimitPauser, PRIORITY_MERGE, scanIdleWorktrees, cleanupOrphanedWorktrees, NtfyNotifier, PrMonitor, PrCommentHandler, CronRunner, StuckTaskDetector } from "@fusion/engine";
|
||||
import { AuthStorage, ModelRegistry, discoverAndLoadExtensions, createExtensionRuntime } from "@mariozechner/pi-coding-agent";
|
||||
import { AuthStorage, DefaultPackageManager, ModelRegistry, SettingsManager, discoverAndLoadExtensions, getAgentDir, createExtensionRuntime } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* Prompt the user for a port number interactively.
|
||||
@@ -453,7 +453,23 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
try {
|
||||
const extensionsResult = await discoverAndLoadExtensions([], cwd, undefined);
|
||||
// Resolve extension paths from pi settings packages (npm, git, local).
|
||||
// This picks up extensions like @howaboua/pi-glm-via-anthropic that
|
||||
// register custom providers (e.g. glm-5.1) via registerProvider().
|
||||
const agentDir = getAgentDir();
|
||||
const piSettingsManager = SettingsManager.create(cwd, agentDir);
|
||||
const packageManager = new DefaultPackageManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager: piSettingsManager,
|
||||
});
|
||||
const resolvedPaths = await packageManager.resolve();
|
||||
const packageExtensionPaths = resolvedPaths.extensions
|
||||
.filter((r) => r.enabled)
|
||||
.map((r) => r.path);
|
||||
|
||||
// Load all extensions: filesystem-discovered + package-resolved
|
||||
const extensionsResult = await discoverAndLoadExtensions(packageExtensionPaths, cwd, undefined);
|
||||
|
||||
for (const { path, error } of extensionsResult.errors) {
|
||||
console.log(`[extensions] Failed to load ${path}: ${error}`);
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ChangedFilesModal({ taskId, worktree, column, isOpen, onClose }:
|
||||
) : null}
|
||||
|
||||
{selectedFile ? (
|
||||
<div className="gm-diff-section" aria-label={`Diff for ${selectedFile.path}`}>
|
||||
<div className="gm-diff-section changed-files-diff-section" aria-label={`Diff for ${selectedFile.path}`}>
|
||||
<div className="file-browser-toolbar">
|
||||
<div className="file-browser-file-info">
|
||||
<strong>{selectedFile.path}</strong>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Unlink,
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import type {
|
||||
Mission,
|
||||
MissionWithHierarchy,
|
||||
@@ -29,8 +30,7 @@ import type {
|
||||
FeatureStatus,
|
||||
MilestoneWithSlices,
|
||||
SliceWithFeatures,
|
||||
} from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
} from "./mission-types";
|
||||
import {
|
||||
fetchMissions,
|
||||
createMission,
|
||||
|
||||
@@ -82,7 +82,7 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<div className="detail-loading">
|
||||
<div className="task-changes-state task-changes-state--loading">
|
||||
<div className="loading-spinner" />
|
||||
<span>Loading changes...</span>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<div className="detail-error">
|
||||
<div className="task-changes-state task-changes-state--error">
|
||||
<AlertCircle size={16} />
|
||||
<span>Error loading changes: {error}</span>
|
||||
</div>
|
||||
@@ -104,10 +104,10 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
if (!worktree) {
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<div className="detail-empty-state">
|
||||
<div className="task-changes-state task-changes-state--empty">
|
||||
<FileCode size={24} />
|
||||
<p>No worktree available for this task.</p>
|
||||
<span className="detail-empty-hint">
|
||||
<span className="task-changes-state-hint">
|
||||
Changes will be shown once the task is in progress.
|
||||
</span>
|
||||
</div>
|
||||
@@ -118,10 +118,10 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
if (!diffData || diffData.files.length === 0) {
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<div className="detail-empty-state">
|
||||
<div className="task-changes-state task-changes-state--empty">
|
||||
<FileCode size={24} />
|
||||
<p>No files modified.</p>
|
||||
<span className="detail-empty-hint">
|
||||
<span className="task-changes-state-hint">
|
||||
The agent did not modify any files during execution.
|
||||
</span>
|
||||
</div>
|
||||
@@ -174,7 +174,10 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
<span className="changes-file-path" title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<span className="changes-file-stat" title={`+${fileEntry.additions} -${fileEntry.deletions}`}>
|
||||
<span
|
||||
className="changes-file-stat"
|
||||
title={`+${fileEntry.additions} -${fileEntry.deletions}${fileEntry.size ? ` · ${formatFileSize(fileEntry.size)}` : ""}`}
|
||||
>
|
||||
+{fileEntry.additions} -{fileEntry.deletions}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Local type definitions for MissionManager
|
||||
// Mission types for MissionManager - local copy to avoid module resolution issues
|
||||
|
||||
export type MissionStatus = "planning" | "active" | "blocked" | "complete" | "archived";
|
||||
export type MilestoneStatus = "planning" | "active" | "blocked" | "complete";
|
||||
@@ -41,6 +41,8 @@ export interface Slice {
|
||||
features: MissionFeature[];
|
||||
}
|
||||
|
||||
export type SliceWithFeatures = Slice;
|
||||
|
||||
export interface Milestone {
|
||||
id: string;
|
||||
missionId: string;
|
||||
@@ -55,6 +57,8 @@ export interface Milestone {
|
||||
slices: Slice[];
|
||||
}
|
||||
|
||||
export type MilestoneWithSlices = Milestone;
|
||||
|
||||
export interface MissionWithHierarchy extends Mission {
|
||||
milestones: Milestone[];
|
||||
}
|
||||
|
||||
@@ -2759,6 +2759,149 @@ body {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.task-changes-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xl);
|
||||
min-height: 180px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.task-changes-state--loading,
|
||||
.task-changes-state--error {
|
||||
flex-direction: row;
|
||||
min-height: auto;
|
||||
padding: var(--space-md);
|
||||
justify-content: flex-start;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.task-changes-state--error {
|
||||
color: var(--color-error);
|
||||
background: rgba(248, 81, 73, 0.08);
|
||||
border-color: rgba(248, 81, 73, 0.25);
|
||||
}
|
||||
|
||||
.task-changes-state-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
max-width: 440px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-changes-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.changes-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.changes-header h4 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.changes-file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.changes-file-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.changes-file-item.expanded {
|
||||
border-color: var(--text-dim);
|
||||
}
|
||||
|
||||
.changes-file-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.changes-file-header:hover {
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.changes-file-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.changes-file-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.changes-file-path {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.changes-file-stat {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.changes-file-content {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.changes-diff-patch {
|
||||
margin: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: rgba(0, 0, 0, 0.14);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Summary section - styled for done tasks with success accent */
|
||||
.detail-summary {
|
||||
background: rgba(63, 185, 80, 0.08);
|
||||
@@ -2796,6 +2939,34 @@ body {
|
||||
|
||||
/* Mobile responsive for spec tab */
|
||||
@media (max-width: 768px) {
|
||||
.changed-files-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.changed-files-sidebar {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
.changed-files-content {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.changes-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.changes-file-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.changes-file-stat {
|
||||
width: 100%;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.detail-section--spec {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -11623,10 +11794,15 @@ html .column.drag-over * {
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.changed-files-diff-section {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gm-diff-viewer {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.gm-diff-stat {
|
||||
@@ -11647,7 +11823,7 @@ html .column.drag-over * {
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
max-height: 300px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
@@ -11669,6 +11845,31 @@ html .column.drag-over * {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.changed-files-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.changed-files-sidebar {
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.changed-files-content {
|
||||
padding: var(--space-md);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.changed-files-entry {
|
||||
justify-content: flex-start;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.changed-files-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── Commit Form ── */
|
||||
|
||||
.gm-commit-form {
|
||||
|
||||
Reference in New Issue
Block a user