Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions desktop/src/shared/flow-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const shown = (
effect: FlowEffect,
color: FlowActionView["color"] = "neutral",
caption?: string,
): FlowActionView => ({ label, icon: `i-lucide-${icon}`, effect, color, caption });
): FlowActionView => ({ label, icon, effect, color, caption });

export const retryCaption = (lane: FlowLane, node: FlowRun): string => {
const kept = lane.nodes
Expand Down Expand Up @@ -82,23 +82,27 @@ export const desktopFlowAction = (
one.id === (node.pauseTarget ?? stepId) ||
one.runs.some((run) => run.id === node.pauseTarget),
) ?? node;
return shown(target.pause == null ? `Pause before ${target.title}` : "Don't pause", "pause", {
kind: "write",
installation,
write: {
_tag: "Control",
runId,
control: "hold",
set: target.pause == null,
before: target.id,
return shown(
target.pause == null ? `Pause before ${target.title}` : "Don't pause",
"i-lucide-pause",
{
kind: "write",
installation,
write: {
_tag: "Control",
runId,
control: "hold",
set: target.pause == null,
before: target.id,
},
},
});
);
}
case "note":
case "retry-noted":
return shown(
kind === "note" ? `Note for ${node.title}…` : "Retry with a note…",
"message-square-plus",
"i-lucide-message-square-plus",
{
kind: "text",
installation,
Expand All @@ -108,35 +112,39 @@ export const desktopFlowAction = (
},
);
case "log":
return shown("Log", "scroll-text", { kind: "part", run: runId, part: "log" });
return shown("Log", "i-lucide-scroll-text", { kind: "part", run: runId, part: "log" });
case "evidence":
return shown("Evidence", "file-check", { kind: "part", run: runId, part: "evidence" });
return shown("Evidence", "i-lucide-file-check", {
kind: "part",
run: runId,
part: "evidence",
});
case "retry":
return shown(
`Retry ${node.title}`,
"rotate-ccw",
"i-lucide-rotate-ccw",
{ kind: "write", installation, write: { _tag: "Resume", runId: node.link?.run ?? runId } },
"error",
retryCaption(lane, node),
);
case "continue":
return shown("Continue", "play", {
return shown("Continue", "i-lucide-play", {
kind: "write",
installation,
write: { _tag: "Control", runId, control: "hold", set: false },
});
case "tell":
return node.agent == null
? null
: shown(`Tell the ${role}…`, "message-square", {
: shown(`Tell the ${role}…`, "i-lucide-message-square", {
kind: "text",
installation,
write: { _tag: "Deliver", runId, agent: node.agent, text: "" },
});
case "terminal":
return node.agent == null
? null
: shown(`Watch the ${role}`, "terminal", {
: shown(`Watch the ${role}`, "i-lucide-terminal", {
kind: "terminal",
run: runId,
agent: node.agent,
Expand All @@ -157,7 +165,7 @@ export const desktopFlowAction = (
};
return shown(
"Answer",
"message-circle-question",
"i-lucide-message-circle-question",
{
kind: "answer",
installation,
Expand All @@ -173,7 +181,7 @@ export const desktopFlowAction = (
? null
: shown(
"Review",
"check",
"i-lucide-check",
{
kind: "proposal",
installation,
Expand All @@ -186,20 +194,20 @@ export const desktopFlowAction = (
}
case "open": {
if (node.link?.run != null)
return shown(`Open ${node.title}`, "arrow-up-right", {
return shown(`Open ${node.title}`, "i-lucide-arrow-up-right", {
kind: "run",
run: node.link.run,
machine: node.link.machine,
});
return node.url === undefined
? null
: shown(node.id === "ci" ? "Open pipeline" : "Open MR", "external-link", {
: shown(node.id === "ci" ? "Open pipeline" : "Open MR", "i-lucide-external-link", {
kind: "url",
url: node.url,
});
}
case "start":
return shown(node.title, "play", {
return shown(node.title, "i-lucide-play", {
kind: "offer",
run: runId,
offer: node.id.replace(/^offer:/, ""),
Expand All @@ -217,31 +225,31 @@ export const desktopLaneAction = (
const runId = lane.run;
switch (kind) {
case "hold":
return shown("Hold run", "pause", {
return shown("Hold run", "i-lucide-pause", {
kind: "write",
installation,
write: { _tag: "Control", runId, control: "hold", set: true },
});
case "release":
return shown("Release hold", "play", {
return shown("Release hold", "i-lucide-play", {
kind: "write",
installation,
write: { _tag: "Control", runId, control: "hold", set: false },
});
case "stop":
return shown("Stop run", "square", {
return shown("Stop run", "i-lucide-square", {
kind: "write",
installation,
write: { _tag: "Control", runId, control: "stop", set: true },
});
case "resume":
return shown("Resume", "rotate-ccw", {
return shown("Resume", "i-lucide-rotate-ccw", {
kind: "write",
installation,
write: { _tag: "Resume", runId },
});
case "follow-up":
return shown("Follow-up run", "message-square-plus", {
return shown("Follow-up run", "i-lucide-message-square-plus", {
kind: "text",
installation,
write: { _tag: "FollowUp", runId, text: "" },
Expand Down
43 changes: 38 additions & 5 deletions desktop/src/shared/flow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Flow, FlowNode, FlowRun } from "../../../src/board-model";
import { epochMs } from "../../../src/time";
import { stepTiming } from "./step-timing";
export interface FlowSpot {
readonly run: string;
Expand Down Expand Up @@ -122,7 +123,39 @@ export type FlowEntry = {
readonly folded: boolean;
readonly wave?: number;
};
export const inspectorOf = (flow: Flow, selection: FlowSelection, now: number) => {
const dateParts = (ms: number, timeZone: string | undefined) =>
Object.fromEntries(
new Intl.DateTimeFormat("en-GB", {
timeZone,
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
})
.formatToParts(ms)
.map((part) => [part.type, part.value]),
);

/** An instant on this Machine's clock: `11:22` today, `24 Oct, 11:22` on another day. */
export const whenOf = (iso: string, now: number, timeZone?: string): string => {
const ms = epochMs(iso);
if (Number.isNaN(ms)) return iso;
const at = dateParts(ms, timeZone);
const today = dateParts(now, timeZone);
const time = `${at.hour}:${at.minute}`;
if (at.year !== today.year) return `${at.day} ${at.month} ${at.year}, ${time}`;
return at.month === today.month && at.day === today.day ? time : `${at.day} ${at.month}, ${time}`;
};

export const inspectorOf = (
flow: Flow,
selection: FlowSelection,
now: number,
timeZone?: string,
) => {
const when = (iso: string) => whenOf(iso, now, timeZone);
const parent = nodeAt(flow, selection);
if (parent === null) return null;
const all = [...parent.runs, ...(parent.links ?? [])];
Expand All @@ -131,20 +164,20 @@ export const inspectorOf = (flow: Flow, selection: FlowSelection, now: number) =
if (node.agent != null) lines.push(`Agent ${node.agent}`);
if (node.ranOn !== null) lines.push([node.role, node.ranOn].filter(Boolean).join(" · "));
if (node.started !== null) {
lines.push(`Started ${node.started}`);
lines.push(`Started ${when(node.started)}`);
}
const timing = stepTiming(node, now);
if (timing !== null) lines.push(timing);
if (node.ended !== null) lines.push(`Ended ${node.ended}`);
if (node.ended !== null) lines.push(`Ended ${when(node.ended)}`);
node.attempts
.slice(1)
.forEach((attempt, i) =>
lines.push(`Attempt ${i + 2} · ${attempt.cause ?? "retried"} · ${attempt.at}`),
lines.push(`Attempt ${i + 2} · ${attempt.cause ?? "retried"} · ${when(attempt.at)}`),
);
for (const question of node.questions)
if (question.answered !== null)
lines.push(
`Answered ${question.answered}${question.by === null ? "" : ` by ${question.by}`}`,
`Answered ${when(question.answered)}${question.by === null ? "" : ` by ${question.by}`}`,
);
const waves = [
...new Set(all.flatMap((one) => (one.wave === undefined ? [] : [one.wave]))),
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/shared/terminal-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ export const linksIn = (line: string) =>
/** Where a pane is, as its card says it: Machine › workspace › tab. */
export const paneWhere = (machine: string, at: PaneAt) =>
[machine, at.workspace, at.tab].filter((part) => part !== null).join(" › ");

/** A pane on its card's one line, whose footer names the Machine: tab · workspace. */
export const paneLine = (at: PaneAt) =>
[at.tab, at.workspace].filter((part) => part !== null).join(" · ");
23 changes: 18 additions & 5 deletions desktop/view/app/components/BoardCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ const menu = computed(() =>
:data-as-of="asOf === null ? undefined : ''"
:variant="task.state === 'blocked' ? 'soft' : 'outline'"
:class="{ 'opacity-50': asOf !== null, 'ring-2 ring-primary': chosen }"
:ui="{
header: 'px-4 py-3 sm:px-4',
body: 'px-4 py-3 sm:p-4 sm:py-3',
footer: 'px-4 py-2 sm:px-4',
}"
>
<template #header>
<div class="flex items-start justify-between gap-2">
Expand Down Expand Up @@ -215,13 +220,15 @@ const menu = computed(() =>
</UDropdownMenu>
</div>
</div>
<small class="text-muted">{{ repoAndBranch }}</small>
<small class="mt-0.5 block truncate text-muted" :title="repoAndBranch">{{
repoAndBranch
}}</small>
</template>
<p data-testid="sentence" class="text-sm">{{ task.sentence }}</p>
<button
v-if="flow !== null"
type="button"
class="mt-2 flex w-full items-center gap-2 text-left text-xs text-muted hover:text-default focus-visible:outline-2 focus-visible:outline-primary"
class="mt-2 flex w-full flex-wrap items-center gap-x-2 gap-y-1 text-left text-xs text-muted hover:text-default focus-visible:outline-2 focus-visible:outline-primary"
:aria-label="flow.name"
@click="gesture({ kind: 'name', card: cardKey }, 'flow')"
>
Expand All @@ -239,7 +246,7 @@ const menu = computed(() =>
<FlowGlyph v-for="(glyph, at) in flow.release" :key="at" :state="glyph.state" />
</span>
</span>
<span class="min-w-0 truncate">{{ flow.label }}</span>
<span class="min-w-0">{{ flow.label }}</span>
</button>
<p v-if="task.held" class="text-sm text-muted">{{ task.held }}</p>
<p v-if="task.drift" class="text-sm text-warning">{{ task.drift }}</p>
Expand Down Expand Up @@ -284,8 +291,14 @@ const menu = computed(() =>
/>
</div>

<div v-if="pane" class="mt-3 flex flex-col gap-1 text-sm" data-testid="pane">
<span data-testid="pane-where">{{ pane.where }}</span>
<div v-if="pane" class="mt-2 flex flex-col gap-1 text-xs text-muted" data-testid="pane">
<span class="flex min-w-0 items-center gap-1" :title="pane.where"
><UIcon name="i-lucide-terminal" class="shrink-0" aria-hidden="true" /><span
class="truncate"
data-testid="pane-where"
>{{ pane.line }}</span
></span
>
<div v-if="pane.command !== null" class="flex items-center gap-1">
<code class="truncate text-xs" data-testid="pane-command">{{ pane.command }}</code>
<UButton
Expand Down
3 changes: 2 additions & 1 deletion desktop/view/app/components/FlowGlyph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ const colors: Record<FlowState, string> = {
<style scoped>
.flow-turn {
transform-origin: center;
animation: flow-turn 6s linear infinite;
/* Stepped: Desktop composites in software, where a smooth turn repaints every frame. */
animation: flow-turn 6s steps(12) infinite;
}
@keyframes flow-turn {
to {
Expand Down
2 changes: 1 addition & 1 deletion desktop/view/app/components/FlowInspector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const labels = {
>
{{ line }}
</p>
<slot />
<p v-if="info.description" class="text-sm">{{ info.description }}</p>
<slot name="decision" />
<UButton
Expand Down Expand Up @@ -95,6 +96,5 @@ const labels = {
:label="`Open in ${labels[info.open.tab] ?? info.open.tab}`"
@click="emit('open', info.open.run, info.open.tab)"
/>
<slot />
</section>
</template>
Loading
Loading