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
40 changes: 36 additions & 4 deletions packages/features/src/task-list-live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CONTEXT_WINDOW,
type CodingAgentArchivedBy,
type CodingAgentStatus,
type CodingAgentSummary,
codingAgentPullRequestSchema,
codingAgentSummarySchema,
PULL_REQUEST_STATE,
Expand Down Expand Up @@ -324,16 +325,16 @@ function archivedByOf(row: AgentLiveRow): CodingAgentArchivedBy | undefined {
};
}

/** An agent with no plan as the list carries it, as its own page reads it, its status read at `now`. */
function tasklessAgentOf(row: AgentLiveRow, now: number): TasklessAgent {
/** One agent as the tabs draw it, on a plan or with none, its status read at `now`, as the service's summary of it says it. */
function agentSummaryOf(row: AgentLiveRow, now: number): CodingAgentSummary {
const servers = (row.disabledServers ?? []).filter(
(name) => integrationConnectionOf(name) !== undefined,
);
const archivedBy = archivedByOf(row);
const started = runStartedAt(row, now);
return {
id: row.id,
planId: null,
planId: row.planId,
model: row.model,
effort: row.effort,
...(row.contextWindow === CONTEXT_WINDOW.STANDARD
Expand All @@ -350,10 +351,19 @@ function tasklessAgentOf(row: AgentLiveRow, now: number): TasklessAgent {
...(row.starterName === null
? undefined
: { startedBy: { userId: row.starterId, name: row.starterName } }),
automationId: row.automationId,
...(row.automationId === null ? undefined : { automationId: row.automationId }),
...(row.automationName === null ? undefined : { automationName: row.automationName }),
...(row.startedFrom === null ? undefined : { from: row.startedFrom }),
...(started === undefined ? undefined : { runStartedAt: started }),
};
}

/** An agent with no plan as the list carries it, as its own page reads it, its status read at `now`. */
function tasklessAgentOf(row: AgentLiveRow, now: number): TasklessAgent {
return {
...agentSummaryOf(row, now),
planId: null,
automationId: row.automationId,
workspaceId: row.workspaceId,
repository: row.repository,
activityAt: activityAt(row),
Expand All @@ -372,6 +382,28 @@ function pullRequestOf(published: AgentLiveRow["published"]) {
return { pullRequest: { number, title, url, state, checks } };
}

/** First started first, and by id among those started at one instant, as a plan's agents are listed. */
function firstStarted(a: AgentLiveRow, b: AgentLiveRow): number {
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
}

/**
* Every agent on a plan, the archived ones left out, each as the plan's own
* list of agents names it with its status read at `now`, the first started
* first: what the desktop's ledger of agents reads where it once listed
* each plan's agents again on a clock.
*/
export function taskAgentsOfLiveRows(
rows: TaskListLiveRows,
now: number,
): readonly CodingAgentSummary[] {
return rows.agents
.filter((row) => row.planId !== null && row.archivedAt === null)
.toSorted(firstStarted)
.map((row) => agentSummaryOf(row, now));
}

/**
* The plan list as the held read answers it with every archived plan and
* agent included, as the live rows stand at `now`: the plans newest started
Expand Down
8 changes: 8 additions & 0 deletions packages/features/src/tasks-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CODING_AGENT_BOUNDS,
codingAgentCallIdSchema,
codingAgentMessagesAnswerSchema,
codingAgentSummarySchema,
storedObjectSchema,
tasklessAgentSchema,
turnRecordFields,
Expand Down Expand Up @@ -403,6 +404,13 @@ export const tasksViewSchema = EffectSchema.Struct({
tasks: EffectSchema.Array(taskSummarySchema),
/** The agents with no plan in the account's workspaces, newest started first, as the last list read answered; absent before the first that carried them. */
tasklessAgents: EffectSchema.optionalKey(EffectSchema.Array(tasklessAgentSchema)),
/**
* Every agent on a plan, the archived ones left out, each as its plan's
* own list names it, the first started first: present only while the list
* is followed live over Neon Realtime, when it is what the notices' ledger
* reads in place of listing each plan's agents again on a clock.
*/
taskAgents: EffectSchema.optionalKey(EffectSchema.Array(codingAgentSummarySchema)),
listStatus: tasksReadSchema,
/** The workspaces the account is a member of, its first one first, as the last list read answered; none before the first lands. */
workspaces: EffectSchema.Array(workspaceSchema),
Expand Down
41 changes: 1 addition & 40 deletions packages/host/src/act-port/agents-procedures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ import {
type CodingAgentTranscriptKey,
TRANSCRIPT_KIND,
} from "@luke/features/coding-agent-view";
import {
CHECK_SUMMARY,
CODING_AGENT_STATUS,
PULL_REQUEST_STATE,
} from "@luke/features/coding-agent-wire";
import { CODING_AGENT_STATUS } from "@luke/features/coding-agent-wire";
import {
FILE_VIEWED_STATE,
PULL_REQUEST_ACTION,
Expand Down Expand Up @@ -63,22 +59,6 @@ const STARTED = {

const UNANSWERED = { failure: CODING_AGENT_CALL_FAILURE.UNANSWERED } as const;

/** What an agent published, as the service answers it: its branch and an open pull request. */
const PUBLISHED = {
repository: "acme/relay",
branch: "luke/invites",
pullRequest: {
number: 41,
title: "Teammate invitations",
url: "https://github.com/acme/relay/pull/41",
state: PULL_REQUEST_STATE.OPEN,
checks: CHECK_SUMMARY.PASSING,
additions: 12,
deletions: 3,
changedFiles: 2,
},
};

/** Each kind's refusal, as the window drew it before the kinds moved onto the port. */
const REFUSAL = {
[AGENT_ACT_KIND.CODING_AGENTS_MODELS]: "Could not read the models on this system.",
Expand Down Expand Up @@ -296,9 +276,6 @@ function windowPort(
observeModels: (models) => {
noted.push(`models:${models.length}`);
},
observePublished: (agentId, pullRequest) => {
noted.push(`published:${agentId}=${pullRequest?.number ?? "none"}`);
},
observeTaskGone: (taskId) => {
noted.push(`gone:${taskId}`);
},
Expand Down Expand Up @@ -494,22 +471,6 @@ it("a panel's word on what it shows is noted whole, the Chat as none where it na
assert.deepEqual(asked, []);
});

it("a panel's read of an agent's own pull request is noted as what it published, and a read of another by its number is not", async () => {
const { call, noted } = windowPort(PANEL, {
answers: { codingAgentPullRequest: () => Effect.succeed(PUBLISHED) },
});

const own = await call(AGENT_ACT_KIND.CODING_AGENTS_PULL_REQUEST, { agentId: AGENT_ID });
const other = await call(AGENT_ACT_KIND.CODING_AGENTS_PULL_REQUEST, {
agentId: AGENT_ID,
number: 52,
});

assert.deepEqual(own, { answer: PUBLISHED });
assert.deepEqual(other, { answer: PUBLISHED });
assert.deepEqual(noted, [`published:${AGENT_ID}=41`]);
});

it("a panel's change to an agent's pull request reaches the host as asked and hears how GitHub took it, and its read of which files it viewed hears each file's mark", async () => {
const viewed = {
outcome: PULL_REQUEST_WRITE_OUTCOME.DONE,
Expand Down
8 changes: 1 addition & 7 deletions packages/host/src/act-port/agents-procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ export interface AgentProceduresDependencies {
| "observeTitle"
| "observeMessaged"
| "observeModels"
| "observePublished"
| "observeTaskGone"
| "shown"
| "shownInbox"
Expand Down Expand Up @@ -360,12 +359,7 @@ export function agentProcedures(dependencies: AgentProceduresDependencies) {
AGENT_ACT_KIND.CODING_AGENTS_PULL_REQUEST
].effect(function* ({ input, context, errors }) {
yield* refuseUnlessPanel(context.actor, () => errors.REFUSED());
// Note that a read of another pull request by its number is no word on what the agent itself published.
return yield* noted(host.codingAgentPullRequest(input), (answer) => {
if ("pullRequest" in answer && input.number === undefined) {
notices.observePublished(input.agentId, answer.pullRequest);
}
});
return yield* host.codingAgentPullRequest(input);
}),
[AGENT_ACT_KIND.CODING_AGENTS_CHANGES]: actPort[AGENT_ACT_KIND.CODING_AGENTS_CHANGES].effect(
function* ({ input, context, errors }) {
Expand Down
Loading
Loading