This commit is contained in:
+166
-26
@@ -12,6 +12,8 @@ import {
|
||||
import type { GiteaIssueAttachment, GiteaIssueComment, GiteaIssueSummary, GiteaRepoSummary } from "./api";
|
||||
import type { KnownUrl, LocalContext, LocalWorkspace } from "./localContext";
|
||||
|
||||
type CropHandle = "n" | "e" | "s" | "w" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
const status = document.querySelector<HTMLElement>("#status");
|
||||
const refreshData = document.querySelector<HTMLButtonElement>("#refresh-data");
|
||||
const addUrlButton = document.querySelector<HTMLButtonElement>("#add-url");
|
||||
@@ -185,6 +187,9 @@ function renderWorkspaces(workspaces: LocalWorkspace[]): string {
|
||||
}
|
||||
|
||||
function renderAgents(agents: AgentProfile[]): string {
|
||||
const state = appStore.getState();
|
||||
const draft = state.agentDraft;
|
||||
|
||||
return `
|
||||
<div class="compact-list">
|
||||
${agents
|
||||
@@ -196,11 +201,39 @@ function renderAgents(agents: AgentProfile[]): string {
|
||||
<span class="path-text">${escapeHtml(agent.file)}</span>
|
||||
<span class="muted">${escapeHtml(agent.description)}</span>
|
||||
</div>
|
||||
<div class="agent-actions">
|
||||
<button class="secondary-button compact-button" type="button" data-edit-agent="${agent.id}">Edit</button>
|
||||
<button class="danger-button compact-button" type="button" data-delete-agent="${agent.id}">Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
<article class="item-card agent-editor-card">
|
||||
<div class="form-stack">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input id="agent-name" type="text" value="${escapeHtml(draft.name)}" placeholder="GLOBAL AGENT" />
|
||||
</label>
|
||||
<label>
|
||||
<span>File</span>
|
||||
<input id="agent-file" type="text" value="${escapeHtml(draft.file)}" placeholder="AGENTS.md" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Description</span>
|
||||
<input id="agent-description" type="text" value="${escapeHtml(draft.description)}" placeholder="Agent handoff rules" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Content</span>
|
||||
<textarea id="agent-content" rows="14" placeholder="Markdown content">${escapeHtml(draft.content)}</textarea>
|
||||
</label>
|
||||
<div class="agent-editor-actions">
|
||||
<button id="new-agent" class="secondary-button" type="button">New</button>
|
||||
<button id="save-agent" class="primary-button" type="button">${state.editingAgentId ? "Save agent" : "Add agent"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -466,20 +499,11 @@ function renderScreenshotPreview(): string {
|
||||
</div>
|
||||
<div class="crop-frame">
|
||||
<img src="${screenshot.dataUrl}" alt="${escapeHtml(screenshot.name)}" />
|
||||
<div class="crop-box" style="left:${crop.left}%; top:${crop.top}%; width:${crop.width}%; height:${crop.height}%"></div>
|
||||
</div>
|
||||
<div class="crop-controls">
|
||||
${(["left", "top", "width", "height"] as const)
|
||||
.map(
|
||||
(field) => `
|
||||
<label>
|
||||
<span>${field}</span>
|
||||
<input type="range" min="${field === "width" || field === "height" ? "1" : "0"}" max="100" value="${crop[field]}" data-crop-field="${field}" />
|
||||
<output>${crop[field]}%</output>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
<button class="crop-box" type="button" data-crop-drag style="left:${crop.left}%; top:${crop.top}%; width:${crop.width}%; height:${crop.height}%">
|
||||
${(["nw", "n", "ne", "e", "se", "s", "sw", "w"] as CropHandle[])
|
||||
.map((handle) => `<span class="crop-handle crop-handle-${handle}" data-crop-handle="${handle}"></span>`)
|
||||
.join("")}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="primary-button" type="button" data-apply-screenshot-crop>Crop</button>
|
||||
@@ -631,10 +655,13 @@ function renderAddIssue(): string {
|
||||
return `
|
||||
<article class="item-card issue-card">
|
||||
<div class="target-summary">
|
||||
<span>Target repo</span>
|
||||
<span class="target-label">Target repo</span>
|
||||
<a class="item-title" href="${knownUrl.giteaRepo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.giteaRepo.fullName)}</a>
|
||||
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
|
||||
<span class="target-label">Active URL</span>
|
||||
<span class="url-text">${escapeHtml(state.activeTabUrl || "Unknown active tab")}</span>
|
||||
<span class="target-label">Known URL</span>
|
||||
<span class="url-text">${escapeHtml(knownUrl.url)}</span>
|
||||
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
|
||||
<span class="muted">Chrome profile: ${escapeHtml(CHROME_PROFILE_NAME)}</span>
|
||||
</div>
|
||||
|
||||
@@ -696,6 +723,56 @@ function openUrl(url: string): void {
|
||||
}
|
||||
|
||||
function bindViewActions(): void {
|
||||
viewContent?.querySelector<HTMLInputElement>("#agent-name")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setAgentDraft({
|
||||
name: (event.target as HTMLInputElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#agent-file")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setAgentDraft({
|
||||
file: (event.target as HTMLInputElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#agent-description")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setAgentDraft({
|
||||
description: (event.target as HTMLInputElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLTextAreaElement>("#agent-content")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setAgentDraft({
|
||||
content: (event.target as HTMLTextAreaElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#new-agent")?.addEventListener("click", () => {
|
||||
appStore.getState().startNewAgent();
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#save-agent")?.addEventListener("click", () => {
|
||||
void appStore.getState().saveAgent();
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-edit-agent]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const id = button.dataset.editAgent;
|
||||
if (id) {
|
||||
appStore.getState().editAgent(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-delete-agent]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const id = button.dataset.deleteAgent;
|
||||
if (id && window.confirm("Delete this agent?")) {
|
||||
void appStore.getState().deleteAgent(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-open-url]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const url = button.dataset.openUrl;
|
||||
@@ -864,16 +941,7 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLInputElement>("[data-crop-field]").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
const field = input.dataset.cropField;
|
||||
if (field === "left" || field === "top" || field === "width" || field === "height") {
|
||||
appStore.getState().setScreenshotPreviewCrop({
|
||||
[field]: Number(input.value)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
bindCropInteractions();
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("[data-close-screenshot-preview]")?.addEventListener("click", () => {
|
||||
appStore.getState().closeScreenshotPreview();
|
||||
@@ -896,6 +964,78 @@ function bindViewActions(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function bindCropInteractions(): void {
|
||||
const frame = viewContent?.querySelector<HTMLElement>(".crop-frame");
|
||||
const cropBox = viewContent?.querySelector<HTMLButtonElement>("[data-crop-drag]");
|
||||
|
||||
if (!frame || !cropBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropBox.addEventListener("pointerdown", (event) => {
|
||||
const state = appStore.getState();
|
||||
const preview = state.screenshotPreview;
|
||||
|
||||
if (!preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
cropBox.setPointerCapture(event.pointerId);
|
||||
|
||||
const handle = (event.target as HTMLElement).dataset.cropHandle as CropHandle | undefined;
|
||||
const frameRect = frame.getBoundingClientRect();
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startCrop = { ...preview.crop };
|
||||
|
||||
const updateCrop = (moveEvent: PointerEvent): void => {
|
||||
const deltaX = ((moveEvent.clientX - startX) / frameRect.width) * 100;
|
||||
const deltaY = ((moveEvent.clientY - startY) / frameRect.height) * 100;
|
||||
|
||||
if (!handle) {
|
||||
appStore.getState().setScreenshotPreviewCrop({
|
||||
left: startCrop.left + deltaX,
|
||||
top: startCrop.top + deltaY
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCrop = { ...startCrop };
|
||||
|
||||
if (handle.includes("e")) {
|
||||
nextCrop.width = startCrop.width + deltaX;
|
||||
}
|
||||
|
||||
if (handle.includes("s")) {
|
||||
nextCrop.height = startCrop.height + deltaY;
|
||||
}
|
||||
|
||||
if (handle.includes("w")) {
|
||||
nextCrop.left = startCrop.left + deltaX;
|
||||
nextCrop.width = startCrop.width - deltaX;
|
||||
}
|
||||
|
||||
if (handle.includes("n")) {
|
||||
nextCrop.top = startCrop.top + deltaY;
|
||||
nextCrop.height = startCrop.height - deltaY;
|
||||
}
|
||||
|
||||
appStore.getState().setScreenshotPreviewCrop(nextCrop);
|
||||
};
|
||||
|
||||
const stopCrop = (): void => {
|
||||
window.removeEventListener("pointermove", updateCrop);
|
||||
window.removeEventListener("pointerup", stopCrop);
|
||||
window.removeEventListener("pointercancel", stopCrop);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", updateCrop);
|
||||
window.addEventListener("pointerup", stopCrop);
|
||||
window.addEventListener("pointercancel", stopCrop);
|
||||
});
|
||||
}
|
||||
|
||||
function renderView(): void {
|
||||
if (!viewHeader || !viewContent) {
|
||||
return;
|
||||
|
||||
+190
-6
@@ -32,17 +32,53 @@ export interface AgentProfile {
|
||||
name: string;
|
||||
file: string;
|
||||
description: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const DEFAULT_GLOBAL_AGENT_CONTENT = `# GLOBAL AGENT
|
||||
|
||||
## Mission
|
||||
|
||||
Use this file as the governing handoff contract for the issue it is attached to. It is intentionally project-agnostic and should be interpreted in the context supplied by the issue, linked workspace, linked Git repository, and source URL.
|
||||
|
||||
## Context Variables
|
||||
|
||||
- Git target: {{git}}
|
||||
- Workspace: {{workspace}}
|
||||
- Source URL: {{url}}
|
||||
- Base URL: {{baseUrl}}
|
||||
- Chrome profile: {{profile}}
|
||||
- Issue: {{issue}}
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Treat the issue subject, issue body, screenshots, and attachments as the source of truth for the requested change.
|
||||
- Inspect {{workspace}} before proposing or applying code changes.
|
||||
- Use {{git}} as the repository identity for branch, commit, issue, and review references.
|
||||
- Preserve user intent. If evidence conflicts, state the conflict clearly and ask only when the next action would otherwise be risky.
|
||||
- Keep changes scoped to the issue. Avoid unrelated refactors, formatting churn, and generated output unless the issue requires them.
|
||||
- Run the narrowest meaningful verification for the changed surface, then report exactly what passed or could not be run.
|
||||
- Do not include credentials, local-only secrets, or private tokens in comments, commits, logs, or attachments.
|
||||
- When handing off to another agent, include current status, touched files, verification results, and any unresolved decisions.`;
|
||||
|
||||
export const AVAILABLE_AGENTS: AgentProfile[] = [
|
||||
{
|
||||
id: "global-agent",
|
||||
name: "GLOBAL AGENT",
|
||||
file: "AGENTS.md",
|
||||
description: "Default project-wide agent rules for issue handoff."
|
||||
description: "Default project-wide agent rules for issue handoff.",
|
||||
content: DEFAULT_GLOBAL_AGENT_CONTENT
|
||||
}
|
||||
];
|
||||
|
||||
const EMPTY_AGENT_DRAFT: AgentProfile = {
|
||||
id: "",
|
||||
name: "",
|
||||
file: "AGENTS.md",
|
||||
description: "",
|
||||
content: ""
|
||||
};
|
||||
|
||||
interface AddUrlDraft {
|
||||
mode: AddUrlMode | null;
|
||||
selectedValue: string;
|
||||
@@ -97,6 +133,8 @@ interface AppState {
|
||||
issueDraft: IssueDraft;
|
||||
issueCommentDraft: IssueCommentDraft;
|
||||
agents: AgentProfile[];
|
||||
agentDraft: AgentProfile;
|
||||
editingAgentId: string | null;
|
||||
screenshotPreview: ScreenshotPreview | null;
|
||||
targetIssues: GiteaIssueSummary[];
|
||||
issueListRepoFullName: string;
|
||||
@@ -125,6 +163,11 @@ interface AppState {
|
||||
setIssueCommentFiles: (files: File[]) => void;
|
||||
deleteIssueCommentFile: (index: number) => void;
|
||||
deleteIssueCommentScreenshot: (id: string) => void;
|
||||
setAgentDraft: (draft: Partial<AgentProfile>) => void;
|
||||
startNewAgent: () => void;
|
||||
editAgent: (id: string) => void;
|
||||
saveAgent: () => Promise<void>;
|
||||
deleteAgent: (id: string) => Promise<void>;
|
||||
openScreenshotPreview: (target: ScreenshotTarget, id: string) => void;
|
||||
closeScreenshotPreview: () => void;
|
||||
setScreenshotPreviewCrop: (crop: Partial<CropArea>) => void;
|
||||
@@ -148,6 +191,7 @@ interface AppState {
|
||||
|
||||
const USER_URLS_STORAGE_KEY = "silmaAide.userUrls";
|
||||
const DELETED_URL_BASES_STORAGE_KEY = "silmaAide.deletedUrlBases";
|
||||
const AGENTS_STORAGE_KEY = "silmaAide.agents";
|
||||
let statusTimer: number | undefined;
|
||||
|
||||
function getBaseUrl(value: string): string {
|
||||
@@ -401,8 +445,23 @@ function createScreenshotAttachment(dataUrl: string, blob: Blob): ScreenshotAtta
|
||||
};
|
||||
}
|
||||
|
||||
function getAgentById(id: string): AgentProfile {
|
||||
return AVAILABLE_AGENTS.find((agent) => agent.id === id) ?? AVAILABLE_AGENTS[0];
|
||||
function normalizeAgent(agent: Partial<AgentProfile>, index: number): AgentProfile {
|
||||
return {
|
||||
id: agent.id || crypto.randomUUID(),
|
||||
name: agent.name?.trim() || (index === 0 ? "GLOBAL AGENT" : "Unnamed agent"),
|
||||
file: agent.file?.trim() || "AGENTS.md",
|
||||
description: agent.description?.trim() || "",
|
||||
content: agent.content?.trim() || DEFAULT_GLOBAL_AGENT_CONTENT
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAgents(agents: AgentProfile[]): AgentProfile[] {
|
||||
const normalized = agents.map(normalizeAgent);
|
||||
return normalized.length ? normalized : AVAILABLE_AGENTS;
|
||||
}
|
||||
|
||||
function getAgentById(id: string, agents: AgentProfile[]): AgentProfile {
|
||||
return agents.find((agent) => agent.id === id) ?? agents[0] ?? AVAILABLE_AGENTS[0];
|
||||
}
|
||||
|
||||
function appendFiles(existingFiles: File[], incomingFiles: File[]): File[] {
|
||||
@@ -478,7 +537,7 @@ async function cropScreenshot(screenshot: ScreenshotAttachment, crop: CropArea):
|
||||
}
|
||||
|
||||
function buildIssueBody(state: AppState, knownUrl: KnownUrl): string {
|
||||
const agent = getAgentById(state.issueDraft.agentId);
|
||||
const agent = getAgentById(state.issueDraft.agentId, state.agents);
|
||||
const parts = [
|
||||
state.issueDraft.content.trim(),
|
||||
"---",
|
||||
@@ -493,6 +552,24 @@ function buildIssueBody(state: AppState, knownUrl: KnownUrl): string {
|
||||
return parts.filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
function renderAgentContent(agent: AgentProfile, state: AppState, knownUrl: KnownUrl, issueNumber: number): string {
|
||||
const replacements: Record<string, string> = {
|
||||
git: knownUrl.giteaRepo?.fullName ?? "",
|
||||
repo: knownUrl.giteaRepo?.fullName ?? "",
|
||||
workspace: knownUrl.workspacePath,
|
||||
url: state.activeTabUrl || knownUrl.url,
|
||||
baseUrl: state.activeBaseUrl || getBaseUrl(knownUrl.url),
|
||||
profile: CHROME_PROFILE_NAME,
|
||||
issue: `#${issueNumber}`
|
||||
};
|
||||
|
||||
return agent.content.replace(/\{\{(git|repo|workspace|url|baseUrl|profile|issue)\}\}/g, (_match, key: string) => replacements[key] ?? "");
|
||||
}
|
||||
|
||||
function agentContentBlob(content: string): Blob {
|
||||
return new Blob([content], { type: "text/markdown;charset=utf-8" });
|
||||
}
|
||||
|
||||
function buildAttachmentMarkdown(attachments: GiteaIssueAttachment[]): string {
|
||||
const lines = attachments
|
||||
.filter((attachment) => attachment.downloadUrl)
|
||||
@@ -542,6 +619,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
files: []
|
||||
},
|
||||
agents: AVAILABLE_AGENTS,
|
||||
agentDraft: AVAILABLE_AGENTS[0],
|
||||
editingAgentId: AVAILABLE_AGENTS[0].id,
|
||||
screenshotPreview: null,
|
||||
targetIssues: [],
|
||||
issueListRepoFullName: "",
|
||||
@@ -630,6 +709,94 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
screenshots: state.issueCommentDraft.screenshots.filter((screenshot) => screenshot.id !== id)
|
||||
}
|
||||
})),
|
||||
setAgentDraft: (draft) =>
|
||||
set((state) => ({
|
||||
agentDraft: {
|
||||
...state.agentDraft,
|
||||
...draft
|
||||
}
|
||||
})),
|
||||
startNewAgent: () =>
|
||||
set({
|
||||
agentDraft: {
|
||||
...EMPTY_AGENT_DRAFT,
|
||||
id: crypto.randomUUID()
|
||||
},
|
||||
editingAgentId: null
|
||||
}),
|
||||
editAgent: (id) =>
|
||||
set((state) => {
|
||||
const agent = state.agents.find((item) => item.id === id);
|
||||
return agent
|
||||
? {
|
||||
agentDraft: { ...agent },
|
||||
editingAgentId: id
|
||||
}
|
||||
: {};
|
||||
}),
|
||||
saveAgent: async () => {
|
||||
const state = appStore.getState();
|
||||
const draft = normalizeAgent(
|
||||
{
|
||||
...state.agentDraft,
|
||||
id: state.editingAgentId ?? state.agentDraft.id ?? crypto.randomUUID()
|
||||
},
|
||||
state.agents.length
|
||||
);
|
||||
|
||||
if (!draft.name.trim() || !draft.file.trim() || !draft.content.trim()) {
|
||||
setStatus(set, "Agent name, file, and content are required.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = state.editingAgentId
|
||||
? state.agents.map((agent) => (agent.id === state.editingAgentId ? draft : agent))
|
||||
: [...state.agents, draft];
|
||||
const normalizedAgents = normalizeAgents(agents);
|
||||
|
||||
await storageSet(AGENTS_STORAGE_KEY, normalizedAgents);
|
||||
set((current) => ({
|
||||
agents: normalizedAgents,
|
||||
issueDraft: {
|
||||
...current.issueDraft,
|
||||
agentId: normalizedAgents.some((agent) => agent.id === current.issueDraft.agentId)
|
||||
? current.issueDraft.agentId
|
||||
: normalizedAgents[0].id
|
||||
},
|
||||
agentDraft: {
|
||||
...draft
|
||||
},
|
||||
editingAgentId: draft.id
|
||||
}));
|
||||
setStatus(set, `Saved ${draft.name}.`, "success");
|
||||
},
|
||||
deleteAgent: async (id) => {
|
||||
const state = appStore.getState();
|
||||
const agent = state.agents.find((item) => item.id === id);
|
||||
|
||||
if (!agent) {
|
||||
setStatus(set, "Could not find that agent.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.agents.length <= 1) {
|
||||
setStatus(set, "At least one agent must remain available.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = state.agents.filter((item) => item.id !== id);
|
||||
await storageSet(AGENTS_STORAGE_KEY, agents);
|
||||
set((current) => ({
|
||||
agents,
|
||||
issueDraft: {
|
||||
...current.issueDraft,
|
||||
agentId: current.issueDraft.agentId === id ? agents[0].id : current.issueDraft.agentId
|
||||
},
|
||||
agentDraft: current.editingAgentId === id ? EMPTY_AGENT_DRAFT : current.agentDraft,
|
||||
editingAgentId: current.editingAgentId === id ? null : current.editingAgentId
|
||||
}));
|
||||
setStatus(set, `Deleted ${agent.name}.`, "success");
|
||||
},
|
||||
openScreenshotPreview: (target, id) =>
|
||||
set({
|
||||
screenshotPreview: {
|
||||
@@ -1008,7 +1175,12 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
buildIssueBody(state, target.knownUrl),
|
||||
[changeRequestLabel.id]
|
||||
);
|
||||
const agent = getAgentById(state.issueDraft.agentId, state.agents);
|
||||
const uploads = [
|
||||
{
|
||||
name: agent.file,
|
||||
blob: agentContentBlob(renderAgentContent(agent, state, target.knownUrl, issue.number))
|
||||
},
|
||||
...state.issueDraft.screenshots.map((screenshot) => ({
|
||||
name: screenshot.name,
|
||||
blob: screenshot.blob
|
||||
@@ -1032,7 +1204,7 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
issueDraft: {
|
||||
subject: "",
|
||||
content: "",
|
||||
agentId: AVAILABLE_AGENTS[0].id,
|
||||
agentId: appStore.getState().agents[0]?.id ?? AVAILABLE_AGENTS[0].id,
|
||||
ready: false,
|
||||
screenshots: [],
|
||||
files: []
|
||||
@@ -1090,20 +1262,32 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
setStatus(set, "Loading workspace and Gitea context...", "neutral", { autoClear: false });
|
||||
|
||||
try {
|
||||
const [localContext, repos, userUrls, deletedUrlBases, activeTabUrl] = await Promise.all([
|
||||
const [localContext, repos, userUrls, deletedUrlBases, agents, activeTabUrl] = await Promise.all([
|
||||
loadLocalContext(),
|
||||
getReposOrderedByLastCommit(),
|
||||
storageGet<KnownUrl[]>(USER_URLS_STORAGE_KEY, []),
|
||||
storageGet<string[]>(DELETED_URL_BASES_STORAGE_KEY, []),
|
||||
storageGet<AgentProfile[]>(AGENTS_STORAGE_KEY, AVAILABLE_AGENTS),
|
||||
getActiveTabUrl()
|
||||
]);
|
||||
const activeBaseUrl = getBaseUrl(activeTabUrl);
|
||||
const normalizedAgents = normalizeAgents(agents);
|
||||
|
||||
set({
|
||||
localContext,
|
||||
repos,
|
||||
userUrls: userUrls.filter((url) => !deletedUrlBases.includes(getBaseUrl(url.url))),
|
||||
deletedUrlBases,
|
||||
agents: normalizedAgents,
|
||||
issueDraft: {
|
||||
...get().issueDraft,
|
||||
agentId: normalizedAgents.some((agent) => agent.id === get().issueDraft.agentId)
|
||||
? get().issueDraft.agentId
|
||||
: normalizedAgents[0].id
|
||||
},
|
||||
agentDraft: normalizedAgents.find((agent) => agent.id === get().editingAgentId) ?? normalizedAgents[0],
|
||||
editingAgentId:
|
||||
normalizedAgents.find((agent) => agent.id === get().editingAgentId)?.id ?? normalizedAgents[0].id,
|
||||
activeTabUrl,
|
||||
activeBaseUrl
|
||||
});
|
||||
|
||||
+79
-22
@@ -197,6 +197,10 @@ h1 {
|
||||
grid-template-columns: minmax(150px, 1fr) auto;
|
||||
}
|
||||
|
||||
.agent-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.workspace-row:first-child,
|
||||
.url-row:first-child,
|
||||
.agent-row:first-child {
|
||||
@@ -345,6 +349,17 @@ a.item-title:hover,
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.agent-actions,
|
||||
.agent-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.agent-editor-actions .primary-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background: #fee4e2;
|
||||
color: #9f1d14;
|
||||
@@ -617,7 +632,7 @@ input[readonly] {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.target-summary > span:first-child,
|
||||
.target-label,
|
||||
.attachment-heading > span {
|
||||
color: #52616f;
|
||||
font-size: 10px;
|
||||
@@ -777,35 +792,76 @@ input[readonly] {
|
||||
|
||||
.crop-box {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border: 2px solid #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
pointer-events: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: move;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.crop-controls {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
.crop-handle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 2px;
|
||||
background: #f59e0b;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.24);
|
||||
}
|
||||
|
||||
.crop-controls label {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #52616f;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
text-transform: uppercase;
|
||||
.crop-handle-nw {
|
||||
top: -7px;
|
||||
left: -7px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.crop-controls input {
|
||||
width: 100%;
|
||||
.crop-handle-n {
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.crop-controls output {
|
||||
color: #334e68;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
.crop-handle-ne {
|
||||
top: -7px;
|
||||
right: -7px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.crop-handle-e {
|
||||
top: 50%;
|
||||
right: -7px;
|
||||
transform: translateY(-50%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.crop-handle-se {
|
||||
right: -7px;
|
||||
bottom: -7px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.crop-handle-s {
|
||||
bottom: -7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.crop-handle-sw {
|
||||
bottom: -7px;
|
||||
left: -7px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.crop-handle-w {
|
||||
top: 50%;
|
||||
left: -7px;
|
||||
transform: translateY(-50%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.form-stack .checkbox-row {
|
||||
@@ -885,7 +941,8 @@ input[readonly] {
|
||||
}
|
||||
|
||||
.workspace-row,
|
||||
.url-row {
|
||||
.url-row,
|
||||
.agent-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user