From ffd36c529084f177f2d1d2169ed86426b0b4ba9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Wed, 11 Feb 2026 16:15:03 +0100 Subject: [PATCH 01/32] feat: Add new CommandPreview component --- .../pixi/process/commandPreview.tsx | 112 ++++++++++++++++++ src/components/pixi/process/processRow.tsx | 62 ++++++---- src/components/pixi/tasks/taskArgsDialog.tsx | 40 +++++-- src/routes/workspace.$path/process.tsx | 66 ++--------- 4 files changed, 196 insertions(+), 84 deletions(-) create mode 100644 src/components/pixi/process/commandPreview.tsx diff --git a/src/components/pixi/process/commandPreview.tsx b/src/components/pixi/process/commandPreview.tsx new file mode 100644 index 0000000..c0b1ef6 --- /dev/null +++ b/src/components/pixi/process/commandPreview.tsx @@ -0,0 +1,112 @@ +import { useMemo } from "react"; + +import type { TaskArgument } from "@/lib/pixi/workspace/task"; + +interface CommandPreviewProps { + command: string; + taskArguments?: TaskArgument[]; + argValues?: string[]; + extraArgs?: string; +} + +export function CommandPreview({ + command, + taskArguments = [], + argValues = [], + extraArgs = "", +}: CommandPreviewProps) { + const parts = useMemo( + () => parseCommand(command, taskArguments, argValues), + [command, taskArguments, argValues], + ); + + const trimmedExtra = extraArgs.trim(); + + return ( +
+ + {parts.map((part, index) => { + if (part.kind === "text") { + return ( + + {part.value} + + ); + } + + if (part.resolved) { + return ( + + {part.resolved} + + ); + } + + return ( + + {"{{ "} + {part.name} + {" }}"} + + ); + })} + {trimmedExtra && ( + <> + + + {trimmedExtra} + + + )} + +
+ ); +} + +type CommandPart = + | { kind: "text"; value: string } + | { kind: "variable"; name: string; resolved?: string }; + +function parseCommand( + command: string, + taskArguments: TaskArgument[], + argValues: string[], +): CommandPart[] { + const parts: CommandPart[] = []; + const regex = /\{\{\s*(\w+)\s*\}\}/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = regex.exec(command)) !== null) { + if (match.index > lastIndex) { + parts.push({ + kind: "text", + value: command.slice(lastIndex, match.index), + }); + } + + const varName = match[1]; + const argIndex = taskArguments.findIndex((arg) => arg.name === varName); + const resolved = argIndex !== -1 ? argValues[argIndex] : undefined; + + parts.push({ + kind: "variable", + name: varName, + resolved: resolved && resolved.trim() !== "" ? resolved : undefined, + }); + + lastIndex = regex.lastIndex; + } + + if (lastIndex < command.length) { + parts.push({ kind: "text", value: command.slice(lastIndex) }); + } + + return parts; +} diff --git a/src/components/pixi/process/processRow.tsx b/src/components/pixi/process/processRow.tsx index e38f4f3..960f407 100644 --- a/src/components/pixi/process/processRow.tsx +++ b/src/components/pixi/process/processRow.tsx @@ -1,5 +1,5 @@ import { getRouteApi } from "@tanstack/react-router"; -import { PlayIcon, Square } from "lucide-react"; +import { PencilLineIcon, PlayIcon, Square } from "lucide-react"; import { useState } from "react"; import { CircularIcon } from "@/components/common/circularIcon"; @@ -12,6 +12,7 @@ import type { Editor } from "@/lib/editor"; import { type Task, taskArguments as getTaskArguments, + command as getTaskCommand, description as getTaskDescription, } from "@/lib/pixi/workspace/task"; @@ -32,6 +33,8 @@ export function ProcessRow(props: ProcessRowProps) { // Task arguments dialog (only for tasks) const args = props.kind === "task" ? getTaskArguments(props.task) : []; + const taskCommand = + props.kind === "task" ? getTaskCommand(props.task) : undefined; const [argsDialogOpen, setArgsDialogOpen] = useState(false); // Icon based on kind @@ -99,27 +102,41 @@ export function ProcessRow(props: ProcessRowProps) { onClick={() => navigateToProcess()} suffix={ props.kind === "task" ? ( - + <> + + + ) : ( - + diff --git a/src/routes/workspace.$path/process.tsx b/src/routes/workspace.$path/process.tsx index 7d27ef5..870f6a3 100644 --- a/src/routes/workspace.$path/process.tsx +++ b/src/routes/workspace.$path/process.tsx @@ -3,12 +3,17 @@ import { getRouteApi, useRouter, } from "@tanstack/react-router"; -import { ChevronLeftIcon, PencilIcon, PlayIcon, Square } from "lucide-react"; +import { + ChevronLeftIcon, + PencilLineIcon, + PlayIcon, + Square, +} from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { CommandPreview } from "@/components/pixi/process/commandPreview"; import { Terminal } from "@/components/pixi/process/terminal"; import { TaskArgumentsDialog } from "@/components/pixi/tasks/taskArgsDialog"; -import { TaskDialog } from "@/components/pixi/tasks/taskDialog"; import { Badge } from "@/components/shadcn/badge"; import { Button } from "@/components/shadcn/button"; @@ -21,7 +26,6 @@ import { dependsOn as getTaskDependencies, description as getTaskDescription, } from "@/lib/pixi/workspace/task"; -import { type Feature, featureByTask } from "@/lib/pixi/workspace/workspace"; type RouteSearch = { environment: string; @@ -59,10 +63,6 @@ function ProcessComponent() { search.kind === "task" ? getTaskCommand(search.task) : search.command; const [argsDialogOpen, setArgsDialogOpen] = useState(false); - const [isEditingFeature, setIsEditingFeature] = useState( - null, - ); - // Track terminal dimensions so we can pass them when creating a PTY const [terminalDims, setTerminalDims] = useState<{ cols: number; @@ -129,24 +129,6 @@ function ProcessComponent() { } }; - const handleEdit = async () => { - if (!taskName) return; - try { - const feature = await featureByTask( - workspace.manifest, - taskName, - environment, - ); - if (feature) { - setIsEditingFeature(feature); - } else { - console.error("Could not find feature for task:", taskName); - } - } catch (err) { - console.error("Failed to get feature for task:", err); - } - }; - const onBack = () => router.history.back(); // Header title and subtitle @@ -177,10 +159,10 @@ function ProcessComponent() { type="button" variant="ghost" size="icon" - title="Edit Task" - onClick={handleEdit} + title="Set Task Arguments" + onClick={() => setArgsDialogOpen(true)} > - + )} diff --git a/src/lib/taskArgs.ts b/src/lib/taskArgs.ts new file mode 100644 index 0000000..dd3ce9b --- /dev/null +++ b/src/lib/taskArgs.ts @@ -0,0 +1,54 @@ +import { LazyStore } from "@tauri-apps/plugin-store"; + +import type { TaskArgument } from "@/lib/pixi/workspace/task"; + +export type TaskArgumentValues = + | { values: Record } // Argument name + Argument value + | { appended: string }; + +export function resolveTaskArgs( + taskArgumentValues: TaskArgumentValues, + args: TaskArgument[], +): string[] { + if ("values" in taskArgumentValues) { + return args.map((a) => { + const val = taskArgumentValues.values[a.name] ?? ""; + return val.trim() !== "" ? val : (a.default ?? ""); + }); + } + const appended = taskArgumentValues.appended.trim(); + return appended ? appended.split(/\s+/) : []; +} + +const store = new LazyStore("task-args.json"); + +function getKey( + workspaceRoot: string, + environment: string, + taskName: string, +): string { + return `${workspaceRoot}:${environment}:${taskName}`; +} + +export async function getTaskArgs( + workspaceRoot: string, + environment: string, + taskName: string, +): Promise { + const allArgs = + (await store.get>("taskArgs")) ?? {}; + return allArgs[getKey(workspaceRoot, environment, taskName)] ?? null; +} + +export async function saveTaskArgs( + workspaceRoot: string, + environment: string, + taskName: string, + args: TaskArgumentValues, +): Promise { + const allArgs = + (await store.get>("taskArgs")) ?? {}; + allArgs[getKey(workspaceRoot, environment, taskName)] = args; + await store.set("taskArgs", allArgs); + await store.save(); +} diff --git a/src/routes/workspace.$path/process.tsx b/src/routes/workspace.$path/process.tsx index 870f6a3..9d4080c 100644 --- a/src/routes/workspace.$path/process.tsx +++ b/src/routes/workspace.$path/process.tsx @@ -26,6 +26,12 @@ import { dependsOn as getTaskDependencies, description as getTaskDescription, } from "@/lib/pixi/workspace/task"; +import { + type TaskArgumentValues, + getTaskArgs, + resolveTaskArgs, + saveTaskArgs, +} from "@/lib/taskArgs"; type RouteSearch = { environment: string; @@ -74,6 +80,16 @@ function ProcessComponent() { setTerminalDims({ cols, rows }); }, []); + // Saved task arguments + const [savedArgValues, setSavedArgValues] = + useState(null); + useEffect(() => { + if (!isTask || !taskName) return; + void getTaskArgs(workspace.root, environment, taskName).then( + setSavedArgValues, + ); + }, [isTask, workspace.root, environment, taskName]); + // PTY handling const { isRunning, isBusy, start, kill, ptyId } = useProcess( search.kind === "task" @@ -109,18 +125,27 @@ function ProcessComponent() { ]); const handleStart = () => { - if (args.length === 0) { - const dims = terminalDimsRef.current!; - void start([], dims.cols, dims.rows); - } else { + // Show dialog if there are arguments without defaults and no saved values + if (!savedArgValues && args.some((a) => !a.default?.trim())) { setArgsDialogOpen(true); + return; } + + const dims = terminalDimsRef.current!; + void start( + resolveTaskArgs(savedArgValues ?? { values: {} }, args), + dims.cols, + dims.rows, + ); }; - const handleStartWithArgs = (taskArgs: string[]) => { + const handleStartWithArgs = async (values: TaskArgumentValues) => { + if (!taskName) return; setArgsDialogOpen(false); + setSavedArgValues(values); + await saveTaskArgs(workspace.root, environment, taskName, values); const dims = terminalDimsRef.current!; - void start(taskArgs, dims.cols, dims.rows); + void start(resolveTaskArgs(values, args), dims.cols, dims.rows); }; const handleKill = () => { @@ -185,7 +210,11 @@ function ProcessComponent() { {command && (

Command

- +
)} @@ -223,6 +252,7 @@ function ProcessComponent() { taskName={taskName} taskCommand={command} taskArguments={args} + initialValues={savedArgValues ?? undefined} onSubmit={handleStartWithArgs} /> )} From f261d5c831fc3b336cffcc96561a4dd57e4750d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 10:27:34 +0100 Subject: [PATCH 04/32] Don't allow editing args for running tasks --- src/components/pixi/process/processRow.tsx | 1 + src/routes/workspace.$path/process.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/pixi/process/processRow.tsx b/src/components/pixi/process/processRow.tsx index a77db0b..5558234 100644 --- a/src/components/pixi/process/processRow.tsx +++ b/src/components/pixi/process/processRow.tsx @@ -138,6 +138,7 @@ export function ProcessRow(props: ProcessRowProps) { size="icon" variant="ghost" title="Set Task Arguments" + disabled={isRunning} onClick={(event) => { event.stopPropagation(); setArgsDialogOpen(true); diff --git a/src/routes/workspace.$path/process.tsx b/src/routes/workspace.$path/process.tsx index 9d4080c..1eac52d 100644 --- a/src/routes/workspace.$path/process.tsx +++ b/src/routes/workspace.$path/process.tsx @@ -185,6 +185,7 @@ function ProcessComponent() { variant="ghost" size="icon" title="Set Task Arguments" + disabled={isRunning} onClick={() => setArgsDialogOpen(true)} > From 49cd60d25c8012471c27340aefd9ff5e224d26d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 10:30:44 +0100 Subject: [PATCH 05/32] use orange instead of red for missing arguments --- src/components/pixi/process/commandPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pixi/process/commandPreview.tsx b/src/components/pixi/process/commandPreview.tsx index 17b16d2..cfdd90c 100644 --- a/src/components/pixi/process/commandPreview.tsx +++ b/src/components/pixi/process/commandPreview.tsx @@ -39,7 +39,7 @@ export function CommandPreview({ command, args, values }: CommandPreviewProps) { return ( {"{{ "} {part.name} From 64363a3ae4af5c71bbeacecab56d43e134a67352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 10:37:05 +0100 Subject: [PATCH 06/32] Make arguments clickable --- .../pixi/process/commandPreview.tsx | 20 ++++++++++++++++--- src/components/pixi/tasks/taskArgsDialog.tsx | 4 ++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/pixi/process/commandPreview.tsx b/src/components/pixi/process/commandPreview.tsx index cfdd90c..9f85460 100644 --- a/src/components/pixi/process/commandPreview.tsx +++ b/src/components/pixi/process/commandPreview.tsx @@ -5,9 +5,15 @@ interface CommandPreviewProps { command: string; args: TaskArgument[]; values?: TaskArgumentValues; + onArgumentClick?: (name: string) => void; } -export function CommandPreview({ command, args, values }: CommandPreviewProps) { +export function CommandPreview({ + command, + args, + values, + onArgumentClick, +}: CommandPreviewProps) { const argValues = values && "values" in values ? values.values : {}; const appended = values && "appended" in values ? values.appended : ""; @@ -25,11 +31,17 @@ export function CommandPreview({ command, args, values }: CommandPreviewProps) { ); } + const handleClick = onArgumentClick + ? () => onArgumentClick(part.name) + : undefined; + if (part.resolved) { return ( {part.resolved} @@ -39,7 +51,9 @@ export function CommandPreview({ command, args, values }: CommandPreviewProps) { return ( {"{{ "} {part.name} diff --git a/src/components/pixi/tasks/taskArgsDialog.tsx b/src/components/pixi/tasks/taskArgsDialog.tsx index 924cfb2..c305762 100644 --- a/src/components/pixi/tasks/taskArgsDialog.tsx +++ b/src/components/pixi/tasks/taskArgsDialog.tsx @@ -76,6 +76,9 @@ export function TaskArgumentsDialog({ command={taskCommand} args={taskArguments} values={values} + onArgumentClick={(name) => + document.getElementById(`arg-${name}`)?.focus() + } /> )} @@ -83,6 +86,7 @@ export function TaskArgumentsDialog({ taskArguments.map((argument) => ( Date: Mon, 16 Feb 2026 10:56:50 +0100 Subject: [PATCH 07/32] Move buttons into command preview --- .../pixi/process/commandPreview.tsx | 9 ++- src/components/shadcn/button.tsx | 2 +- src/routes/workspace.$path/process.tsx | 61 ++++++++++--------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/components/pixi/process/commandPreview.tsx b/src/components/pixi/process/commandPreview.tsx index 9f85460..d4fe16c 100644 --- a/src/components/pixi/process/commandPreview.tsx +++ b/src/components/pixi/process/commandPreview.tsx @@ -1,3 +1,5 @@ +import type { ReactNode } from "react"; + import type { TaskArgument } from "@/lib/pixi/workspace/task"; import type { TaskArgumentValues } from "@/lib/taskArgs"; @@ -6,6 +8,7 @@ interface CommandPreviewProps { args: TaskArgument[]; values?: TaskArgumentValues; onArgumentClick?: (name: string) => void; + suffix?: ReactNode; } export function CommandPreview({ @@ -13,6 +16,7 @@ export function CommandPreview({ args, values, onArgumentClick, + suffix, }: CommandPreviewProps) { const argValues = values && "values" in values ? values.values : {}; const appended = values && "appended" in values ? values.appended : ""; @@ -20,7 +24,7 @@ export function CommandPreview({ const parts = parseCommand(command, argValues, args); return ( -
+
{parts.map((part, index) => { if (part.kind === "text") { @@ -70,6 +74,9 @@ export function CommandPreview({ )} + {suffix && ( +
{suffix}
+ )}
); } diff --git a/src/components/shadcn/button.tsx b/src/components/shadcn/button.tsx index 753240e..4821e0a 100644 --- a/src/components/shadcn/button.tsx +++ b/src/components/shadcn/button.tsx @@ -15,7 +15,7 @@ const buttonVariants = cva( "border-2 border-pfxl-card-border dark:border-pfxgsd-600 bg-white dark:bg-pfxgsd-700 hover:text-primary-foreground hover:bg-primary/90 hover:border-border", outline: "border-2 hover:bg-primary hover:text-accent-foreground", ghost: - "hover:bg-pfxgsl-200 hover:text-accent-foreground dark:hover:bg-accent/50", + "hover:bg-pfxgsl-300/50 hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { diff --git a/src/routes/workspace.$path/process.tsx b/src/routes/workspace.$path/process.tsx index 1eac52d..e50fedb 100644 --- a/src/routes/workspace.$path/process.tsx +++ b/src/routes/workspace.$path/process.tsx @@ -178,34 +178,6 @@ function ProcessComponent() { )}
-
- {isTask && ( - - )} - -
{command && ( @@ -215,6 +187,39 @@ function ProcessComponent() { command={command} args={args} values={savedArgValues ?? undefined} + onArgumentClick={ + isRunning ? undefined : () => setArgsDialogOpen(true) + } + suffix={ + <> + {isTask && ( + + )} + + + } />
)} From 08e83a73a16bd967063459e7b24ca1f648198eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 11:30:19 +0100 Subject: [PATCH 08/32] Add edit task button to TaskArgsDialog --- src/components/pixi/process/processRow.tsx | 25 +++++++++++++ src/components/pixi/tasks/taskArgsDialog.tsx | 15 ++++++++ src/routes/workspace.$path/process.tsx | 38 +++++++++++++++++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/components/pixi/process/processRow.tsx b/src/components/pixi/process/processRow.tsx index 5558234..cc5f83d 100644 --- a/src/components/pixi/process/processRow.tsx +++ b/src/components/pixi/process/processRow.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { CircularIcon } from "@/components/common/circularIcon"; import { Row } from "@/components/common/row"; import { TaskArgumentsDialog } from "@/components/pixi/tasks/taskArgsDialog"; +import { TaskDialog } from "@/components/pixi/tasks/taskDialog"; import { Button } from "@/components/shadcn/button"; import { useProcess } from "@/hooks/useProcess"; @@ -15,6 +16,7 @@ import { command as getTaskCommand, description as getTaskDescription, } from "@/lib/pixi/workspace/task"; +import { type Feature, featureByTask } from "@/lib/pixi/workspace/workspace"; import { type TaskArgumentValues, getTaskArgs, @@ -42,6 +44,17 @@ export function ProcessRow(props: ProcessRowProps) { const taskCommand = props.kind === "task" ? getTaskCommand(props.task) : undefined; const [argsDialogOpen, setArgsDialogOpen] = useState(false); + const [feature, setFeature] = useState(null); + + const handleEditTask = async () => { + if (props.kind !== "task") return; + const f = await featureByTask( + workspace.root, + props.taskName, + props.environment, + ); + if (f) setFeature(f); + }; // Saved task arguments const [savedArgValues, setSavedArgValues] = @@ -194,6 +207,18 @@ export function ProcessRow(props: ProcessRowProps) { taskArguments={args} initialValues={savedArgValues ?? undefined} onSubmit={handleStartWithArgs} + onEdit={handleEditTask} + /> + )} + + {feature && props.kind === "task" && ( + !o && setFeature(null)} + workspace={workspace} + feature={feature} + editTask={props.task} + editTaskName={props.taskName} /> )} diff --git a/src/components/pixi/tasks/taskArgsDialog.tsx b/src/components/pixi/tasks/taskArgsDialog.tsx index c305762..c30f973 100644 --- a/src/components/pixi/tasks/taskArgsDialog.tsx +++ b/src/components/pixi/tasks/taskArgsDialog.tsx @@ -1,3 +1,4 @@ +import { PencilIcon } from "lucide-react"; import React, { useState } from "react"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; @@ -24,6 +25,7 @@ interface TaskArgumentsDialogProps { taskArguments: TaskArgument[]; initialValues?: TaskArgumentValues; onSubmit: (values: TaskArgumentValues) => void; + onEdit?: () => void; } export function TaskArgumentsDialog({ @@ -34,6 +36,7 @@ export function TaskArgumentsDialog({ taskArguments, initialValues, onSubmit, + onEdit, }: TaskArgumentsDialogProps) { const [values, setValues] = useState(() => { if (taskArguments.length > 0) { @@ -112,6 +115,18 @@ export function TaskArgumentsDialog({ + {onEdit && ( + + )} + } + /> + + {/* Auto-detected arguments from {{ var }} in command */} + {detectedArgNames.map((argName, index) => { + // Default is required if any previous arg has a default + const defaultRequired = detectedArgNames + .slice(0, index) + .some((prev) => !!argDefaults[prev]); + + return ( +
+ { + const newName = e.target.value; + const pattern = new RegExp( + `\\{\\{\\s*${argName}\\s*\\}\\}`, + ); + // Remove entire {{ arg }} if name is cleared, otherwise rename + setCommand((prev) => + prev.replace( + pattern, + newName ? `{{ ${newName} }}` : "", + ), + ); + // Transfer default value to new name + setArgDefaults((prev) => { + const { [argName]: value, ...rest } = prev; + if (newName && value != null) { + return { ...rest, [newName]: value }; + } + return rest; + }); + }} + className="flex-1" + /> + + setArgDefaults((prev) => ({ + ...prev, + [argName]: e.target.value, + })) + } + required={defaultRequired} + className="flex-1" + /> +
+ ); + })} + {/* Advanced Settings */} - {/* Task Arguments */} - - {taskArguments.map((arg, index) => ( -
- - - -
- ))} -
- setNewArgName(e.target.value)} - label="Argument Name" - className="flex-1" - /> - setNewArgDefault(e.target.value)} - label="Default Value" - className="flex-1" - /> - -
-
- {/* Caching */} Date: Mon, 16 Feb 2026 13:53:12 +0100 Subject: [PATCH 10/32] Avoid {{ {{ {{ {{ nesting }} }} }} }} --- src/components/pixi/tasks/taskDialog.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/pixi/tasks/taskDialog.tsx b/src/components/pixi/tasks/taskDialog.tsx index c8e71a3..579b56d 100644 --- a/src/components/pixi/tasks/taskDialog.tsx +++ b/src/components/pixi/tasks/taskDialog.tsx @@ -381,8 +381,21 @@ export function TaskDialog({ const input = commandInputRef.current; if (!input) return; - const selStart = input.selectionStart ?? command.length; - const selEnd = input.selectionEnd ?? selStart; + let selStart = input.selectionStart ?? command.length; + let selEnd = input.selectionEnd ?? selStart; + + // If cursor is inside a {{ }} block, move insertion point after it + for (const match of command.matchAll( + /\{\{[^}]*\}\}/g, + )) { + const matchEnd = match.index + match[0].length; + if (selStart > match.index && selStart < matchEnd) { + selStart = matchEnd; + selEnd = matchEnd; + break; + } + } + const selected = command.slice(selStart, selEnd); const argName = selected.trim() || "variable"; const insertion = `{{ ${argName} }}`; From cb2b87b2d99a7cfd918c5a7ded504f4db011bc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 14:08:58 +0100 Subject: [PATCH 11/32] Automatically offer file picker --- src/components/pixi/tasks/taskArgsDialog.tsx | 73 +++++++++++++++----- src/lib/taskArgs.ts | 15 ++++ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/components/pixi/tasks/taskArgsDialog.tsx b/src/components/pixi/tasks/taskArgsDialog.tsx index c30f973..c43e144 100644 --- a/src/components/pixi/tasks/taskArgsDialog.tsx +++ b/src/components/pixi/tasks/taskArgsDialog.tsx @@ -1,4 +1,5 @@ -import { PencilIcon } from "lucide-react"; +import { open as openDialog } from "@tauri-apps/plugin-dialog"; +import { FolderOpenIcon, PencilIcon } from "lucide-react"; import React, { useState } from "react"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; @@ -15,7 +16,11 @@ import { import { Input } from "@/components/shadcn/input"; import type { TaskArgument } from "@/lib/pixi/workspace/task"; -import type { TaskArgumentValues } from "@/lib/taskArgs"; +import { + type TaskArgumentValues, + isDirectoryArgument, + isPathArgument, +} from "@/lib/taskArgs"; interface TaskArgumentsDialogProps { open: boolean; @@ -86,23 +91,53 @@ export function TaskArgumentsDialog({ )} {"values" in values ? ( - taskArguments.map((argument) => ( - - setValues({ - values: { - ...values.values, - [argument.name]: event.target.value, - }, - }) - } - /> - )) + taskArguments.map((argument) => { + const isPath = isPathArgument(argument.name); + const isDirectory = isDirectoryArgument(argument.name); + + return ( + + setValues({ + values: { + ...values.values, + [argument.name]: event.target.value, + }, + }) + } + suffix={ + isPath ? ( + + ) : undefined + } + /> + ); + }) ) : ( } // Argument name + Argument value | { appended: string }; +export function isPathArgument(name: string): boolean { + const lower = name.toLowerCase(); + return ( + lower.includes("path") || + lower.includes("file") || + lower.includes("dir") || + lower.includes("folder") + ); +} + +export function isDirectoryArgument(name: string): boolean { + const lower = name.toLowerCase(); + return lower.includes("dir") || lower.includes("folder"); +} + export function resolveTaskArgs( taskArgumentValues: TaskArgumentValues, args: TaskArgument[], From 1cb709bac9857936147f4cf0a76d064f70e4e923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 16 Feb 2026 14:17:50 +0100 Subject: [PATCH 12/32] Properly ensure task argument values --- src/components/pixi/process/processRow.tsx | 10 ++++++++-- src/routes/workspace.$path/process.tsx | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/pixi/process/processRow.tsx b/src/components/pixi/process/processRow.tsx index cc5f83d..51191ae 100644 --- a/src/components/pixi/process/processRow.tsx +++ b/src/components/pixi/process/processRow.tsx @@ -105,8 +105,14 @@ export function ProcessRow(props: ProcessRowProps) { const handleStart = () => { if (props.kind !== "task") return; - // Show dialog if there are arguments without defaults and no saved values - if (!savedArgValues && args.some((a) => !a.default?.trim())) { + // Show dialog if any required argument is missing a value + const hasRequiredArgWithoutValue = args.some((a) => { + if (a.default?.trim()) return false; + if (!savedArgValues || !("values" in savedArgValues)) return true; + return !savedArgValues.values[a.name]?.trim(); + }); + + if (hasRequiredArgWithoutValue) { setArgsDialogOpen(true); return; } diff --git a/src/routes/workspace.$path/process.tsx b/src/routes/workspace.$path/process.tsx index 0abdfa4..ad4d61b 100644 --- a/src/routes/workspace.$path/process.tsx +++ b/src/routes/workspace.$path/process.tsx @@ -148,8 +148,13 @@ function ProcessComponent() { ]); const handleStart = () => { - // Show dialog if there are arguments without defaults and no saved values - if (!savedArgValues && args.some((a) => !a.default?.trim())) { + // Show dialog if any required argument is missing a value + const hasRequiredArgWithoutValue = args.some((a) => { + if (a.default?.trim()) return false; + if (!savedArgValues || !("values" in savedArgValues)) return true; + return !savedArgValues.values[a.name]?.trim(); + }); + if (hasRequiredArgWithoutValue) { setArgsDialogOpen(true); return; } From c782011743877bb0d235c3518a78297d7a6f00c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 17 Feb 2026 13:32:04 +0100 Subject: [PATCH 13/32] hide play button if task cannot be executed --- src/components/pixi/process/processRow.tsx | 60 ++++++++++------------ src/lib/taskArgs.ts | 14 +++++ src/routes/workspace.$path/process.tsx | 44 +++++++--------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/components/pixi/process/processRow.tsx b/src/components/pixi/process/processRow.tsx index 51191ae..7c48dec 100644 --- a/src/components/pixi/process/processRow.tsx +++ b/src/components/pixi/process/processRow.tsx @@ -19,6 +19,7 @@ import { import { type Feature, featureByTask } from "@/lib/pixi/workspace/workspace"; import { type TaskArgumentValues, + canRunDirectly, getTaskArgs, resolveTaskArgs, saveTaskArgs, @@ -102,21 +103,10 @@ export function ProcessRow(props: ProcessRowProps) { }); }; + const runnable = canRunDirectly(args, savedArgValues); + const handleStart = () => { if (props.kind !== "task") return; - - // Show dialog if any required argument is missing a value - const hasRequiredArgWithoutValue = args.some((a) => { - if (a.default?.trim()) return false; - if (!savedArgValues || !("values" in savedArgValues)) return true; - return !savedArgValues.values[a.name]?.trim(); - }); - - if (hasRequiredArgWithoutValue) { - setArgsDialogOpen(true); - return; - } - navigateToProcess( true, resolveTaskArgs(savedArgValues ?? { values: {} }, args), @@ -165,27 +155,29 @@ export function ProcessRow(props: ProcessRowProps) { > - + {(runnable || isRunning) && ( + + )} ) : ( )} - + {(runnable || isRunning) && ( + + )} } /> From 1a777bfaa3fcee888050563aa5eb54235a90d1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 17 Feb 2026 13:34:52 +0100 Subject: [PATCH 14/32] set "required" for inputs --- src/components/pixi/tasks/taskArgsDialog.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/pixi/tasks/taskArgsDialog.tsx b/src/components/pixi/tasks/taskArgsDialog.tsx index c43e144..1c3bf88 100644 --- a/src/components/pixi/tasks/taskArgsDialog.tsx +++ b/src/components/pixi/tasks/taskArgsDialog.tsx @@ -101,6 +101,7 @@ export function TaskArgumentsDialog({ id={`arg-${argument.name}`} label={argument.name} placeholder={argument.default} + required={!argument.default} value={values.values[argument.name] ?? ""} onChange={(event) => setValues({ @@ -171,18 +172,7 @@ export function TaskArgumentsDialog({ > Cancel - +
From bafe4882c0943ae8522098a9c7e658842f5af1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Wed, 18 Feb 2026 14:44:35 +0100 Subject: [PATCH 15/32] feat: Implement "inspect" tab --- package.json | 1 + pnpm-lock.yaml | 35 +- src-tauri/Cargo.lock | 40 +-- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 2 + src-tauri/src/pixi/workspace/list.rs | 42 +++ src-tauri/src/pixi/workspace/mod.rs | 1 + src-tauri/src/pixi/workspace/workspace.rs | 5 + src/components/common/circularIcon.tsx | 14 +- .../pixi/environments/environment.tsx | 2 +- .../pixi/environments/environments.tsx | 4 +- src/components/pixi/inspect/inspect.tsx | 301 ++++++++++++++++++ src/components/pixi/inspect/packageDialog.tsx | 157 +++++++++ src/lib/pixi/workspace/list.ts | 56 ++++ src/lib/pixi/workspace/workspace.ts | 4 + src/routes/workspace.$path/index.tsx | 10 +- src/routes/workspace.$path/route.tsx | 15 +- 17 files changed, 622 insertions(+), 69 deletions(-) create mode 100644 src-tauri/src/pixi/workspace/list.rs create mode 100644 src/components/pixi/inspect/inspect.tsx create mode 100644 src/components/pixi/inspect/packageDialog.tsx create mode 100644 src/lib/pixi/workspace/list.ts diff --git a/package.json b/package.json index 392bf91..e164f44 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.563.0", "next-themes": "^0.4.6", + "pretty-bytes": "^7.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-xtermjs": "^1.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac2cdb6..5c524b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + pretty-bytes: + specifier: ^7.1.0 + version: 7.1.0 react: specifier: ^19.2.4 version: 19.2.4 @@ -1051,79 +1054,66 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -1231,28 +1221,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -1425,35 +1411,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.10.0': resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.10.0': resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.10.0': resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.10.0': resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.10.0': resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==} @@ -2761,28 +2742,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3281,6 +3258,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@7.1.0: + resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} + engines: {node: '>=20'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -7339,6 +7320,8 @@ snapshots: prettier@3.8.1: {} + pretty-bytes@7.1.0: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7edb3f2..7b0373b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -955,7 +955,6 @@ dependencies = [ [[package]] name = "barrier_cell" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "parking_lot", "thiserror 2.0.18", @@ -2446,7 +2445,6 @@ dependencies = [ [[package]] name = "fancy_display" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "console", ] @@ -5787,7 +5785,6 @@ dependencies = [ [[package]] name = "pixi_api" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "console", "dunce", @@ -5805,6 +5802,8 @@ dependencies = [ "pixi_pypi_spec", "pixi_spec", "pixi_utils", + "pixi_uv_conversions", + "pypi_modifiers", "rattler_conda_types", "rattler_lock", "rattler_repodata_gateway", @@ -5816,13 +5815,15 @@ dependencies = [ "tokio", "tracing", "url", + "uv-distribution", + "uv-distribution-types", "uv-normalize", + "uv-types", ] [[package]] name = "pixi_auth" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "pixi_config", "rattler_networking", @@ -5832,7 +5833,6 @@ dependencies = [ [[package]] name = "pixi_build_discovery" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "dunce", "itertools 0.14.0", @@ -5855,7 +5855,6 @@ dependencies = [ [[package]] name = "pixi_build_frontend" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "fs-err", "futures", @@ -5876,7 +5875,6 @@ dependencies = [ [[package]] name = "pixi_build_type_conversions" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "itertools 0.14.0", "ordermap", @@ -5889,7 +5887,6 @@ dependencies = [ [[package]] name = "pixi_build_types" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "ordermap", "pixi_stable_hash", @@ -5904,7 +5901,6 @@ dependencies = [ [[package]] name = "pixi_command_dispatcher" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "async-fd-lock", "base64 0.22.1", @@ -5963,7 +5959,6 @@ dependencies = [ [[package]] name = "pixi_config" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "clap", "console", @@ -5989,7 +5984,6 @@ dependencies = [ [[package]] name = "pixi_consts" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "console", "rattler_cache", @@ -6000,7 +5994,6 @@ dependencies = [ [[package]] name = "pixi_core" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "async-once-cell", "barrier_cell", @@ -6097,7 +6090,6 @@ dependencies = [ [[package]] name = "pixi_default_versions" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "rattler_conda_types", ] @@ -6105,7 +6097,6 @@ dependencies = [ [[package]] name = "pixi_diff" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "ahash 0.8.12", "console", @@ -6123,7 +6114,6 @@ dependencies = [ [[package]] name = "pixi_git" version = "0.0.1" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "dashmap", "dunce", @@ -6144,7 +6134,6 @@ dependencies = [ [[package]] name = "pixi_glob" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "dashmap", "fs-err", @@ -6161,7 +6150,6 @@ dependencies = [ [[package]] name = "pixi_install_pypi" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "ahash 0.8.12", "chrono", @@ -6222,7 +6210,6 @@ dependencies = [ [[package]] name = "pixi_manifest" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "chrono", "console", @@ -6263,7 +6250,6 @@ dependencies = [ [[package]] name = "pixi_path" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "fs-err", "serde", @@ -6274,7 +6260,6 @@ dependencies = [ [[package]] name = "pixi_progress" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "indicatif", "parking_lot", @@ -6283,7 +6268,6 @@ dependencies = [ [[package]] name = "pixi_pypi_spec" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "itertools 0.14.0", "pep440_rs", @@ -6303,7 +6287,6 @@ dependencies = [ [[package]] name = "pixi_python_status" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "rattler", ] @@ -6311,7 +6294,6 @@ dependencies = [ [[package]] name = "pixi_record" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "file_url", "itertools 0.14.0", @@ -6334,7 +6316,6 @@ dependencies = [ [[package]] name = "pixi_reporters" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "console", "futures", @@ -6366,7 +6347,6 @@ dependencies = [ [[package]] name = "pixi_spec" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "dirs", "file_url", @@ -6392,7 +6372,6 @@ dependencies = [ [[package]] name = "pixi_spec_containers" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "indexmap 2.13.0", "itertools 0.14.0", @@ -6404,7 +6383,6 @@ dependencies = [ [[package]] name = "pixi_stable_hash" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "ordermap", "rattler_conda_types", @@ -6416,7 +6394,6 @@ dependencies = [ [[package]] name = "pixi_toml" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "digest", "hex", @@ -6431,7 +6408,6 @@ dependencies = [ [[package]] name = "pixi_url" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "bzip2 0.6.1", "dashmap", @@ -6461,7 +6437,6 @@ dependencies = [ [[package]] name = "pixi_utils" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "async-fd-lock", "fs-err", @@ -6497,7 +6472,6 @@ dependencies = [ [[package]] name = "pixi_uv_context" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "fs-err", "miette 7.6.0", @@ -6522,7 +6496,6 @@ dependencies = [ [[package]] name = "pixi_uv_conversions" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "chrono", "dunce", @@ -6557,7 +6530,6 @@ dependencies = [ [[package]] name = "pixi_variant" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "pixi_build_types", "rattler_lock", @@ -6906,7 +6878,6 @@ dependencies = [ [[package]] name = "pypi_mapping" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "async-once-cell", "dashmap", @@ -6936,7 +6907,6 @@ dependencies = [ [[package]] name = "pypi_modifiers" version = "0.1.0" -source = "git+https://github.com/prefix-dev/pixi?rev=ce8654e0a401024dda627bc550059a89fb4add0a#ce8654e0a401024dda627bc550059a89fb4add0a" dependencies = [ "miette 7.6.0", "pixi_default_versions", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4cec93b..dd377b5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,7 +27,7 @@ miette = "7" notify = "8" notify-debouncer-full = "0.7" percent-encoding = "2" -pixi_api = { package = "pixi_api", git = "https://github.com/prefix-dev/pixi", rev = "ce8654e0a401024dda627bc550059a89fb4add0a" } +pixi_api = { package = "pixi_api", path = "../../pixi/crates/pixi_api" } portable-pty = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d0235dd..b940703 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -85,6 +85,7 @@ pub fn run(workspace_path: Option) { pixi::workspace::add::add_conda_deps, pixi::workspace::add::add_pypi_deps, pixi::workspace::init::init, + pixi::workspace::list::list_packages, pixi::workspace::reinstall::reinstall, pixi::workspace::remove::remove_conda_deps, pixi::workspace::remove::remove_pypi_deps, @@ -111,6 +112,7 @@ pub fn run(workspace_path: Option) { pixi::workspace::workspace::list_platforms, pixi::workspace::workspace::add_platforms, pixi::workspace::workspace::remove_platforms, + pixi::workspace::workspace::current_platform, pixi::workspace::task::list_tasks, pixi::workspace::task::add_task, pixi::workspace::task::remove_task, diff --git a/src-tauri/src/pixi/workspace/list.rs b/src-tauri/src/pixi/workspace/list.rs new file mode 100644 index 0000000..cf2ccfe --- /dev/null +++ b/src-tauri/src/pixi/workspace/list.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use crate::{ + error::Error, + utils::{self, spawn_local}, +}; +use pixi_api::{ + core::environment::LockFileUsage, rattler_conda_types::Platform, workspace::Package, +}; +use tauri::{Runtime, Window}; + +#[tauri::command] +#[allow(clippy::too_many_arguments)] +pub async fn list_packages( + window: Window, + workspace: PathBuf, + regex: Option, + platform: Option, + environment: Option, + explicit: bool, + no_install: bool, + lock_file_usage: LockFileUsage, +) -> Result, Error> { + spawn_local(move || async move { + let platform: Option = + platform.map(|p| p.parse::()).transpose().unwrap(); + + let packages = utils::workspace_context(window, workspace)? + .list_packages( + regex, + platform, + environment, + explicit, + no_install, + lock_file_usage, + ) + .await?; + + Ok(packages) + }) + .await +} diff --git a/src-tauri/src/pixi/workspace/mod.rs b/src-tauri/src/pixi/workspace/mod.rs index dd8fc38..7c3d9a8 100644 --- a/src-tauri/src/pixi/workspace/mod.rs +++ b/src-tauri/src/pixi/workspace/mod.rs @@ -1,5 +1,6 @@ pub mod add; pub mod init; +pub mod list; pub mod reinstall; pub mod remove; pub mod search; diff --git a/src-tauri/src/pixi/workspace/workspace.rs b/src-tauri/src/pixi/workspace/workspace.rs index 747b11e..e1a714a 100644 --- a/src-tauri/src/pixi/workspace/workspace.rs +++ b/src-tauri/src/pixi/workspace/workspace.rs @@ -184,6 +184,11 @@ pub async fn remove_platforms( .await } +#[tauri::command] +pub fn current_platform() -> String { + Platform::current().to_string() +} + #[tauri::command] pub async fn list_features( window: Window, diff --git a/src/components/common/circularIcon.tsx b/src/components/common/circularIcon.tsx index 2347d16..c206e3f 100644 --- a/src/components/common/circularIcon.tsx +++ b/src/components/common/circularIcon.tsx @@ -11,6 +11,8 @@ import { } from "lucide-react"; import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + export const presetIcons = { task: GalleryVerticalEndIcon, feature: PuzzleIcon, @@ -29,19 +31,29 @@ interface CircularIconProps { children?: ReactNode; icon?: PresetIcon; size?: "sm" | "md"; + variant?: "default" | "muted"; + className?: string; } export function CircularIcon({ children, icon, size = "sm", + variant = "default", + className, }: CircularIconProps) { const sizeClasses = size === "md" ? "h-12 w-12" : "h-9 w-9"; const IconComponent = icon ? presetIcons[icon] : null; return (
{IconComponent ? : children}
diff --git a/src/components/pixi/environments/environment.tsx b/src/components/pixi/environments/environment.tsx index 2e45c67..151b022 100644 --- a/src/components/pixi/environments/environment.tsx +++ b/src/components/pixi/environments/environment.tsx @@ -270,7 +270,7 @@ export function Environment({ name, tasks, filter }: EnvironmentProps) { value={commandInput} onChange={(e) => setCommandInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && runFreeformTask()} - icon={} + icon={} /> {filteredCommands.map(([id, { command, editor }]) => ( setLocalSearch(event.target.value)} - placeholder="Search…" + placeholder="Search tasks…" autoComplete="off" spellCheck={false} autoCorrect="off" autoFocus={true} - icon={} + icon={} /> {Object.entries(tasks) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx new file mode 100644 index 0000000..e19b187 --- /dev/null +++ b/src/components/pixi/inspect/inspect.tsx @@ -0,0 +1,301 @@ +import { getRouteApi } from "@tanstack/react-router"; +import { + ChevronRightIcon, + ListIcon, + ListTreeIcon, + PackageCheckIcon, + SearchIcon, +} from "lucide-react"; +import prettyBytes from "pretty-bytes"; +import { useEffect, useState } from "react"; + +import { CircularIcon } from "@/components/common/circularIcon"; +import { PreferencesGroup } from "@/components/common/preferencesGroup"; +import { Row } from "@/components/common/row"; +import { PackageDialog } from "@/components/pixi/inspect/packageDialog"; +import { Button } from "@/components/shadcn/button"; +import { Input } from "@/components/shadcn/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select"; + +import { type Package, listPackages } from "@/lib/pixi/workspace/list"; + +export function Inspect() { + const { workspace, environments, platforms, currentPlatform } = + getRouteApi("/workspace/$path").useLoaderData(); + const { search = "" } = getRouteApi("/workspace/$path/").useSearch(); + const navigate = getRouteApi("/workspace/$path").useNavigate(); + + const [localSearch, setLocalSearch] = useState(search); + const [selectedEnvironment, setSelectedEnvironment] = + useState("default"); + const [selectedPlatform, setSelectedPlatform] = + useState(currentPlatform); + + const [treeMode, setTreeMode] = useState(false); + const [packages, setPackages] = useState([]); + const [selectedPackage, setSelectedPackage] = useState(null); + const [expanded, setExpanded] = useState>(new Set()); + + // Sync local state when URL search changes externally + useEffect(() => { + setLocalSearch(search); + }, [search]); + + // Debounced URL update + useEffect(() => { + const timeout = setTimeout(() => { + if (localSearch !== search) { + navigate({ + search: (prev) => ({ ...prev, search: localSearch }), + replace: true, + }); + } + }, 300); + return () => clearTimeout(timeout); + }, [localSearch, search, navigate]); + + // Fetch packages when environment/platform changes + useEffect(() => { + let cancelled = false; + + listPackages(workspace.root, { + environment: selectedEnvironment, + platform: selectedPlatform, + }).then((pkgs) => { + if (!cancelled) { + setPackages(pkgs); + } + }); + + return () => { + cancelled = true; + }; + }, [workspace.root, selectedEnvironment, selectedPlatform]); + + // Reset platform when environment changes and current platform is unavailable + const availablePlatforms = platforms[selectedEnvironment] ?? []; + useEffect(() => { + const available = platforms[selectedEnvironment] ?? []; + if (!available.includes(selectedPlatform)) { + setSelectedPlatform(currentPlatform); + } + }, [platforms, selectedEnvironment, selectedPlatform, currentPlatform]); + + // Reset expanded nodes when switching modes or refetching + useEffect(() => { + setExpanded(new Set()); + }, [treeMode, packages]); + + // Client-side search filtering + const needle = localSearch.trim().toLowerCase(); + const filteredPackages = needle + ? packages.filter((pkg) => pkg.name.toLowerCase().includes(needle)) + : packages; + + // Dependency tree + const packageMap = new Map(); + for (const pkg of packages) { + packageMap.set(pkg.name, pkg); + } + + function getDependencyNames(pkg: Package): string[] { + return [ + ...new Set( + pkg.depends + .map((dep) => dep.split(/[\s[]/)[0]) + .filter((name) => packageMap.has(name)), + ), + ]; + } + + function toggleExpand(name: string) { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + } + + function renderPackageRow(pkg: Package, depth: number = 0, nodeKey?: string) { + const depNames = treeMode ? getDependencyNames(pkg) : []; + const hasChildren = depNames.length > 0; + const expandKey = nodeKey ?? pkg.name; + const isOpen = expanded.has(expandKey); + + return ( +
0 ? { marginLeft: depth * 36 } : undefined} + > + {treeMode && + (hasChildren ? ( + + ) : ( +
+ ))} + + + + ) : ( + + ) + } + suffix={ + pkg.size_bytes != null ? ( + + {prettyBytes(pkg.size_bytes)} + + ) : undefined + } + onClick={() => setSelectedPackage(pkg)} + /> +
+ ); + } + + // Recursive tree renderer + function renderTree( + pkg: Package, + depth: number, + visited: Set, + parentKey: string = "", + ): React.ReactNode { + if (visited.has(pkg.name)) return null; + const nextVisited = new Set(visited); + nextVisited.add(pkg.name); + + const depNames = getDependencyNames(pkg); + const nodeKey = parentKey ? `${parentKey}>${pkg.name}` : pkg.name; + const isOpen = expanded.has(nodeKey); + + return ( +
+ {renderPackageRow(pkg, depth, nodeKey)} + {isOpen && + depNames.sort().map((depName) => { + const depPkg = packageMap.get(depName); + if (!depPkg) return null; + return renderTree(depPkg, depth + 1, nextVisited, nodeKey); + })} +
+ ); + } + + // Determine roots for tree mode + const roots = filteredPackages.filter((pkg) => pkg.is_explicit); + const treeRoots = roots.length > 0 ? roots : filteredPackages; + + // Sort packages alphabetically + const sortedPackages = [...filteredPackages].sort((a, b) => + a.name.localeCompare(b.name), + ); + const sortedTreeRoots = [...treeRoots].sort((a, b) => + a.name.localeCompare(b.name), + ); + + return ( + <> + {/* Toolbar */} +
+ setLocalSearch(event.target.value)} + placeholder="Search packages…" + autoComplete="off" + spellCheck={false} + autoCorrect="off" + icon={} + /> +
+ {/* Environment */} + + + {/* Platform */} + +
+
+ + {/* Content */} + setTreeMode((prev) => !prev)} + > + {treeMode ? : } + {treeMode ? "All Packages" : "Dependency Tree"} + + } + > + {treeMode + ? sortedTreeRoots.map((pkg) => renderTree(pkg, 0, new Set())) + : sortedPackages.map((pkg) => renderPackageRow(pkg))} + + + {/* Package detail dialog */} + {selectedPackage && ( + !open && setSelectedPackage(null)} + /> + )} + + ); +} diff --git a/src/components/pixi/inspect/packageDialog.tsx b/src/components/pixi/inspect/packageDialog.tsx new file mode 100644 index 0000000..4974261 --- /dev/null +++ b/src/components/pixi/inspect/packageDialog.tsx @@ -0,0 +1,157 @@ +import prettyBytes from "pretty-bytes"; + +import { PreferencesGroup } from "@/components/common/preferencesGroup"; +import { Row } from "@/components/common/row"; +import { Badge } from "@/components/shadcn/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/shadcn/dialog"; + +import type { Package } from "@/lib/pixi/workspace/list"; + +interface PackageDialogProps { + pkg: Package; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function PackageDialog({ pkg, open, onOpenChange }: PackageDialogProps) { + return ( + + + + {pkg.name} + + +
+ {/* General */} + + + {pkg.requested_spec && ( + + )} + + + {pkg.build && } + + {pkg.license && ( + + )} + {pkg.source && ( + + )} + + + {pkg.is_editable && ( + + )} + + + {/* File / Location */} + + {pkg.file_name && ( + + )} + {pkg.url && } + {pkg.subdir && ( + + )} + {pkg.platform && ( + + )} + {pkg.arch && } + {pkg.noarch && ( + + )} + + + {/* Size & Integrity */} + {(pkg.size_bytes != null || + pkg.sha256 || + pkg.md5 || + pkg.timestamp != null) && ( + + {pkg.size_bytes != null && ( + + )} + {pkg.sha256 && ( + + )} + {pkg.md5 && } + {pkg.timestamp != null && ( + + )} + + )} + + {/* Dependencies */} + {pkg.depends.length > 0 && ( + + {[...pkg.depends].sort().map((dep) => ( + + ))} + + )} + + {/* Constrains */} + {pkg.constrains.length > 0 && ( + + {[...pkg.constrains].sort().map((c) => ( + + ))} + + )} + + {/* Track Features */} + {pkg.track_features.length > 0 && ( + +
+ {pkg.track_features.map((f) => ( + + {f} + + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/src/lib/pixi/workspace/list.ts b/src/lib/pixi/workspace/list.ts new file mode 100644 index 0000000..b2d42eb --- /dev/null +++ b/src/lib/pixi/workspace/list.ts @@ -0,0 +1,56 @@ +import { invoke } from "@tauri-apps/api/core"; + +import type { LockFileUsage } from "@/lib/pixi/workspace/reinstall"; + +export type PackageKind = "conda" | "pypi"; + +export interface Package { + name: string; + version: string; + build: string | null; + build_number: number | null; + size_bytes: number | null; + kind: PackageKind; + source: string | null; + license: string | null; + license_family: string | null; + is_explicit: boolean; + is_editable?: boolean; + md5: string | null; + sha256: string | null; + arch: string | null; + platform: string | null; + subdir: string | null; + timestamp: number | null; + noarch: string | null; + file_name: string | null; + url: string | null; + requested_spec: string | null; + constrains: string[]; + depends: string[]; + track_features: string[]; +} + +export interface ListPackagesOptions { + regex?: string | null; + platform?: string | null; + environment?: string | null; + explicit?: boolean; + noInstall?: boolean; + lockFileUsage?: LockFileUsage; +} + +export function listPackages( + workspace: string, + options: ListPackagesOptions = {}, +): Promise { + return invoke("list_packages", { + workspace, + regex: options.regex ?? null, + platform: options.platform ?? null, + environment: options.environment ?? null, + explicit: options.explicit ?? false, + noInstall: options.noInstall ?? false, + lockFileUsage: options.lockFileUsage ?? "Update", + }); +} diff --git a/src/lib/pixi/workspace/workspace.ts b/src/lib/pixi/workspace/workspace.ts index 750a62e..06e26ac 100644 --- a/src/lib/pixi/workspace/workspace.ts +++ b/src/lib/pixi/workspace/workspace.ts @@ -84,6 +84,10 @@ export function listPlatforms( return invoke>("list_platforms", { workspace }); } +export function currentPlatform(): Promise { + return invoke("current_platform"); +} + export async function addPlatforms( workspace: string, platforms: string[], diff --git a/src/routes/workspace.$path/index.tsx b/src/routes/workspace.$path/index.tsx index 27f90d5..6ea730f 100644 --- a/src/routes/workspace.$path/index.tsx +++ b/src/routes/workspace.$path/index.tsx @@ -1,10 +1,11 @@ import { createFileRoute, getRouteApi } from "@tanstack/react-router"; -import { Bug, CirclePlay, ScrollTextIcon } from "lucide-react"; +import { Bug, CirclePlay, PackageIcon, ScrollTextIcon } from "lucide-react"; import { AppMenu } from "@/components/common/appMenu"; import { Header } from "@/components/common/header"; import { Debug } from "@/components/pixi/debug"; import { Environments } from "@/components/pixi/environments/environments"; +import { Inspect } from "@/components/pixi/inspect/inspect"; import { Manifest } from "@/components/pixi/manifest/manifest"; import { Tabs, @@ -48,6 +49,10 @@ function WorkspaceComponent() { Run + + + Inspect + Manifest @@ -62,6 +67,9 @@ function WorkspaceComponent() { + + + diff --git a/src/routes/workspace.$path/route.tsx b/src/routes/workspace.$path/route.tsx index 7b3c896..9d1c08b 100644 --- a/src/routes/workspace.$path/route.tsx +++ b/src/routes/workspace.$path/route.tsx @@ -22,6 +22,7 @@ import { type Environment, type Feature, type Workspace, + currentPlatform, getWorkspace, listChannels, listEnvironments, @@ -39,22 +40,32 @@ export interface WorkspaceLoaderData { environments: Environment[]; channels: Record; platforms: Record; + currentPlatform: string; } export const Route = createFileRoute("/workspace/$path")({ loader: async ({ params: { path } }): Promise => { console.info("Load manifest:", path); const workspace = await getWorkspace(path); - const [tasks, features, environments, channels, platforms] = + const [tasks, features, environments, channels, platforms, hostPlatform] = await Promise.all([ listTask(workspace.root), listFeatures(workspace.root), listEnvironments(workspace.root), listChannels(workspace.root), listPlatforms(workspace.root), + currentPlatform(), ]); await addRecentWorkspace(workspace); - return { workspace, tasks, features, environments, channels, platforms }; + return { + workspace, + tasks, + features, + environments, + channels, + platforms, + currentPlatform: hostPlatform, + }; }, staleTime: 1_000, onError: async (error) => { From 8a8e0956fcfa534ece492c974521d973f2dd2d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Wed, 18 Feb 2026 15:34:41 +0100 Subject: [PATCH 16/32] Update dropdown menu border --- src/components/shadcn/dropdown-menu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/shadcn/dropdown-menu.tsx b/src/components/shadcn/dropdown-menu.tsx index 00cd139..217204d 100644 --- a/src/components/shadcn/dropdown-menu.tsx +++ b/src/components/shadcn/dropdown-menu.tsx @@ -95,7 +95,7 @@ function DropdownMenuCheckboxItem({ Date: Wed, 18 Feb 2026 16:43:39 +0100 Subject: [PATCH 17/32] Use git for pixi_api --- src-tauri/Cargo.lock | 35 +++++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7b0373b..43e4c15 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -955,6 +955,7 @@ dependencies = [ [[package]] name = "barrier_cell" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "parking_lot", "thiserror 2.0.18", @@ -2445,6 +2446,7 @@ dependencies = [ [[package]] name = "fancy_display" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "console", ] @@ -5785,6 +5787,7 @@ dependencies = [ [[package]] name = "pixi_api" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "console", "dunce", @@ -5824,6 +5827,7 @@ dependencies = [ [[package]] name = "pixi_auth" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "pixi_config", "rattler_networking", @@ -5833,6 +5837,7 @@ dependencies = [ [[package]] name = "pixi_build_discovery" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "dunce", "itertools 0.14.0", @@ -5855,6 +5860,7 @@ dependencies = [ [[package]] name = "pixi_build_frontend" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "fs-err", "futures", @@ -5875,6 +5881,7 @@ dependencies = [ [[package]] name = "pixi_build_type_conversions" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "itertools 0.14.0", "ordermap", @@ -5887,6 +5894,7 @@ dependencies = [ [[package]] name = "pixi_build_types" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "ordermap", "pixi_stable_hash", @@ -5901,6 +5909,7 @@ dependencies = [ [[package]] name = "pixi_command_dispatcher" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "async-fd-lock", "base64 0.22.1", @@ -5959,6 +5968,7 @@ dependencies = [ [[package]] name = "pixi_config" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "clap", "console", @@ -5984,6 +5994,7 @@ dependencies = [ [[package]] name = "pixi_consts" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "console", "rattler_cache", @@ -5994,6 +6005,7 @@ dependencies = [ [[package]] name = "pixi_core" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "async-once-cell", "barrier_cell", @@ -6090,6 +6102,7 @@ dependencies = [ [[package]] name = "pixi_default_versions" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "rattler_conda_types", ] @@ -6097,6 +6110,7 @@ dependencies = [ [[package]] name = "pixi_diff" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "ahash 0.8.12", "console", @@ -6114,6 +6128,7 @@ dependencies = [ [[package]] name = "pixi_git" version = "0.0.1" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "dashmap", "dunce", @@ -6134,6 +6149,7 @@ dependencies = [ [[package]] name = "pixi_glob" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "dashmap", "fs-err", @@ -6150,6 +6166,7 @@ dependencies = [ [[package]] name = "pixi_install_pypi" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "ahash 0.8.12", "chrono", @@ -6210,6 +6227,7 @@ dependencies = [ [[package]] name = "pixi_manifest" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "chrono", "console", @@ -6250,6 +6268,7 @@ dependencies = [ [[package]] name = "pixi_path" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "fs-err", "serde", @@ -6260,6 +6279,7 @@ dependencies = [ [[package]] name = "pixi_progress" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "indicatif", "parking_lot", @@ -6268,6 +6288,7 @@ dependencies = [ [[package]] name = "pixi_pypi_spec" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "itertools 0.14.0", "pep440_rs", @@ -6287,6 +6308,7 @@ dependencies = [ [[package]] name = "pixi_python_status" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "rattler", ] @@ -6294,6 +6316,7 @@ dependencies = [ [[package]] name = "pixi_record" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "file_url", "itertools 0.14.0", @@ -6316,6 +6339,7 @@ dependencies = [ [[package]] name = "pixi_reporters" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "console", "futures", @@ -6347,6 +6371,7 @@ dependencies = [ [[package]] name = "pixi_spec" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "dirs", "file_url", @@ -6372,6 +6397,7 @@ dependencies = [ [[package]] name = "pixi_spec_containers" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "indexmap 2.13.0", "itertools 0.14.0", @@ -6383,6 +6409,7 @@ dependencies = [ [[package]] name = "pixi_stable_hash" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "ordermap", "rattler_conda_types", @@ -6394,6 +6421,7 @@ dependencies = [ [[package]] name = "pixi_toml" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "digest", "hex", @@ -6408,6 +6436,7 @@ dependencies = [ [[package]] name = "pixi_url" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "bzip2 0.6.1", "dashmap", @@ -6437,6 +6466,7 @@ dependencies = [ [[package]] name = "pixi_utils" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "async-fd-lock", "fs-err", @@ -6472,6 +6502,7 @@ dependencies = [ [[package]] name = "pixi_uv_context" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "fs-err", "miette 7.6.0", @@ -6496,6 +6527,7 @@ dependencies = [ [[package]] name = "pixi_uv_conversions" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "chrono", "dunce", @@ -6530,6 +6562,7 @@ dependencies = [ [[package]] name = "pixi_variant" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "pixi_build_types", "rattler_lock", @@ -6878,6 +6911,7 @@ dependencies = [ [[package]] name = "pypi_mapping" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "async-once-cell", "dashmap", @@ -6907,6 +6941,7 @@ dependencies = [ [[package]] name = "pypi_modifiers" version = "0.1.0" +source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" dependencies = [ "miette 7.6.0", "pixi_default_versions", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dd377b5..30bb078 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,7 +27,7 @@ miette = "7" notify = "8" notify-debouncer-full = "0.7" percent-encoding = "2" -pixi_api = { package = "pixi_api", path = "../../pixi/crates/pixi_api" } +pixi_api = { package = "pixi_api", git = "https://github.com/haecker-felix/pixi", branch = "pixi_api_list" } portable-pty = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" From 95803daac4ca33bd4bb89377a7eebd6e8bc654b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 23 Feb 2026 10:44:52 +0100 Subject: [PATCH 18/32] Render packages in a table --- src/components/pixi/inspect/inspect.tsx | 356 +++++++++++++++++++----- 1 file changed, 283 insertions(+), 73 deletions(-) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index e19b187..c0e9ce2 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -1,19 +1,29 @@ import { getRouteApi } from "@tanstack/react-router"; import { + ArrowDownIcon, + ArrowUpIcon, ChevronRightIcon, + Columns3CogIcon, ListIcon, ListTreeIcon, PackageCheckIcon, + PackageIcon, SearchIcon, } from "lucide-react"; import prettyBytes from "pretty-bytes"; import { useEffect, useState } from "react"; -import { CircularIcon } from "@/components/common/circularIcon"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; -import { Row } from "@/components/common/row"; import { PackageDialog } from "@/components/pixi/inspect/packageDialog"; import { Button } from "@/components/shadcn/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/shadcn/dropdown-menu"; import { Input } from "@/components/shadcn/input"; import { Select, @@ -25,6 +35,46 @@ import { import { type Package, listPackages } from "@/lib/pixi/workspace/list"; +type FieldKey = + | "requested-spec" + | "version" + | "build" + | "license" + | "size" + | "kind" + | "timestamp" + | "platform" + | "arch" + | "subdir" + | "source" + | "file-name" + | "url" + | "is-editable" + | "constrains" + | "depends"; + +type SortField = "name" | FieldKey; +type SortDirection = "asc" | "desc"; + +const FIELD_OPTIONS: { key: FieldKey; label: string }[] = [ + { key: "requested-spec", label: "Requested Spec" }, + { key: "version", label: "Version" }, + { key: "build", label: "Build" }, + { key: "license", label: "License" }, + { key: "size", label: "Size" }, + { key: "kind", label: "Package Kind" }, + { key: "timestamp", label: "Timestamp" }, + { key: "platform", label: "Platform" }, + { key: "arch", label: "Architecture" }, + { key: "subdir", label: "Subdirectory" }, + { key: "source", label: "Source" }, + { key: "file-name", label: "File Name" }, + { key: "url", label: "URL" }, + { key: "is-editable", label: "Editable" }, + { key: "constrains", label: "Constrains" }, + { key: "depends", label: "Dependencies" }, +]; + export function Inspect() { const { workspace, environments, platforms, currentPlatform } = getRouteApi("/workspace/$path").useLoaderData(); @@ -41,6 +91,11 @@ export function Inspect() { const [packages, setPackages] = useState([]); const [selectedPackage, setSelectedPackage] = useState(null); const [expanded, setExpanded] = useState>(new Set()); + const [visibleFields, setVisibleFields] = useState>( + new Set(["version", "build", "license", "size"]), + ); + const [sortField, setSortField] = useState("name"); + const [sortDirection, setSortDirection] = useState("asc"); // Sync local state when URL search changes externally useEffect(() => { @@ -114,6 +169,87 @@ export function Inspect() { ]; } + function toggleField(col: FieldKey) { + setVisibleFields((prev) => { + const next = new Set(prev); + if (next.has(col)) next.delete(col); + else next.add(col); + return next; + }); + } + + function toggleSort(field: SortField) { + if (sortField === field) { + setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); + } else { + setSortField(field); + setSortDirection("asc"); + } + } + + function getFieldValue(pkg: Package, field: FieldKey): string { + switch (field) { + case "requested-spec": + return pkg.requested_spec ?? ""; + case "version": + return pkg.version; + case "build": + return pkg.build ?? ""; + case "license": + return pkg.license ?? ""; + case "size": + return pkg.size_bytes != null ? prettyBytes(pkg.size_bytes) : ""; + case "kind": + return pkg.kind === "conda" ? "Conda" : "PyPI"; + case "timestamp": + return pkg.timestamp != null + ? new Date(pkg.timestamp).toLocaleDateString() + : ""; + case "platform": + return pkg.platform ?? ""; + case "arch": + return pkg.arch ?? ""; + case "subdir": + return pkg.subdir ?? ""; + case "source": + return pkg.source ?? ""; + case "file-name": + return pkg.file_name ?? ""; + case "url": + return pkg.url ?? ""; + case "is-editable": + return pkg.is_editable ? "Yes" : ""; + case "constrains": + return pkg.constrains.join(", "); + case "depends": + return pkg.depends.join(", "); + } + } + + function comparePackages(a: Package, b: Package): number { + let result: number; + switch (sortField) { + case "name": + result = a.name.localeCompare(b.name); + break; + case "size": + result = (a.size_bytes ?? 0) - (b.size_bytes ?? 0); + break; + case "timestamp": + result = (a.timestamp ?? 0) - (b.timestamp ?? 0); + break; + default: + result = getFieldValue(a, sortField).localeCompare( + getFieldValue(b, sortField), + ); + break; + } + return ( + (sortDirection === "asc" ? result : -result) || + a.name.localeCompare(b.name) + ); + } + function toggleExpand(name: string) { setExpanded((prev) => { const next = new Set(prev); @@ -123,67 +259,64 @@ export function Inspect() { }); } - function renderPackageRow(pkg: Package, depth: number = 0, nodeKey?: string) { + function renderTableRow(pkg: Package, depth: number = 0, nodeKey?: string) { const depNames = treeMode ? getDependencyNames(pkg) : []; const hasChildren = depNames.length > 0; const expandKey = nodeKey ?? pkg.name; const isOpen = expanded.has(expandKey); return ( -
0 ? { marginLeft: depth * 36 } : undefined} + setSelectedPackage(pkg)} > - {treeMode && - (hasChildren ? ( - - ) : ( -
- ))} - - - + +
0 ? { paddingLeft: depth * 20 } : undefined} + > + {treeMode && + (hasChildren ? ( + + ) : ( +
+ ))} + {pkg.is_explicit ? ( + ) : ( - - ) - } - suffix={ - pkg.size_bytes != null ? ( - - {prettyBytes(pkg.size_bytes)} - - ) : undefined - } - onClick={() => setSelectedPackage(pkg)} - /> -
+ + )} + {pkg.name} +
+ + {activeFields.map((f) => ( + + {getFieldValue(pkg, f.key)} + + ))} + ); } - // Recursive tree renderer - function renderTree( + function renderTreeRows( pkg: Package, depth: number, visited: Set, parentKey: string = "", - ): React.ReactNode { - if (visited.has(pkg.name)) return null; + ): React.ReactNode[] { + if (visited.has(pkg.name)) return []; const nextVisited = new Set(visited); nextVisited.add(pkg.name); @@ -191,15 +324,67 @@ export function Inspect() { const nodeKey = parentKey ? `${parentKey}>${pkg.name}` : pkg.name; const isOpen = expanded.has(nodeKey); + const rows: React.ReactNode[] = [renderTableRow(pkg, depth, nodeKey)]; + + if (isOpen) { + for (const depName of depNames.sort()) { + const depPkg = packageMap.get(depName); + if (depPkg) { + rows.push(...renderTreeRows(depPkg, depth + 1, nextVisited, nodeKey)); + } + } + } + + return rows; + } + + function renderTable() { return ( -
- {renderPackageRow(pkg, depth, nodeKey)} - {isOpen && - depNames.sort().map((depName) => { - const depPkg = packageMap.get(depName); - if (!depPkg) return null; - return renderTree(depPkg, depth + 1, nextVisited, nodeKey); - })} +
+ + + + + {activeFields.map((f) => ( + + ))} + + + + {treeMode + ? sortedTreeRoots.flatMap((pkg) => + renderTreeRows(pkg, 0, new Set()), + ) + : sortedPackages.map((pkg) => renderTableRow(pkg))} + +
toggleSort("name")} + > + + Name + {sortField === "name" && + (sortDirection === "asc" ? ( + + ) : ( + + ))} + + toggleSort(f.key)} + > + + {f.label} + {sortField === f.key && + (sortDirection === "asc" ? ( + + ) : ( + + ))} + +
); } @@ -208,14 +393,15 @@ export function Inspect() { const roots = filteredPackages.filter((pkg) => pkg.is_explicit); const treeRoots = roots.length > 0 ? roots : filteredPackages; - // Sort packages alphabetically - const sortedPackages = [...filteredPackages].sort((a, b) => - a.name.localeCompare(b.name), - ); - const sortedTreeRoots = [...treeRoots].sort((a, b) => - a.name.localeCompare(b.name), + // Visible field columns in display order + const activeFields = FIELD_OPTIONS.filter((opt) => + visibleFields.has(opt.key), ); + // Sort packages + const sortedPackages = [...filteredPackages].sort(comparePackages); + const sortedTreeRoots = [...treeRoots].sort(comparePackages); + return ( <> {/* Toolbar */} @@ -273,19 +459,43 @@ export function Inspect() { setTreeMode((prev) => !prev)} - > - {treeMode ? : } - {treeMode ? "All Packages" : "Dependency Tree"} - +
+ {/* Field Selection */} + + + + + + Visible Fields + + {FIELD_OPTIONS.map((opt) => ( + toggleField(opt.key)} + onSelect={(e) => e.preventDefault()} + > + {opt.label} + + ))} + + + + {/* List/Tree Toggle */} + +
} > - {treeMode - ? sortedTreeRoots.map((pkg) => renderTree(pkg, 0, new Set())) - : sortedPackages.map((pkg) => renderPackageRow(pkg))} + {renderTable()}
{/* Package detail dialog */} From ed5ac79556393052ad2c65139279b0974aaf0e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 23 Feb 2026 14:37:58 +0100 Subject: [PATCH 19/32] Add fullscreen mode and sticky package colunm/header --- src/components/common/preferencesGroup.tsx | 2 +- src/components/pixi/inspect/inspect.tsx | 379 ++++++++++++--------- src/components/shadcn/input.tsx | 20 +- src/components/shadcn/select.tsx | 2 +- 4 files changed, 236 insertions(+), 167 deletions(-) diff --git a/src/components/common/preferencesGroup.tsx b/src/components/common/preferencesGroup.tsx index f1483ac..1b76835 100644 --- a/src/components/common/preferencesGroup.tsx +++ b/src/components/common/preferencesGroup.tsx @@ -70,7 +70,7 @@ export function PreferencesGroup({ {(title || headerPrefix || headerSuffix) && (
(null); const [expanded, setExpanded] = useState>(new Set()); const [visibleFields, setVisibleFields] = useState>( - new Set(["version", "build", "license", "size"]), + new Set(["version", "requested-spec", "build", "size"]), ); const [sortField, setSortField] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); + const [maximized, setMaximized] = useState(false); // Sync local state when URL search changes externally useEffect(() => { @@ -268,10 +267,10 @@ export function Inspect() { return ( setSelectedPackage(pkg)} > - +
0 ? { paddingLeft: depth * 20 } : undefined} @@ -301,11 +300,32 @@ export function Inspect() { {pkg.name}
- {activeFields.map((f) => ( - - {getFieldValue(pkg, f.key)} - - ))} + {activeFields.map((f) => { + const value = getFieldValue(pkg, f.key); + const isLink = + value.startsWith("http://") || value.startsWith("https://"); + return ( + + {isLink ? ( + + ) : ( + value + )} + + ); + })} ); } @@ -338,57 +358,6 @@ export function Inspect() { return rows; } - function renderTable() { - return ( -
- - - - - {activeFields.map((f) => ( - - ))} - - - - {treeMode - ? sortedTreeRoots.flatMap((pkg) => - renderTreeRows(pkg, 0, new Set()), - ) - : sortedPackages.map((pkg) => renderTableRow(pkg))} - -
toggleSort("name")} - > - - Name - {sortField === "name" && - (sortDirection === "asc" ? ( - - ) : ( - - ))} - - toggleSort(f.key)} - > - - {f.label} - {sortField === f.key && - (sortDirection === "asc" ? ( - - ) : ( - - ))} - -
-
- ); - } - // Determine roots for tree mode const roots = filteredPackages.filter((pkg) => pkg.is_explicit); const treeRoots = roots.length > 0 ? roots : filteredPackages; @@ -404,99 +373,191 @@ export function Inspect() { return ( <> - {/* Toolbar */} -
- setLocalSearch(event.target.value)} - placeholder="Search packages…" - autoComplete="off" - spellCheck={false} - autoCorrect="off" - icon={} - /> -
- {/* Environment */} - - - {/* Platform */} - -
-
- {/* Content */} - - {/* Field Selection */} - - - - - - Visible Fields - - {FIELD_OPTIONS.map((opt) => ( +
+ div]:flex [&>div]:flex-1 [&>div]:flex-col [&>div]:min-h-0 [&>div]:mt-0! [&>div]:mb-0! [&>div>div:last-child]:flex [&>div>div:last-child]:flex-1 [&>div>div:last-child]:flex-col [&>div>div:last-child]:min-h-0" + : "-mt-2" + } + headerPrefix={ +
+ {/* Search */} + setLocalSearch(event.target.value)} + placeholder="Search…" + autoComplete="off" + spellCheck={false} + autoCorrect="off" + icon={} + size="sm" + className="w-48" + /> + {/* Environment */} + + + + + + Environment + + + {[...environments] + .sort((a, b) => { + if (a.name === "default") return -1; + if (b.name === "default") return 1; + return a.name.localeCompare(b.name); + }) + .map((env) => ( + + {env.name} + + ))} + + + + {/* Platform */} + + + + + + Platform + + + {[...availablePlatforms].sort().map((p) => ( + + {p} + + ))} + + + +
+ } + headerSuffix={ +
+ + {filteredPackages.length}{" "} + {filteredPackages.length === 1 ? "package" : "packages"} + {(() => { + const totalBytes = filteredPackages.reduce( + (sum, pkg) => sum + (pkg.size_bytes ?? 0), + 0, + ); + return totalBytes > 0 ? ` (${prettyBytes(totalBytes)})` : ""; + })()} + + {/* Field Selection */} + + + + + + Visible Fields + toggleField(opt.key)} - onSelect={(e) => e.preventDefault()} + checked={treeMode} + onCheckedChange={() => setTreeMode((prev) => !prev)} > - {opt.label} + Dependency Tree - ))} - - - - {/* List/Tree Toggle */} - + + {FIELD_OPTIONS.map((opt) => ( + toggleField(opt.key)} + onSelect={(e) => e.preventDefault()} + > + {opt.label} + + ))} + + + {/* Maximize/Minimize */} + +
+ } + > +
+ {/* Actual List */} + + + + + {activeFields.map((f) => ( + + ))} + + + + {treeMode + ? sortedTreeRoots.flatMap((pkg) => + renderTreeRows(pkg, 0, new Set()), + ) + : sortedPackages.map((pkg) => renderTableRow(pkg))} + +
toggleSort("name")} + > + + Package + {sortField === "name" && + (sortDirection === "asc" ? ( + + ) : ( + + ))} + + toggleSort(f.key)} + > + + {f.label} + {sortField === f.key && + (sortDirection === "asc" ? ( + + ) : ( + + ))} + +
- } - > - {renderTable()} -
+ +
{/* Package detail dialog */} {selectedPackage && ( diff --git a/src/components/shadcn/input.tsx b/src/components/shadcn/input.tsx index 98150ed..36d2e17 100644 --- a/src/components/shadcn/input.tsx +++ b/src/components/shadcn/input.tsx @@ -17,11 +17,13 @@ function Input({ required, icon, suffix, + size = "default", ...props -}: React.ComponentProps<"input"> & { +}: Omit, "size"> & { label?: string; icon?: ReactNode; suffix?: ReactNode; + size?: "default" | "sm"; }) { if (label) { return ( @@ -65,7 +67,9 @@ function Input({ } return (
- {icon &&
{icon}
} + {icon && ( +
{icon}
+ )} - {suffix &&
{suffix}
} + {suffix && ( +
+ {suffix} +
+ )}
); } diff --git a/src/components/shadcn/select.tsx b/src/components/shadcn/select.tsx index 073037a..2354a9f 100644 --- a/src/components/shadcn/select.tsx +++ b/src/components/shadcn/select.tsx @@ -125,7 +125,7 @@ function SelectItem({ Date: Mon, 23 Feb 2026 15:01:42 +0100 Subject: [PATCH 20/32] Refactor field -> column --- src/components/pixi/inspect/inspect.tsx | 54 ++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index cee7751..228ef01 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -33,7 +33,7 @@ import { Input } from "@/components/shadcn/input"; import { type Package, listPackages } from "@/lib/pixi/workspace/list"; -type FieldKey = +type ColumnKey = | "version" | "requested-spec" | "build" @@ -51,10 +51,10 @@ type FieldKey = | "constrains" | "depends"; -type SortField = "name" | FieldKey; +type SortColumn = "name" | ColumnKey; type SortDirection = "asc" | "desc"; -const FIELD_OPTIONS: { key: FieldKey; label: string }[] = [ +const COLUMN_OPTIONS: { key: ColumnKey; label: string }[] = [ { key: "version", label: "Version" }, { key: "requested-spec", label: "Requested Spec" }, { key: "build", label: "Build" }, @@ -89,10 +89,10 @@ export function Inspect() { const [packages, setPackages] = useState([]); const [selectedPackage, setSelectedPackage] = useState(null); const [expanded, setExpanded] = useState>(new Set()); - const [visibleFields, setVisibleFields] = useState>( + const [visibleColumns, setVisibleColumns] = useState>( new Set(["version", "requested-spec", "build", "size"]), ); - const [sortField, setSortField] = useState("name"); + const [sortColumn, setSortColumn] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [maximized, setMaximized] = useState(false); @@ -168,8 +168,8 @@ export function Inspect() { ]; } - function toggleField(col: FieldKey) { - setVisibleFields((prev) => { + function toggleColumn(col: ColumnKey) { + setVisibleColumns((prev) => { const next = new Set(prev); if (next.has(col)) next.delete(col); else next.add(col); @@ -177,16 +177,16 @@ export function Inspect() { }); } - function toggleSort(field: SortField) { - if (sortField === field) { + function toggleSort(column: SortColumn) { + if (sortColumn === column) { setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); } else { - setSortField(field); + setSortColumn(column); setSortDirection("asc"); } } - function getFieldValue(pkg: Package, field: FieldKey): string { + function getColumnValue(pkg: Package, field: ColumnKey): string { switch (field) { case "requested-spec": return pkg.requested_spec ?? ""; @@ -227,7 +227,7 @@ export function Inspect() { function comparePackages(a: Package, b: Package): number { let result: number; - switch (sortField) { + switch (sortColumn) { case "name": result = a.name.localeCompare(b.name); break; @@ -238,8 +238,8 @@ export function Inspect() { result = (a.timestamp ?? 0) - (b.timestamp ?? 0); break; default: - result = getFieldValue(a, sortField).localeCompare( - getFieldValue(b, sortField), + result = getColumnValue(a, sortColumn).localeCompare( + getColumnValue(b, sortColumn), ); break; } @@ -300,8 +300,8 @@ export function Inspect() { {pkg.name}
- {activeFields.map((f) => { - const value = getFieldValue(pkg, f.key); + {activeColumns.map((f) => { + const value = getColumnValue(pkg, f.key); const isLink = value.startsWith("http://") || value.startsWith("https://"); return ( @@ -362,9 +362,9 @@ export function Inspect() { const roots = filteredPackages.filter((pkg) => pkg.is_explicit); const treeRoots = roots.length > 0 ? roots : filteredPackages; - // Visible field columns in display order - const activeFields = FIELD_OPTIONS.filter((opt) => - visibleFields.has(opt.key), + // Visible columns in display order + const activeColumns = COLUMN_OPTIONS.filter((opt) => + visibleColumns.has(opt.key), ); // Sort packages @@ -468,7 +468,7 @@ export function Inspect() { return totalBytes > 0 ? ` (${prettyBytes(totalBytes)})` : ""; })()} - {/* Field Selection */} + {/* Column Selection */} - Visible Fields + View Settings - {FIELD_OPTIONS.map((opt) => ( + {COLUMN_OPTIONS.map((opt) => ( toggleField(opt.key)} + checked={visibleColumns.has(opt.key)} + onCheckedChange={() => toggleColumn(opt.key)} onSelect={(e) => e.preventDefault()} > {opt.label} @@ -520,7 +520,7 @@ export function Inspect() { > Package - {sortField === "name" && + {sortColumn === "name" && (sortDirection === "asc" ? ( ) : ( @@ -528,7 +528,7 @@ export function Inspect() { ))} - {activeFields.map((f) => ( + {activeColumns.map((f) => ( {f.label} - {sortField === f.key && + {sortColumn === f.key && (sortDirection === "asc" ? ( ) : ( From b5eab29dc2dfec7e37ecf4249a7d27cc045bff38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 23 Feb 2026 15:20:26 +0100 Subject: [PATCH 21/32] Apply search to all columns --- src/components/pixi/inspect/inspect.tsx | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index 228ef01..c6d62d7 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -149,7 +149,12 @@ export function Inspect() { // Client-side search filtering const needle = localSearch.trim().toLowerCase(); const filteredPackages = needle - ? packages.filter((pkg) => pkg.name.toLowerCase().includes(needle)) + ? packages.filter((pkg) => { + if (pkg.name.toLowerCase().includes(needle)) return true; + return COLUMN_OPTIONS.some((col) => + getColumnValue(pkg, col.key).toLowerCase().includes(needle), + ); + }) : packages; // Dependency tree @@ -258,6 +263,25 @@ export function Inspect() { }); } + function highlightMatch(text: string): React.ReactNode { + if (!needle || !text.toLowerCase().includes(needle)) return text; + const parts = text.split( + new RegExp(`(${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"), + ); + return parts.map((part, i) => + part.toLowerCase() === needle ? ( + + {part} + + ) : ( + part + ), + ); + } + function renderTableRow(pkg: Package, depth: number = 0, nodeKey?: string) { const depNames = treeMode ? getDependencyNames(pkg) : []; const hasChildren = depNames.length > 0; @@ -297,7 +321,7 @@ export function Inspect() { ) : ( )} - {pkg.name} + {highlightMatch(pkg.name)}
{activeColumns.map((f) => { @@ -318,10 +342,10 @@ export function Inspect() { openUrl(value); }} > - {value} + {highlightMatch(value)} ) : ( - value + highlightMatch(value) )} ); From c82c41bd870d31984b66f967bc91e6a19ca391a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Mon, 23 Feb 2026 16:38:09 +0100 Subject: [PATCH 22/32] Add support for inverse tree --- src/components/pixi/inspect/inspect.tsx | 64 +++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index c6d62d7..fa43168 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -85,7 +85,9 @@ export function Inspect() { const [selectedPlatform, setSelectedPlatform] = useState(currentPlatform); - const [treeMode, setTreeMode] = useState(false); + const [viewMode, setViewMode] = useState<"list" | "tree" | "inverted-tree">( + "list", + ); const [packages, setPackages] = useState([]); const [selectedPackage, setSelectedPackage] = useState(null); const [expanded, setExpanded] = useState>(new Set()); @@ -144,7 +146,7 @@ export function Inspect() { // Reset expanded nodes when switching modes or refetching useEffect(() => { setExpanded(new Set()); - }, [treeMode, packages]); + }, [viewMode, packages]); // Client-side search filtering const needle = localSearch.trim().toLowerCase(); @@ -173,6 +175,24 @@ export function Inspect() { ]; } + // Reverse dependency map: package name -> names of packages that depend on it + const reverseDeps = new Map(); + for (const pkg of packages) { + for (const depName of getDependencyNames(pkg)) { + if (!reverseDeps.has(depName)) reverseDeps.set(depName, []); + reverseDeps.get(depName)!.push(pkg.name); + } + } + + const treeMode = viewMode !== "list"; + const invertTree = viewMode === "inverted-tree"; + + function getTreeChildNames(pkg: Package): string[] { + return invertTree + ? (reverseDeps.get(pkg.name) ?? []) + : getDependencyNames(pkg); + } + function toggleColumn(col: ColumnKey) { setVisibleColumns((prev) => { const next = new Set(prev); @@ -283,8 +303,8 @@ export function Inspect() { } function renderTableRow(pkg: Package, depth: number = 0, nodeKey?: string) { - const depNames = treeMode ? getDependencyNames(pkg) : []; - const hasChildren = depNames.length > 0; + const childNames = treeMode ? getTreeChildNames(pkg) : []; + const hasChildren = childNames.length > 0; const expandKey = nodeKey ?? pkg.name; const isOpen = expanded.has(expandKey); @@ -364,15 +384,15 @@ export function Inspect() { const nextVisited = new Set(visited); nextVisited.add(pkg.name); - const depNames = getDependencyNames(pkg); + const childNames = getTreeChildNames(pkg); const nodeKey = parentKey ? `${parentKey}>${pkg.name}` : pkg.name; const isOpen = expanded.has(nodeKey); const rows: React.ReactNode[] = [renderTableRow(pkg, depth, nodeKey)]; if (isOpen) { - for (const depName of depNames.sort()) { - const depPkg = packageMap.get(depName); + for (const childName of childNames.sort()) { + const depPkg = packageMap.get(childName); if (depPkg) { rows.push(...renderTreeRows(depPkg, depth + 1, nextVisited, nodeKey)); } @@ -383,8 +403,14 @@ export function Inspect() { } // Determine roots for tree mode - const roots = filteredPackages.filter((pkg) => pkg.is_explicit); - const treeRoots = roots.length > 0 ? roots : filteredPackages; + // Normal: explicit packages are roots, expand to see their dependencies + // Inverted: all packages are roots, expand to see what depends on them + const treeRoots = invertTree + ? filteredPackages + : (() => { + const roots = filteredPackages.filter((pkg) => pkg.is_explicit); + return roots.length > 0 ? roots : filteredPackages; + })(); // Visible columns in display order const activeColumns = COLUMN_OPTIONS.filter((opt) => @@ -502,12 +528,22 @@ export function Inspect() { View Settings - setTreeMode((prev) => !prev)} + + setViewMode(v as "list" | "tree" | "inverted-tree") + } > - Dependency Tree - + + List + + + Tree + + + Inverted Tree + + {COLUMN_OPTIONS.map((opt) => ( Date: Tue, 24 Feb 2026 09:28:10 +0100 Subject: [PATCH 23/32] Add support for virtual packages --- src/components/pixi/inspect/inspect.tsx | 86 ++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index fa43168..c10ddf3 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -8,6 +8,7 @@ import { Columns3CogIcon, CpuIcon, MaximizeIcon, + MicrochipIcon, MinimizeIcon, PackageCheckIcon, PackageIcon, @@ -97,6 +98,7 @@ export function Inspect() { const [sortColumn, setSortColumn] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [maximized, setMaximized] = useState(false); + const [showVirtualPackages, setShowVirtualPackages] = useState(true); // Sync local state when URL search changes externally useEffect(() => { @@ -148,20 +150,66 @@ export function Inspect() { setExpanded(new Set()); }, [viewMode, packages]); + // Extract virtual packages from dependency specs (names starting with "__") + const realNames = new Set(packages.map((p) => p.name)); + const virtualVersions = new Map(); + for (const pkg of packages) { + for (const dep of pkg.depends) { + const name = dep.split(/[\s[]/)[0]; + if (name.startsWith("__") && !realNames.has(name)) { + const version = dep.slice(name.length).trim(); + if (!virtualVersions.has(name) || version) { + virtualVersions.set(name, version); + } + } + } + } + const virtualPackages: Package[] = Array.from(virtualVersions.entries()).map( + ([name, version]) => ({ + name, + version: version || "", + build: null, + build_number: null, + size_bytes: null, + kind: "conda", + source: null, + license: null, + license_family: null, + is_explicit: false, + is_editable: false, + md5: null, + sha256: null, + arch: null, + platform: null, + subdir: null, + timestamp: null, + noarch: null, + file_name: null, + url: null, + requested_spec: null, + constrains: [], + depends: [], + track_features: [], + }), + ); + const allPackages = showVirtualPackages + ? [...packages, ...virtualPackages] + : packages; + // Client-side search filtering const needle = localSearch.trim().toLowerCase(); const filteredPackages = needle - ? packages.filter((pkg) => { + ? allPackages.filter((pkg) => { if (pkg.name.toLowerCase().includes(needle)) return true; return COLUMN_OPTIONS.some((col) => getColumnValue(pkg, col.key).toLowerCase().includes(needle), ); }) - : packages; + : allPackages; // Dependency tree const packageMap = new Map(); - for (const pkg of packages) { + for (const pkg of allPackages) { packageMap.set(pkg.name, pkg); } @@ -177,7 +225,7 @@ export function Inspect() { // Reverse dependency map: package name -> names of packages that depend on it const reverseDeps = new Map(); - for (const pkg of packages) { + for (const pkg of allPackages) { for (const depName of getDependencyNames(pkg)) { if (!reverseDeps.has(depName)) reverseDeps.set(depName, []); reverseDeps.get(depName)!.push(pkg.name); @@ -224,6 +272,7 @@ export function Inspect() { case "size": return pkg.size_bytes != null ? prettyBytes(pkg.size_bytes) : ""; case "kind": + if (pkg.name.startsWith("__")) return "Virtual"; return pkg.kind === "conda" ? "Conda" : "PyPI"; case "timestamp": return pkg.timestamp != null @@ -336,7 +385,9 @@ export function Inspect() { ) : (
))} - {pkg.is_explicit ? ( + {pkg.name.startsWith("__") ? ( + + ) : pkg.is_explicit ? ( ) : ( @@ -534,17 +585,36 @@ export function Inspect() { setViewMode(v as "list" | "tree" | "inverted-tree") } > - + e.preventDefault()} + > List - + e.preventDefault()} + > Tree - + e.preventDefault()} + > Inverted Tree + + setShowVirtualPackages((prev) => !prev) + } + onSelect={(e) => e.preventDefault()} + > + Show Virtual Packages + + {COLUMN_OPTIONS.map((opt) => ( Date: Tue, 24 Feb 2026 13:21:39 +0100 Subject: [PATCH 24/32] Refactor inspect tab --- src/components/pixi/inspect/columns.ts | 171 +++++++++++ src/components/pixi/inspect/inspect.tsx | 281 ++++-------------- src/components/pixi/inspect/packageDialog.tsx | 154 +++------- src/components/pixi/inspect/packageRow.tsx | 106 +++++++ src/lib/utils.ts | 4 + 5 files changed, 383 insertions(+), 333 deletions(-) create mode 100644 src/components/pixi/inspect/columns.ts create mode 100644 src/components/pixi/inspect/packageRow.tsx diff --git a/src/components/pixi/inspect/columns.ts b/src/components/pixi/inspect/columns.ts new file mode 100644 index 0000000..d3df0ba --- /dev/null +++ b/src/components/pixi/inspect/columns.ts @@ -0,0 +1,171 @@ +import prettyBytes from "pretty-bytes"; + +import type { Package } from "@/lib/pixi/workspace/list"; + +export type ColumnKey = + | "version" + | "requested-spec" + | "build" + | "build-number" + | "timestamp" + | "license" + | "license-family" + | "size" + | "kind" + | "platform" + | "arch" + | "subdir" + | "noarch" + | "source" + | "file-name" + | "url" + | "md5" + | "sha256" + | "constrains" + | "depends"; + +export type SortColumn = "name" | ColumnKey; +export type SortDirection = "asc" | "desc"; + +export interface ColumnDefinition { + key: ColumnKey; + label: string; +} + +export const COLUMNS: ColumnDefinition[] = [ + { key: "kind", label: "Kind" }, + { key: "version", label: "Version" }, + { key: "requested-spec", label: "Requested Spec" }, + { key: "build", label: "Build" }, + { key: "build-number", label: "Build Number" }, + { key: "timestamp", label: "Timestamp" }, + { key: "license", label: "License" }, + { key: "license-family", label: "License Family" }, + { key: "source", label: "Source" }, + { key: "file-name", label: "File Name" }, + { key: "url", label: "URL" }, + { key: "subdir", label: "Subdirectory" }, + { key: "platform", label: "Platform" }, + { key: "arch", label: "Architecture" }, + { key: "noarch", label: "Noarch" }, + { key: "size", label: "Size" }, + { key: "sha256", label: "SHA256" }, + { key: "md5", label: "MD5" }, + { key: "depends", label: "Dependencies" }, + { key: "constrains", label: "Constrains" }, +]; + +export const DEFAULT_VISIBLE_COLUMNS = new Set([ + "version", + "requested-spec", + "build", + "size", +]); + +export function getColumnValue(pkg: Package, key: ColumnKey): string { + switch (key) { + case "version": + return pkg.version; + case "requested-spec": + return pkg.requested_spec ?? ""; + case "build": + return pkg.build ?? ""; + case "build-number": + return pkg.build_number != null ? String(pkg.build_number) : ""; + case "timestamp": + return pkg.timestamp != null + ? new Date(pkg.timestamp).toLocaleDateString() + : ""; + case "license": + return pkg.license ?? ""; + case "license-family": + return pkg.license_family ?? ""; + case "size": + return pkg.size_bytes != null ? prettyBytes(pkg.size_bytes) : ""; + case "kind": + if (pkg.name.startsWith("__")) return "Virtual"; + return pkg.kind === "conda" ? "Conda" : "PyPI"; + case "platform": + return pkg.platform ?? ""; + case "arch": + return pkg.arch ?? ""; + case "subdir": + return pkg.subdir ?? ""; + case "noarch": + return pkg.noarch ?? ""; + case "source": + return pkg.source ?? ""; + case "file-name": + return pkg.file_name ?? ""; + case "url": + return pkg.url ?? ""; + case "md5": + return pkg.md5 ?? ""; + case "sha256": + return pkg.sha256 ?? ""; + case "constrains": + return pkg.constrains.join(", "); + case "depends": + return pkg.depends.join(", "); + } +} + +export function comparePackages( + a: Package, + b: Package, + sortColumn: SortColumn, + sortDirection: SortDirection, +): number { + let result: number; + switch (sortColumn) { + case "name": + result = a.name.localeCompare(b.name); + break; + case "size": + result = (a.size_bytes ?? 0) - (b.size_bytes ?? 0); + break; + case "timestamp": + result = (a.timestamp ?? 0) - (b.timestamp ?? 0); + break; + case "build-number": + result = (a.build_number ?? 0) - (b.build_number ?? 0); + break; + default: + result = getColumnValue(a, sortColumn).localeCompare( + getColumnValue(b, sortColumn), + ); + break; + } + return ( + (sortDirection === "asc" ? result : -result) || a.name.localeCompare(b.name) + ); +} + +export function createVirtualPackage(name: string, version: string): Package { + return { + name, + version: version || "", + build: null, + build_number: null, + size_bytes: null, + kind: "conda", + source: null, + license: null, + license_family: null, + is_explicit: false, + is_editable: false, + md5: null, + sha256: null, + arch: null, + platform: null, + subdir: null, + timestamp: null, + noarch: null, + file_name: null, + url: null, + requested_spec: null, + constrains: [], + depends: [], + track_features: [], + }; +} diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index c10ddf3..d8d7725 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -1,24 +1,30 @@ import { getRouteApi } from "@tanstack/react-router"; -import { openUrl } from "@tauri-apps/plugin-opener"; import { ArrowDownIcon, ArrowUpIcon, BoxIcon, - ChevronRightIcon, Columns3CogIcon, CpuIcon, MaximizeIcon, - MicrochipIcon, MinimizeIcon, - PackageCheckIcon, - PackageIcon, SearchIcon, } from "lucide-react"; import prettyBytes from "pretty-bytes"; import { useEffect, useState } from "react"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; +import { + COLUMNS, + type ColumnKey, + DEFAULT_VISIBLE_COLUMNS, + type SortColumn, + type SortDirection, + comparePackages, + createVirtualPackage, + getColumnValue, +} from "@/components/pixi/inspect/columns"; import { PackageDialog } from "@/components/pixi/inspect/packageDialog"; +import { PackageRow } from "@/components/pixi/inspect/packageRow"; import { Button } from "@/components/shadcn/button"; import { DropdownMenu, @@ -34,46 +40,6 @@ import { Input } from "@/components/shadcn/input"; import { type Package, listPackages } from "@/lib/pixi/workspace/list"; -type ColumnKey = - | "version" - | "requested-spec" - | "build" - | "license" - | "size" - | "kind" - | "timestamp" - | "platform" - | "arch" - | "subdir" - | "source" - | "file-name" - | "url" - | "is-editable" - | "constrains" - | "depends"; - -type SortColumn = "name" | ColumnKey; -type SortDirection = "asc" | "desc"; - -const COLUMN_OPTIONS: { key: ColumnKey; label: string }[] = [ - { key: "version", label: "Version" }, - { key: "requested-spec", label: "Requested Spec" }, - { key: "build", label: "Build" }, - { key: "license", label: "License" }, - { key: "size", label: "Size" }, - { key: "kind", label: "Kind" }, - { key: "timestamp", label: "Timestamp" }, - { key: "platform", label: "Platform" }, - { key: "arch", label: "Architecture" }, - { key: "subdir", label: "Subdirectory" }, - { key: "source", label: "Source" }, - { key: "file-name", label: "File Name" }, - { key: "url", label: "URL" }, - { key: "is-editable", label: "Editable" }, - { key: "constrains", label: "Constrains" }, - { key: "depends", label: "Dependencies" }, -]; - export function Inspect() { const { workspace, environments, platforms, currentPlatform } = getRouteApi("/workspace/$path").useLoaderData(); @@ -89,16 +55,18 @@ export function Inspect() { const [viewMode, setViewMode] = useState<"list" | "tree" | "inverted-tree">( "list", ); + const [showVirtualPackages, setShowVirtualPackages] = useState(false); + const [visibleColumns, setVisibleColumns] = useState>( + new Set(DEFAULT_VISIBLE_COLUMNS), + ); + const [maximized, setMaximized] = useState(false); + const [packages, setPackages] = useState([]); const [selectedPackage, setSelectedPackage] = useState(null); const [expanded, setExpanded] = useState>(new Set()); - const [visibleColumns, setVisibleColumns] = useState>( - new Set(["version", "requested-spec", "build", "size"]), - ); + const [sortColumn, setSortColumn] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); - const [maximized, setMaximized] = useState(false); - const [showVirtualPackages, setShowVirtualPackages] = useState(true); // Sync local state when URL search changes externally useEffect(() => { @@ -150,7 +118,7 @@ export function Inspect() { setExpanded(new Set()); }, [viewMode, packages]); - // Extract virtual packages from dependency specs (names starting with "__") + // Extract virtual packages from dependency specs const realNames = new Set(packages.map((p) => p.name)); const virtualVersions = new Map(); for (const pkg of packages) { @@ -165,32 +133,7 @@ export function Inspect() { } } const virtualPackages: Package[] = Array.from(virtualVersions.entries()).map( - ([name, version]) => ({ - name, - version: version || "", - build: null, - build_number: null, - size_bytes: null, - kind: "conda", - source: null, - license: null, - license_family: null, - is_explicit: false, - is_editable: false, - md5: null, - sha256: null, - arch: null, - platform: null, - subdir: null, - timestamp: null, - noarch: null, - file_name: null, - url: null, - requested_spec: null, - constrains: [], - depends: [], - track_features: [], - }), + ([name, version]) => createVirtualPackage(name, version), ); const allPackages = showVirtualPackages ? [...packages, ...virtualPackages] @@ -201,7 +144,7 @@ export function Inspect() { const filteredPackages = needle ? allPackages.filter((pkg) => { if (pkg.name.toLowerCase().includes(needle)) return true; - return COLUMN_OPTIONS.some((col) => + return COLUMNS.some((col) => getColumnValue(pkg, col.key).toLowerCase().includes(needle), ); }) @@ -259,70 +202,6 @@ export function Inspect() { } } - function getColumnValue(pkg: Package, field: ColumnKey): string { - switch (field) { - case "requested-spec": - return pkg.requested_spec ?? ""; - case "version": - return pkg.version; - case "build": - return pkg.build ?? ""; - case "license": - return pkg.license ?? ""; - case "size": - return pkg.size_bytes != null ? prettyBytes(pkg.size_bytes) : ""; - case "kind": - if (pkg.name.startsWith("__")) return "Virtual"; - return pkg.kind === "conda" ? "Conda" : "PyPI"; - case "timestamp": - return pkg.timestamp != null - ? new Date(pkg.timestamp).toLocaleDateString() - : ""; - case "platform": - return pkg.platform ?? ""; - case "arch": - return pkg.arch ?? ""; - case "subdir": - return pkg.subdir ?? ""; - case "source": - return pkg.source ?? ""; - case "file-name": - return pkg.file_name ?? ""; - case "url": - return pkg.url ?? ""; - case "is-editable": - return pkg.is_editable ? "Yes" : ""; - case "constrains": - return pkg.constrains.join(", "); - case "depends": - return pkg.depends.join(", "); - } - } - - function comparePackages(a: Package, b: Package): number { - let result: number; - switch (sortColumn) { - case "name": - result = a.name.localeCompare(b.name); - break; - case "size": - result = (a.size_bytes ?? 0) - (b.size_bytes ?? 0); - break; - case "timestamp": - result = (a.timestamp ?? 0) - (b.timestamp ?? 0); - break; - default: - result = getColumnValue(a, sortColumn).localeCompare( - getColumnValue(b, sortColumn), - ); - break; - } - return ( - (sortDirection === "asc" ? result : -result) || - a.name.localeCompare(b.name) - ); - } - function toggleExpand(name: string) { setExpanded((prev) => { const next = new Set(prev); @@ -351,80 +230,6 @@ export function Inspect() { ); } - function renderTableRow(pkg: Package, depth: number = 0, nodeKey?: string) { - const childNames = treeMode ? getTreeChildNames(pkg) : []; - const hasChildren = childNames.length > 0; - const expandKey = nodeKey ?? pkg.name; - const isOpen = expanded.has(expandKey); - - return ( - setSelectedPackage(pkg)} - > - -
0 ? { paddingLeft: depth * 20 } : undefined} - > - {treeMode && - (hasChildren ? ( - - ) : ( -
- ))} - {pkg.name.startsWith("__") ? ( - - ) : pkg.is_explicit ? ( - - ) : ( - - )} - {highlightMatch(pkg.name)} -
- - {activeColumns.map((f) => { - const value = getColumnValue(pkg, f.key); - const isLink = - value.startsWith("http://") || value.startsWith("https://"); - return ( - - {isLink ? ( - - ) : ( - highlightMatch(value) - )} - - ); - })} - - ); - } - function renderTreeRows( pkg: Package, depth: number, @@ -439,7 +244,21 @@ export function Inspect() { const nodeKey = parentKey ? `${parentKey}>${pkg.name}` : pkg.name; const isOpen = expanded.has(nodeKey); - const rows: React.ReactNode[] = [renderTableRow(pkg, depth, nodeKey)]; + const rows: React.ReactNode[] = [ + 0} + isOpen={isOpen} + activeColumns={activeColumns} + highlightMatch={highlightMatch} + onSelect={setSelectedPackage} + onToggleExpand={toggleExpand} + />, + ]; if (isOpen) { for (const childName of childNames.sort()) { @@ -464,13 +283,13 @@ export function Inspect() { })(); // Visible columns in display order - const activeColumns = COLUMN_OPTIONS.filter((opt) => - visibleColumns.has(opt.key), - ); + const activeColumns = COLUMNS.filter((opt) => visibleColumns.has(opt.key)); // Sort packages - const sortedPackages = [...filteredPackages].sort(comparePackages); - const sortedTreeRoots = [...treeRoots].sort(comparePackages); + const sort = (a: Package, b: Package) => + comparePackages(a, b, sortColumn, sortDirection); + const sortedPackages = [...filteredPackages].sort(sort); + const sortedTreeRoots = [...treeRoots].sort(sort); return ( <> @@ -615,7 +434,7 @@ export function Inspect() { Show Virtual Packages - {COLUMN_OPTIONS.map((opt) => ( + {COLUMNS.map((opt) => ( renderTreeRows(pkg, 0, new Set()), ) - : sortedPackages.map((pkg) => renderTableRow(pkg))} + : sortedPackages.map((pkg) => ( + + ))}
diff --git a/src/components/pixi/inspect/packageDialog.tsx b/src/components/pixi/inspect/packageDialog.tsx index 4974261..7054c36 100644 --- a/src/components/pixi/inspect/packageDialog.tsx +++ b/src/components/pixi/inspect/packageDialog.tsx @@ -1,7 +1,7 @@ -import prettyBytes from "pretty-bytes"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; -import { Row } from "@/components/common/row"; +import { COLUMNS, getColumnValue } from "@/components/pixi/inspect/columns"; import { Badge } from "@/components/shadcn/badge"; import { Dialog, @@ -11,6 +11,7 @@ import { } from "@/components/shadcn/dialog"; import type { Package } from "@/lib/pixi/workspace/list"; +import { isUrl } from "@/lib/utils"; interface PackageDialogProps { pkg: Package; @@ -19,6 +20,12 @@ interface PackageDialogProps { } export function PackageDialog({ pkg, open, onOpenChange }: PackageDialogProps) { + const entries = COLUMNS.filter( + (c) => c.key !== "depends" && c.key !== "constrains", + ) + .map((col) => ({ col, value: getColumnValue(pkg, col.key) })) + .filter((e) => e.value); + return ( {pkg.name} -
- {/* General */} - - - {pkg.requested_spec && ( - - )} - - - {pkg.build && } - - {pkg.license && ( - - )} - {pkg.source && ( - - )} - - - {pkg.is_editable && ( - - )} - - - {/* File / Location */} - - {pkg.file_name && ( - - )} - {pkg.url && } - {pkg.subdir && ( - - )} - {pkg.platform && ( - - )} - {pkg.arch && } - {pkg.noarch && ( - - )} - - - {/* Size & Integrity */} - {(pkg.size_bytes != null || - pkg.sha256 || - pkg.md5 || - pkg.timestamp != null) && ( - - {pkg.size_bytes != null && ( - - )} - {pkg.sha256 && ( - - )} - {pkg.md5 && } - {pkg.timestamp != null && ( - - )} - - )} +
+
+ {entries.map(({ col, value }) => ( +
+
+ {col.label} +
+
+ {isUrl(value) ? ( + + ) : ( + value + )} +
+
+ ))} +
- {/* Dependencies */} {pkg.depends.length > 0 && ( - {[...pkg.depends].sort().map((dep) => ( - - ))} +
+ {[...pkg.depends].sort().map((dep) => ( + + {dep} + + ))} +
)} - {/* Constrains */} {pkg.constrains.length > 0 && ( - {[...pkg.constrains].sort().map((c) => ( - - ))} - - )} - - {/* Track Features */} - {pkg.track_features.length > 0 && ( -
- {pkg.track_features.map((f) => ( - - {f} + {[...pkg.constrains].sort().map((c) => ( + + {c} ))}
diff --git a/src/components/pixi/inspect/packageRow.tsx b/src/components/pixi/inspect/packageRow.tsx new file mode 100644 index 0000000..88fd400 --- /dev/null +++ b/src/components/pixi/inspect/packageRow.tsx @@ -0,0 +1,106 @@ +import { openUrl } from "@tauri-apps/plugin-opener"; +import { + ChevronRightIcon, + MicrochipIcon, + PackageCheckIcon, + PackageIcon, +} from "lucide-react"; + +import { + type ColumnDefinition, + getColumnValue, +} from "@/components/pixi/inspect/columns"; + +import type { Package } from "@/lib/pixi/workspace/list"; +import { isUrl } from "@/lib/utils"; + +export interface PackageRowProps { + pkg: Package; + depth: number; + nodeKey: string; + treeMode: boolean; + hasChildren: boolean; + isOpen: boolean; + activeColumns: ColumnDefinition[]; + highlightMatch: (text: string) => React.ReactNode; + onSelect: (pkg: Package) => void; + onToggleExpand: (key: string) => void; +} + +export function PackageRow({ + pkg, + depth, + nodeKey, + treeMode, + hasChildren, + isOpen, + activeColumns, + highlightMatch, + onSelect, + onToggleExpand, +}: PackageRowProps) { + return ( + onSelect(pkg)} + > + +
0 ? { paddingLeft: depth * 20 } : undefined} + > + {treeMode && + (hasChildren ? ( + + ) : ( +
+ ))} + {pkg.name.startsWith("__") ? ( + + ) : pkg.is_explicit ? ( + + ) : ( + + )} + {highlightMatch(pkg.name)} +
+ + {activeColumns.map((f) => { + const value = getColumnValue(pkg, f.key); + const isLink = isUrl(value); + return ( + + {isLink ? ( + + ) : ( + highlightMatch(value) + )} + + ); + })} + + ); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9f9d957..24b996a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -11,6 +11,10 @@ export function toPixiName(value: string): string { return value.toLowerCase().replace(/\s+/g, "-"); } +export function isUrl(value: string): boolean { + return value.startsWith("http://") || value.startsWith("https://"); +} + export const COMMON_PLATFORMS = [ { id: "win-64", name: "Windows (x64)" }, { id: "win-arm64", name: "Windows (ARM64)" }, From e4c35257f3aaeac5f4cee22463366b5191560fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 24 Feb 2026 14:01:39 +0100 Subject: [PATCH 25/32] Rework PackageDialog, allow navigation through dependency tree. --- src/components/pixi/inspect/columns.ts | 3 - src/components/pixi/inspect/inspect.tsx | 1 + src/components/pixi/inspect/packageDialog.tsx | 170 ++++++++++++++---- 3 files changed, 138 insertions(+), 36 deletions(-) diff --git a/src/components/pixi/inspect/columns.ts b/src/components/pixi/inspect/columns.ts index d3df0ba..e9a497f 100644 --- a/src/components/pixi/inspect/columns.ts +++ b/src/components/pixi/inspect/columns.ts @@ -37,7 +37,6 @@ export const COLUMNS: ColumnDefinition[] = [ { key: "version", label: "Version" }, { key: "requested-spec", label: "Requested Spec" }, { key: "build", label: "Build" }, - { key: "build-number", label: "Build Number" }, { key: "timestamp", label: "Timestamp" }, { key: "license", label: "License" }, { key: "license-family", label: "License Family" }, @@ -70,8 +69,6 @@ export function getColumnValue(pkg: Package, key: ColumnKey): string { return pkg.requested_spec ?? ""; case "build": return pkg.build ?? ""; - case "build-number": - return pkg.build_number != null ? String(pkg.build_number) : ""; case "timestamp": return pkg.timestamp != null ? new Date(pkg.timestamp).toLocaleDateString() diff --git a/src/components/pixi/inspect/inspect.tsx b/src/components/pixi/inspect/inspect.tsx index d8d7725..136e84a 100644 --- a/src/components/pixi/inspect/inspect.tsx +++ b/src/components/pixi/inspect/inspect.tsx @@ -526,6 +526,7 @@ export function Inspect() { {selectedPackage && ( !open && setSelectedPackage(null)} /> diff --git a/src/components/pixi/inspect/packageDialog.tsx b/src/components/pixi/inspect/packageDialog.tsx index 7054c36..204dd7d 100644 --- a/src/components/pixi/inspect/packageDialog.tsx +++ b/src/components/pixi/inspect/packageDialog.tsx @@ -1,8 +1,12 @@ import { openUrl } from "@tauri-apps/plugin-opener"; +import { ArrowLeftIcon } from "lucide-react"; +import { useState } from "react"; import { PreferencesGroup } from "@/components/common/preferencesGroup"; import { COLUMNS, getColumnValue } from "@/components/pixi/inspect/columns"; +import type { ColumnDefinition } from "@/components/pixi/inspect/columns"; import { Badge } from "@/components/shadcn/badge"; +import { Button } from "@/components/shadcn/button"; import { Dialog, DialogContent, @@ -15,66 +19,110 @@ import { isUrl } from "@/lib/utils"; interface PackageDialogProps { pkg: Package; + allPackages: Package[]; open: boolean; onOpenChange: (open: boolean) => void; } -export function PackageDialog({ pkg, open, onOpenChange }: PackageDialogProps) { - const entries = COLUMNS.filter( +export function PackageDialog({ + pkg: initialPkg, + allPackages, + open, + onOpenChange, +}: PackageDialogProps) { + const [history, setHistory] = useState([]); + const pkg = history.length > 0 ? history[history.length - 1] : initialPkg; + + const packageMap = new Map(); + for (const p of allPackages) { + packageMap.set(p.name, p); + } + + const reverseDeps = new Map(); + for (const p of allPackages) { + for (const dep of p.depends) { + const name = dep.split(/[\s[]/)[0]; + if (!reverseDeps.has(name)) reverseDeps.set(name, []); + reverseDeps.get(name)!.push(p.name); + } + } + + function navigateTo(name: string) { + const target = packageMap.get(name); + if (target) { + setHistory((prev) => [...prev, target]); + } + } + + function goBack() { + setHistory((prev) => prev.slice(0, -1)); + } + + function handleOpenChange(isOpen: boolean) { + if (!isOpen) { + setHistory([]); + } + onOpenChange(isOpen); + } + + const sourceKeys = new Set(["source", "file-name", "url", "sha256", "md5"]); + const allEntries = COLUMNS.filter( (c) => c.key !== "depends" && c.key !== "constrains", ) .map((col) => ({ col, value: getColumnValue(pkg, col.key) })) .filter((e) => e.value); + const entries = allEntries.filter((e) => !sourceKeys.has(e.col.key)); + const sourceEntries = allEntries.filter((e) => sourceKeys.has(e.col.key)); + + const deps = [...pkg.depends].sort().map((dep) => ({ + spec: dep, + name: dep.split(/[\s[]/)[0], + })); + const revDepNames = [...new Set(reverseDeps.get(pkg.name) ?? [])].sort(); return ( - + - {pkg.name} + + {history.length > 0 && ( + + )} + {pkg.name} +
-
- {entries.map(({ col, value }) => ( -
-
- {col.label} -
-
- {isUrl(value) ? ( - - ) : ( - value - )} -
-
- ))} -
+ {/* Details */} + + + - {pkg.depends.length > 0 && ( + {/* Dependencies */} + {deps.length > 0 && (
- {[...pkg.depends].sort().map((dep) => ( - - {dep} + {deps.map(({ spec, name }) => ( + navigateTo(name) : undefined + } + > + {spec} ))}
)} + {/* Constraints */} {pkg.constrains.length > 0 && (
@@ -86,8 +134,64 @@ export function PackageDialog({ pkg, open, onOpenChange }: PackageDialogProps) {
)} + + {/* Required By */} + {revDepNames.length > 0 && ( + +
+ {revDepNames.map((name) => ( + navigateTo(name)} + > + {name} + + ))} +
+
+ )} + + {/* Origin */} + {sourceEntries.length > 0 && ( + + + + )}
); } + +function EntryList({ + entries, +}: { + entries: { col: ColumnDefinition; value: string }[]; +}) { + return ( +
+ {entries.map(({ col, value }) => ( +
+
{col.label}
+
+ {isUrl(value) ? ( + + ) : ( + value + )} +
+
+ ))} +
+ ); +} From 16ea5a2deed20237e46f23ea3b153c30a39c7496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 24 Feb 2026 14:06:22 +0100 Subject: [PATCH 26/32] Switch back to upstream pixi --- src-tauri/Cargo.lock | 142 ++++++++++++++++++++++++++----------------- src-tauri/Cargo.toml | 2 +- 2 files changed, 86 insertions(+), 58 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 43e4c15..6681891 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -223,6 +223,21 @@ dependencies = [ "xattr", ] +[[package]] +name = "astral_async_zip" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab72a761e6085828cc8f0e05ed332b2554701368c5dc54de551bfaec466518ba" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite", + "pin-project", + "thiserror 1.0.69", + "tokio", + "tokio-util", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -400,6 +415,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-spooled-tempfile" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9c58a1dbecfc23aa470d57e5aff60877ab1b459bf05e60e861dd18fcaa5f5" +dependencies = [ + "tempfile", + "tokio", +] + [[package]] name = "async-task" version = "4.7.1" @@ -955,7 +980,7 @@ dependencies = [ [[package]] name = "barrier_cell" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "parking_lot", "thiserror 2.0.18", @@ -2446,7 +2471,7 @@ dependencies = [ [[package]] name = "fancy_display" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "console", ] @@ -5787,7 +5812,7 @@ dependencies = [ [[package]] name = "pixi_api" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "console", "dunce", @@ -5827,7 +5852,7 @@ dependencies = [ [[package]] name = "pixi_auth" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "pixi_config", "rattler_networking", @@ -5837,7 +5862,7 @@ dependencies = [ [[package]] name = "pixi_build_discovery" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "dunce", "itertools 0.14.0", @@ -5860,7 +5885,7 @@ dependencies = [ [[package]] name = "pixi_build_frontend" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "fs-err", "futures", @@ -5881,7 +5906,7 @@ dependencies = [ [[package]] name = "pixi_build_type_conversions" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "itertools 0.14.0", "ordermap", @@ -5894,7 +5919,7 @@ dependencies = [ [[package]] name = "pixi_build_types" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "ordermap", "pixi_stable_hash", @@ -5909,7 +5934,7 @@ dependencies = [ [[package]] name = "pixi_command_dispatcher" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "async-fd-lock", "base64 0.22.1", @@ -5968,7 +5993,7 @@ dependencies = [ [[package]] name = "pixi_config" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "clap", "console", @@ -5994,7 +6019,7 @@ dependencies = [ [[package]] name = "pixi_consts" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "console", "rattler_cache", @@ -6005,7 +6030,7 @@ dependencies = [ [[package]] name = "pixi_core" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "async-once-cell", "barrier_cell", @@ -6102,7 +6127,7 @@ dependencies = [ [[package]] name = "pixi_default_versions" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "rattler_conda_types", ] @@ -6110,7 +6135,7 @@ dependencies = [ [[package]] name = "pixi_diff" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "ahash 0.8.12", "console", @@ -6128,7 +6153,7 @@ dependencies = [ [[package]] name = "pixi_git" version = "0.0.1" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "dashmap", "dunce", @@ -6149,7 +6174,7 @@ dependencies = [ [[package]] name = "pixi_glob" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "dashmap", "fs-err", @@ -6166,7 +6191,7 @@ dependencies = [ [[package]] name = "pixi_install_pypi" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "ahash 0.8.12", "chrono", @@ -6227,7 +6252,7 @@ dependencies = [ [[package]] name = "pixi_manifest" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "chrono", "console", @@ -6268,7 +6293,7 @@ dependencies = [ [[package]] name = "pixi_path" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "fs-err", "serde", @@ -6279,7 +6304,7 @@ dependencies = [ [[package]] name = "pixi_progress" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "indicatif", "parking_lot", @@ -6288,7 +6313,7 @@ dependencies = [ [[package]] name = "pixi_pypi_spec" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "itertools 0.14.0", "pep440_rs", @@ -6308,7 +6333,7 @@ dependencies = [ [[package]] name = "pixi_python_status" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "rattler", ] @@ -6316,7 +6341,7 @@ dependencies = [ [[package]] name = "pixi_record" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "file_url", "itertools 0.14.0", @@ -6339,7 +6364,7 @@ dependencies = [ [[package]] name = "pixi_reporters" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "console", "futures", @@ -6371,7 +6396,7 @@ dependencies = [ [[package]] name = "pixi_spec" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "dirs", "file_url", @@ -6397,7 +6422,7 @@ dependencies = [ [[package]] name = "pixi_spec_containers" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "indexmap 2.13.0", "itertools 0.14.0", @@ -6409,7 +6434,7 @@ dependencies = [ [[package]] name = "pixi_stable_hash" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "ordermap", "rattler_conda_types", @@ -6421,7 +6446,7 @@ dependencies = [ [[package]] name = "pixi_toml" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "digest", "hex", @@ -6436,7 +6461,7 @@ dependencies = [ [[package]] name = "pixi_url" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "bzip2 0.6.1", "dashmap", @@ -6466,7 +6491,7 @@ dependencies = [ [[package]] name = "pixi_utils" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "async-fd-lock", "fs-err", @@ -6502,7 +6527,7 @@ dependencies = [ [[package]] name = "pixi_uv_context" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "fs-err", "miette 7.6.0", @@ -6527,7 +6552,7 @@ dependencies = [ [[package]] name = "pixi_uv_conversions" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "chrono", "dunce", @@ -6562,7 +6587,7 @@ dependencies = [ [[package]] name = "pixi_variant" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "pixi_build_types", "rattler_lock", @@ -6911,7 +6936,7 @@ dependencies = [ [[package]] name = "pypi_mapping" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "async-once-cell", "dashmap", @@ -6941,7 +6966,7 @@ dependencies = [ [[package]] name = "pypi_modifiers" version = "0.1.0" -source = "git+https://github.com/haecker-felix/pixi?branch=pixi_api_list#672c2de257513e8edbcb272fb7ada857ca5bab6f" +source = "git+https://github.com/prefix-dev/pixi#96a96f11d3af81f4e2f346bff79765a9f0255928" dependencies = [ "miette 7.6.0", "pixi_default_versions", @@ -7206,9 +7231,9 @@ dependencies = [ [[package]] name = "rattler" -version = "0.39.10" +version = "0.39.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf0a84307599e2db6ddbe757ee7568b11f71a3ce8fa90cc520206a53a93e4482" +checksum = "a2b36266a8e3aa3ff6a029e2b292c3c070a625a28a5d8d420d254d9cc020a5ed" dependencies = [ "anyhow", "digest", @@ -7249,9 +7274,9 @@ dependencies = [ [[package]] name = "rattler_cache" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03fb92095e4998b5c985a03edeed2bb4d98dfedf19bc585982237ea1e4df628" +checksum = "c1fbe0b42ef6292283dc1ad0f71b67a43cc30e733de0bab198981e03c1889a0d" dependencies = [ "ahash 0.8.12", "anyhow", @@ -7282,9 +7307,9 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.43.1" +version = "0.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "274c3177bbd304ee0963d37855d62b5234c0b243437d02769fa61d4de79047c6" +checksum = "9005286dbf1cb4ce89da2ff359e97e75b5f7f1cb434857822deb55acba00aa8c" dependencies = [ "ahash 0.8.12", "chrono", @@ -7344,9 +7369,9 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.26.12" +version = "0.26.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8c093a0b5edcd91e7823001ca251342332765f10629d80d3954febbb06f095" +checksum = "8f031cf58694067a9ac437d4f7032c7ecd49fe6791cba31fb3866b3d1b6e98a0" dependencies = [ "ahash 0.8.12", "chrono", @@ -7380,9 +7405,9 @@ dependencies = [ [[package]] name = "rattler_menuinst" -version = "0.2.45" +version = "0.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7c55d5a3afeb5b2230ff3bc360a1e02a1fedff1698561024d3cd8133a5120" +checksum = "680f72dbbfeff0559b49cf9cccdf2d3c81c41df65d9e2090b18d5a863a51581b" dependencies = [ "chrono", "configparser", @@ -7411,9 +7436,9 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.25.32" +version = "0.25.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97335853bd2e219abd08004b9548667ed93feed1e3574e6a4dc052ec3a69266d" +checksum = "0eda517ae63aa07521f0016d42d1cff311f7ab08456148485b3d6b328d791ca3" dependencies = [ "anyhow", "async-once-cell", @@ -7443,15 +7468,18 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.23.24" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7c879af2e36e42e829d44f98ecafa2cbf377cdb81002fd59849d7de16bb1b8" +checksum = "38c239645576e4ae97aacf132763a1988981bee1366e9d287141d43592fe80e0" dependencies = [ "astral-tokio-tar", + "astral_async_zip", "async-compression", + "async-spooled-tempfile", "bzip2 0.6.1", "chrono", "fs-err", + "futures", "futures-util", "getrandom 0.2.17", "getrandom 0.3.4", @@ -7500,9 +7528,9 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.25.10" +version = "0.25.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45327ab1159e76a4eb33047ec782cf14409cb157c1c3a04f499877d14013533" +checksum = "a9c27220d71e4ae2563e6b82278fae5c43e20f0a9c92c9bac29e7fae681d89d0" dependencies = [ "anyhow", "async-compression", @@ -7561,9 +7589,9 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.25.18" +version = "0.25.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccec6f2170a78989f2de4cc46ce9dc231151ed5d0f19ef8071118612686b6cae" +checksum = "be6ca946206e132f00d5035de6c075f3aa95f89632ee2bc8af1305b6d0d5a5db" dependencies = [ "anyhow", "enum_dispatch", @@ -7581,9 +7609,9 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10781d00bbb3ccbdad8c2402ff6147552281450eb6ecdc0e12bcc153418d355f" +checksum = "1d3525adc4c8a1b6a08beeea4cbba3dcf79c837a5b3a066544444eaa6dfd5326" dependencies = [ "chrono", "futures", @@ -7600,9 +7628,9 @@ dependencies = [ [[package]] name = "rattler_virtual_packages" -version = "2.3.7" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954f20a004bac67e28f1ca4744f24d717837b470c9899eebd4e54beffac56a3" +checksum = "bd96ff7990366c336bd9b03e8cc4e8f38c2cfc1957c5276dc256b9f4463b694b" dependencies = [ "archspec", "libloading 0.9.0", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 30bb078..2b04d78 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,7 +27,7 @@ miette = "7" notify = "8" notify-debouncer-full = "0.7" percent-encoding = "2" -pixi_api = { package = "pixi_api", git = "https://github.com/haecker-felix/pixi", branch = "pixi_api_list" } +pixi_api = { package = "pixi_api", git = "https://github.com/prefix-dev/pixi" } portable-pty = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" From b8c3d287db908e49c32a79fad2815ce85678b365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 24 Feb 2026 14:08:45 +0100 Subject: [PATCH 27/32] Use regular cursor for rows instead of text caret --- src/components/pixi/inspect/packageRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pixi/inspect/packageRow.tsx b/src/components/pixi/inspect/packageRow.tsx index 88fd400..0b623f3 100644 --- a/src/components/pixi/inspect/packageRow.tsx +++ b/src/components/pixi/inspect/packageRow.tsx @@ -41,7 +41,7 @@ export function PackageRow({ }: PackageRowProps) { return ( onSelect(pkg)} > From e78ba932fa13086bbac3c8d067fceedf70d201eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 24 Feb 2026 14:09:52 +0100 Subject: [PATCH 28/32] Expose time in timestamp as well --- src/components/pixi/inspect/columns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pixi/inspect/columns.ts b/src/components/pixi/inspect/columns.ts index e9a497f..3789bca 100644 --- a/src/components/pixi/inspect/columns.ts +++ b/src/components/pixi/inspect/columns.ts @@ -71,7 +71,7 @@ export function getColumnValue(pkg: Package, key: ColumnKey): string { return pkg.build ?? ""; case "timestamp": return pkg.timestamp != null - ? new Date(pkg.timestamp).toLocaleDateString() + ? new Date(pkg.timestamp).toLocaleString() : ""; case "license": return pkg.license ?? ""; From aeb42950a402f8b5794766dbf01f97ab63e74b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4cker?= Date: Tue, 24 Feb 2026 14:13:13 +0100 Subject: [PATCH 29/32] Make linters happy --- src/components/pixi/inspect/columns.ts | 10 +++------- src/components/pixi/inspect/packageDialog.tsx | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/pixi/inspect/columns.ts b/src/components/pixi/inspect/columns.ts index 3789bca..c9a61ed 100644 --- a/src/components/pixi/inspect/columns.ts +++ b/src/components/pixi/inspect/columns.ts @@ -6,7 +6,6 @@ export type ColumnKey = | "version" | "requested-spec" | "build" - | "build-number" | "timestamp" | "license" | "license-family" @@ -33,7 +32,7 @@ export interface ColumnDefinition { } export const COLUMNS: ColumnDefinition[] = [ - { key: "kind", label: "Kind" }, + { key: "kind", label: "Package Kind" }, { key: "version", label: "Version" }, { key: "requested-spec", label: "Requested Spec" }, { key: "build", label: "Build" }, @@ -48,8 +47,8 @@ export const COLUMNS: ColumnDefinition[] = [ { key: "arch", label: "Architecture" }, { key: "noarch", label: "Noarch" }, { key: "size", label: "Size" }, - { key: "sha256", label: "SHA256" }, - { key: "md5", label: "MD5" }, + { key: "sha256", label: "SHA256 Hash" }, + { key: "md5", label: "MD5 Hash" }, { key: "depends", label: "Dependencies" }, { key: "constrains", label: "Constrains" }, ]; @@ -124,9 +123,6 @@ export function comparePackages( case "timestamp": result = (a.timestamp ?? 0) - (b.timestamp ?? 0); break; - case "build-number": - result = (a.build_number ?? 0) - (b.build_number ?? 0); - break; default: result = getColumnValue(a, sortColumn).localeCompare( getColumnValue(b, sortColumn), diff --git a/src/components/pixi/inspect/packageDialog.tsx b/src/components/pixi/inspect/packageDialog.tsx index 204dd7d..fefc081 100644 --- a/src/components/pixi/inspect/packageDialog.tsx +++ b/src/components/pixi/inspect/packageDialog.tsx @@ -181,7 +181,7 @@ function EntryList({ {isUrl(value) ? (