ework-web 0.10.69 → 0.10.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db.ts +13 -0
- package/src/giteaApi.ts +1 -0
- package/src/index.ts +43 -2
- package/src/render/layout.ts +11 -1
- package/src/schema-mysql.sql +1 -0
- package/src/schema.sql +2 -0
- package/src/static/issue-actions.js +26 -0
- package/src/store.ts +12 -3
- package/src/views/issueNew.ts +10 -1
- package/src/views/issueThread.ts +4 -0
- package/src/webhooks.ts +2 -2
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -131,6 +131,9 @@ function migrateIssuesTable(db: Database): void {
|
|
|
131
131
|
if (!have.has("ai_status")) {
|
|
132
132
|
db.exec(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN ai_status TEXT NOT NULL DEFAULT ''"));
|
|
133
133
|
}
|
|
134
|
+
if (!have.has("model")) {
|
|
135
|
+
db.exec(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN model TEXT NOT NULL DEFAULT ''"));
|
|
136
|
+
}
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
function migrateLabelsTable(db: Database): void {
|
|
@@ -367,6 +370,16 @@ async function migrateMysqlIssuesAiStatus(pool: Pool): Promise<void> {
|
|
|
367
370
|
} catch (e) {
|
|
368
371
|
console.warn("[db] MySQL issues ai_status column add failed:", (e as Error).message);
|
|
369
372
|
}
|
|
373
|
+
try {
|
|
374
|
+
const [mcols] = await pool.query(
|
|
375
|
+
applyPrefix("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{{issues}}' AND COLUMN_NAME = 'model'")
|
|
376
|
+
);
|
|
377
|
+
if (!(Array.isArray(mcols) && mcols.length > 0)) {
|
|
378
|
+
await pool.query(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN model VARCHAR(128) NOT NULL DEFAULT ''"));
|
|
379
|
+
}
|
|
380
|
+
} catch (e) {
|
|
381
|
+
console.warn("[db] MySQL issues model column add failed:", (e as Error).message);
|
|
382
|
+
}
|
|
370
383
|
}
|
|
371
384
|
|
|
372
385
|
class MysqlDriver implements AsyncDatabase {
|
package/src/giteaApi.ts
CHANGED
|
@@ -197,6 +197,7 @@ export async function handleGiteaApi(
|
|
|
197
197
|
updatedAt: asString(body.updated_at),
|
|
198
198
|
state: asIssueState(body.state) ?? "open",
|
|
199
199
|
closedAt: asString(body.closed_at) || undefined,
|
|
200
|
+
model: asString(body.model) || undefined,
|
|
200
201
|
});
|
|
201
202
|
void emitIssueEvent(project.id, created.id, "opened", origin);
|
|
202
203
|
if (created.state === "closed") {
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
postComment,
|
|
39
39
|
setIssueState,
|
|
40
40
|
updateIssueAiStatus,
|
|
41
|
+
updateIssueModel,
|
|
41
42
|
listIssues,
|
|
42
43
|
createAttachment,
|
|
43
44
|
getAttachment,
|
|
@@ -385,6 +386,7 @@ const REPO_LABELS_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels$/;
|
|
|
385
386
|
const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
|
|
386
387
|
const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
|
|
387
388
|
const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on)$/;
|
|
389
|
+
const REPO_ISSUE_MODEL_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/model$/;
|
|
388
390
|
const API_ISSUE_LABELS_RE = /^\/api\/([^/]+)\/([^/]+)\/issues\/(\d+)\/labels$/;
|
|
389
391
|
const WH_ACTION_RE = /^\/__wh\/(\d+)\/(delete|toggle|test)$/;
|
|
390
392
|
const SESSIONS_RE = /^\/sessions$/;
|
|
@@ -1339,6 +1341,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1339
1341
|
}
|
|
1340
1342
|
}
|
|
1341
1343
|
|
|
1344
|
+
if (url.pathname === "/api/v1/dispatch-state" && req.method === "GET") {
|
|
1345
|
+
const owner = url.searchParams.get("owner") ?? "";
|
|
1346
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
1347
|
+
const number = url.searchParams.get("number") ?? "";
|
|
1348
|
+
if (!owner || !repo || !number) return json({ error: "owner, repo, number required" }, 400);
|
|
1349
|
+
const project = await getProject(owner, repo);
|
|
1350
|
+
if (!project) return json({ dispatchOff: false, aiStatus: "" });
|
|
1351
|
+
const issue = await getIssueWithMeta(project.id, Number(number));
|
|
1352
|
+
const cfgKv = await getConfigAll();
|
|
1353
|
+
const globalOff = cfgKv["dispatchEnabled"] === "false";
|
|
1354
|
+
const projectOff = cfgKv[`dispatchOff:${owner}/${repo}`] === "1";
|
|
1355
|
+
const aiStatus = issue?.ai_status ?? "";
|
|
1356
|
+
const issueOff = aiStatus === "dispatch_off" || aiStatus === "halted";
|
|
1357
|
+
return json({ dispatchOff: globalOff || projectOff || issueOff, aiStatus });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1342
1360
|
if (url.pathname === "/api/router/strategy") {
|
|
1343
1361
|
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
1344
1362
|
const routerUrl = cfg.daemonWebhookUrl.replace(/\/$/, "");
|
|
@@ -1816,6 +1834,28 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1816
1834
|
}
|
|
1817
1835
|
}
|
|
1818
1836
|
|
|
1837
|
+
const mp = url.pathname.match(REPO_ISSUE_MODEL_RE);
|
|
1838
|
+
if (mp) {
|
|
1839
|
+
const [, owner, repo, numStr] = mp;
|
|
1840
|
+
if (!(owner && repo && numStr)) return json({ error: "bad path" }, 400);
|
|
1841
|
+
const number = Number(numStr);
|
|
1842
|
+
try {
|
|
1843
|
+
const project = await getProject(owner, repo);
|
|
1844
|
+
if (!project) return json({ error: "project not found" }, 404);
|
|
1845
|
+
const issue = await getIssueWithMeta(project.id, number);
|
|
1846
|
+
if (!issue) return json({ error: "issue not found" }, 404);
|
|
1847
|
+
if (!(await canWriteProject(project.id, ctx.user))) {
|
|
1848
|
+
return json({ error: "writer role required" }, 403);
|
|
1849
|
+
}
|
|
1850
|
+
const form = await req.formData().catch(() => new FormData());
|
|
1851
|
+
const model = String(form.get("model") ?? "").trim().slice(0, 128);
|
|
1852
|
+
await updateIssueModel(issue.id, model);
|
|
1853
|
+
return json({ ok: true, model });
|
|
1854
|
+
} catch (e) {
|
|
1855
|
+
return json({ error: errMsg(e) }, e instanceof StoreError ? e.status : 500);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1819
1859
|
const ci = url.pathname.match(REPO_LIST_RE);
|
|
1820
1860
|
if (ci) {
|
|
1821
1861
|
const [, owner, repo] = ci;
|
|
@@ -1824,6 +1864,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1824
1864
|
const form = await req.formData().catch(() => new FormData());
|
|
1825
1865
|
const title = String(form.get("title") ?? "").trim();
|
|
1826
1866
|
const body = String(form.get("body") ?? "");
|
|
1867
|
+
const model = String(form.get("model") ?? "").trim().slice(0, 128);
|
|
1827
1868
|
if (!title) return html(errorPage("标题不能为空", "回到上一页填写标题后重试"), 400);
|
|
1828
1869
|
let project = await getProject(owner, repo);
|
|
1829
1870
|
let createdProject = false;
|
|
@@ -1838,7 +1879,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1838
1879
|
await ensureProjectBootstrapAdmin(project.id, ctx.user!.login);
|
|
1839
1880
|
await autoWireDaemon(project.id, url.origin);
|
|
1840
1881
|
}
|
|
1841
|
-
const issue = await createIssue(project.id, title, body, ctx.user!.login);
|
|
1882
|
+
const issue = await createIssue(project.id, title, body, ctx.user!.login, model ? { model } : {});
|
|
1842
1883
|
void emitIssueEvent(project.id, issue.id, "opened", url.origin);
|
|
1843
1884
|
return Response.redirect(
|
|
1844
1885
|
`${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issue.number}`,
|
|
@@ -2177,7 +2218,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2177
2218
|
if (isNew) {
|
|
2178
2219
|
const [, owner, repo] = isNew;
|
|
2179
2220
|
if (!(owner && repo)) return html(errorPage("404", "bad path"), 404);
|
|
2180
|
-
return html(buildIssueNew(owner, repo, cfg.writesEnabled));
|
|
2221
|
+
return html(buildIssueNew(owner, repo, cfg.writesEnabled, await listCachedModels()));
|
|
2181
2222
|
}
|
|
2182
2223
|
|
|
2183
2224
|
const view = url.pathname.match(REPO_ISSUE_RE);
|
package/src/render/layout.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface LayoutProps {
|
|
|
23
23
|
viewerIsAdmin?: boolean;
|
|
24
24
|
customActions?: IssueAction[];
|
|
25
25
|
extraStatusBadges?: Record<string, { cls: string; label: string }>;
|
|
26
|
+
modelSelect?: { current: string; options: { id: string; label: string }[] } | null;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const THEME_CSS = `
|
|
@@ -127,6 +128,9 @@ header.topbar .num{opacity:.7}
|
|
|
127
128
|
.issue-label{display:inline-flex;align-items:center;font-size:12px;font-weight:500;padding:.05rem .5rem;border:1px solid;border-radius:99px;line-height:1.6;background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
128
129
|
.label-edit-btn{background:none;border:1px solid var(--border);border-radius:6px;padding:.05rem .35rem;cursor:pointer;font-size:13px;line-height:1.5;color:var(--text-muted)}
|
|
129
130
|
.label-edit-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
131
|
+
.model-select-wrap{display:inline-flex;align-items:center;gap:.25rem}
|
|
132
|
+
.model-select{background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:.15rem .3rem;font:12px system-ui,sans-serif;max-width:180px}
|
|
133
|
+
.model-save-btn{padding:.15rem .45rem;font-size:13px}
|
|
130
134
|
#labelDlg{border:1px solid var(--border);border-radius:10px;background:var(--bg-elev);color:var(--text);padding:1.1rem;max-width:380px;width:90vw}
|
|
131
135
|
#labelDlg::backdrop{background:rgba(0,0,0,.5)}
|
|
132
136
|
#labelDlg h3{margin:0 0 .6rem;font-size:14px}
|
|
@@ -199,6 +203,12 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
199
203
|
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 接单</button>`
|
|
200
204
|
: `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="设为不自动接单">🔕 不接单</button>`
|
|
201
205
|
: "";
|
|
206
|
+
const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions
|
|
207
|
+
? `<span class="model-select-wrap"><select class="model-select" id="issueModelSelect" title="此 issue 的模型(覆盖项目/全局默认)">
|
|
208
|
+
<option value="">默认模型</option>
|
|
209
|
+
${props.modelSelect.options.map((m) => `<option value="${escapeAttr(m.id)}" ${m.id === props.modelSelect!.current ? "selected" : ""}>${escapeHtml(m.label)}</option>`).join("")}
|
|
210
|
+
</select><button type="button" class="action-btn model-save-btn" id="issueModelSave" title="保存模型选择">💾</button></span>`
|
|
211
|
+
: "";
|
|
202
212
|
return `<!doctype html>
|
|
203
213
|
<html lang="zh">
|
|
204
214
|
<head>
|
|
@@ -221,7 +231,7 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
221
231
|
<div class="meta-status">
|
|
222
232
|
<span class="state-badge ${stateClass}">${stateLabel}</span>
|
|
223
233
|
${aiBadgeHtml}
|
|
224
|
-
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${(props.customActions ?? []).map((a) => {
|
|
234
|
+
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => {
|
|
225
235
|
const attrs = [`data-action-href="${escapeAttr(a.href)}"`];
|
|
226
236
|
if (a.method && a.method !== "POST") attrs.push(`data-action-method="${escapeAttr(a.method)}"`);
|
|
227
237
|
if (a.confirm) attrs.push(`data-action-confirm="${escapeAttr(a.confirm)}"`);
|
package/src/schema-mysql.sql
CHANGED
|
@@ -54,6 +54,7 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
54
54
|
updated_at VARCHAR(40) NOT NULL,
|
|
55
55
|
closed_at VARCHAR(40) DEFAULT NULL,
|
|
56
56
|
ai_status VARCHAR(32) NOT NULL DEFAULT '',
|
|
57
|
+
model VARCHAR(128) NOT NULL DEFAULT '',
|
|
57
58
|
UNIQUE (project_id, number),
|
|
58
59
|
CONSTRAINT {{fk_issues_project}} FOREIGN KEY (project_id) REFERENCES {{projects}}(id) ON DELETE CASCADE,
|
|
59
60
|
CONSTRAINT {{fk_issues_author}} FOREIGN KEY (author) REFERENCES {{users}}(login)
|
package/src/schema.sql
CHANGED
|
@@ -65,6 +65,8 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
65
65
|
closed_at TEXT,
|
|
66
66
|
-- AI processing status: '' (none) | 'processing' | 'halted' | 'completed' | 'failed'
|
|
67
67
|
ai_status TEXT NOT NULL DEFAULT '',
|
|
68
|
+
-- Resolved "provider/model" for this issue. Empty = inherit project/global default.
|
|
69
|
+
model TEXT NOT NULL DEFAULT '',
|
|
68
70
|
UNIQUE (project_id, number)
|
|
69
71
|
);
|
|
70
72
|
CREATE INDEX IF NOT EXISTS issues_project_state_updated
|
|
@@ -35,4 +35,30 @@
|
|
|
35
35
|
btn.textContent = orig;
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
document.addEventListener("click", function (e) {
|
|
40
|
+
var saveBtn = e.target.closest("#issueModelSave");
|
|
41
|
+
if (!saveBtn) return;
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
var sel = document.getElementById("issueModelSelect");
|
|
44
|
+
if (!sel) return;
|
|
45
|
+
var m = sel.value;
|
|
46
|
+
saveBtn.disabled = true;
|
|
47
|
+
var path = location.pathname.split("/").slice(0, 5).join("/");
|
|
48
|
+
fetch(path + "/model", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
51
|
+
body: "model=" + encodeURIComponent(m)
|
|
52
|
+
})
|
|
53
|
+
.then(function (r) { return r.json(); })
|
|
54
|
+
.then(function (d) {
|
|
55
|
+
saveBtn.disabled = false;
|
|
56
|
+
if (d.ok) { saveBtn.textContent = "✓"; setTimeout(function () { saveBtn.textContent = "💾"; }, 1200); }
|
|
57
|
+
else alert(d.error || "保存失败");
|
|
58
|
+
})
|
|
59
|
+
.catch(function (err) {
|
|
60
|
+
saveBtn.disabled = false;
|
|
61
|
+
alert("网络错误: " + err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
38
64
|
})();
|
package/src/store.ts
CHANGED
|
@@ -74,6 +74,7 @@ export interface IssueRow {
|
|
|
74
74
|
updated_at: string;
|
|
75
75
|
closed_at: string | null;
|
|
76
76
|
ai_status: string;
|
|
77
|
+
model: string;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export interface IssueWithMeta extends IssueRow {
|
|
@@ -316,7 +317,9 @@ export async function setProjectModel(projectId: number, model: string): Promise
|
|
|
316
317
|
// Resolve the effective model for a project: project override > global
|
|
317
318
|
// default. Returns "" when neither is set (caller omits --model and lets
|
|
318
319
|
// opencode pick per its own config).
|
|
319
|
-
export function resolveModel(projectModel: string, globalDefault: string): string {
|
|
320
|
+
export function resolveModel(projectModel: string, globalDefault: string, issueModel?: string): string {
|
|
321
|
+
const i = String(issueModel ?? "").trim();
|
|
322
|
+
if (i) return i;
|
|
320
323
|
const p = String(projectModel ?? "").trim();
|
|
321
324
|
if (p) return p;
|
|
322
325
|
return String(globalDefault ?? "").trim();
|
|
@@ -448,6 +451,7 @@ export interface CreateIssueOpts {
|
|
|
448
451
|
updatedAt?: string;
|
|
449
452
|
state?: "open" | "closed";
|
|
450
453
|
closedAt?: string | null;
|
|
454
|
+
model?: string;
|
|
451
455
|
}
|
|
452
456
|
|
|
453
457
|
function isoOr(value: string | undefined, fallback: string): string {
|
|
@@ -478,8 +482,8 @@ export async function createIssue(
|
|
|
478
482
|
"SELECT COALESCE(MAX(number), 0) + 1 AS n FROM {{issues}} WHERE project_id = ?", [projectId]
|
|
479
483
|
))!;
|
|
480
484
|
const info = await getDB().run(
|
|
481
|
-
"INSERT INTO {{issues}} (project_id, number, title, body, state, author, created_at, updated_at, closed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
482
|
-
[projectId, next.n, title, body, state, author, createdAt, updatedAt, closedAt]
|
|
485
|
+
"INSERT INTO {{issues}} (project_id, number, title, body, state, author, created_at, updated_at, closed_at, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
486
|
+
[projectId, next.n, title, body, state, author, createdAt, updatedAt, closedAt, opts.model ?? ""]
|
|
483
487
|
);
|
|
484
488
|
await getDB().run("UPDATE {{projects}} SET updated_at = ? WHERE id = ?", [updatedAt, projectId]);
|
|
485
489
|
return (await getIssueById(info.insertId))!;
|
|
@@ -514,6 +518,11 @@ export async function getIssueAiStatus(issueId: number): Promise<string> {
|
|
|
514
518
|
return row?.ai_status ?? "";
|
|
515
519
|
}
|
|
516
520
|
|
|
521
|
+
export async function updateIssueModel(issueId: number, model: string): Promise<void> {
|
|
522
|
+
const clean = model.trim().slice(0, 128);
|
|
523
|
+
await getDB().run("UPDATE {{issues}} SET model = ?, updated_at = ? WHERE id = ?", [clean, now(), issueId]);
|
|
524
|
+
}
|
|
525
|
+
|
|
517
526
|
export interface IssuePatch {
|
|
518
527
|
title?: string;
|
|
519
528
|
body?: string;
|
package/src/views/issueNew.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { THEME_CSS, escapeHtml, escapeAttr } from "../render/layout";
|
|
2
|
+
import type { CachedModel } from "../store";
|
|
2
3
|
|
|
3
|
-
export function buildIssueNew(owner: string, repo: string, writesEnabled: boolean): string {
|
|
4
|
+
export function buildIssueNew(owner: string, repo: string, writesEnabled: boolean, models: CachedModel[] = [], currentModel = ""): string {
|
|
4
5
|
const listHref = `/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
|
|
5
6
|
const action = `/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
|
|
7
|
+
const modelSelect = models.length > 0
|
|
8
|
+
? `<select name="model" class="new-model">
|
|
9
|
+
<option value="">默认模型(项目/全局设置)</option>
|
|
10
|
+
${models.map((m) => `<option value="${escapeAttr(m.id)}" ${m.id === currentModel ? "selected" : ""}>${escapeHtml(m.label)}</option>`).join("")}
|
|
11
|
+
</select>`
|
|
12
|
+
: "";
|
|
6
13
|
const body = writesEnabled
|
|
7
14
|
? `<form class="new-form" method="POST" action="${escapeAttr(action)}">
|
|
8
15
|
<input type="text" name="title" placeholder="标题(必填)" required maxlength="255" class="new-title">
|
|
9
16
|
<textarea name="body" rows="14" placeholder="正文(支持 Markdown)…"></textarea>
|
|
17
|
+
${modelSelect}
|
|
10
18
|
<div class="new-actions"><a class="new-cancel" href="${escapeAttr(listHref)}">取消</a><button type="submit">创建工单</button></div>
|
|
11
19
|
</form>`
|
|
12
20
|
: `<div class="composer-ro">只读模式:创建工单未启用(WORK_WRITES_ENABLED=false)</div>`;
|
|
@@ -20,6 +28,7 @@ export function buildIssueNew(owner: string, repo: string, writesEnabled: boolea
|
|
|
20
28
|
.new-form{display:flex;flex-direction:column;gap:.6rem}
|
|
21
29
|
.new-title{width:100%;background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.55rem .7rem;font:600 16px system-ui,sans-serif}
|
|
22
30
|
.new-form textarea{width:100%;resize:vertical;min-height:14em;background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.6rem .7rem;font:14px/1.55 -apple-system,"PingFang SC",sans-serif}
|
|
31
|
+
.new-model{width:100%;background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;font:13px system-ui,sans-serif}
|
|
23
32
|
.new-actions{display:flex;gap:.6rem;justify-content:flex-end;align-items:center}
|
|
24
33
|
.new-cancel{font-size:13px;color:var(--text-muted)}
|
|
25
34
|
.new-form button{background:var(--green);color:#fff;border:none;border-radius:8px;padding:.55rem 1.2rem;font:600 13px system-ui,sans-serif;cursor:pointer}
|
package/src/views/issueThread.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getDefaultUpstreamUrl,
|
|
15
15
|
listLabelsForIssue,
|
|
16
16
|
getUserByLogin,
|
|
17
|
+
listCachedModels,
|
|
17
18
|
type CommentRow,
|
|
18
19
|
type IssueWithMeta,
|
|
19
20
|
} from "../store";
|
|
@@ -156,6 +157,9 @@ export async function buildIssueThread(
|
|
|
156
157
|
viewerIsAdmin,
|
|
157
158
|
customActions,
|
|
158
159
|
extraStatusBadges,
|
|
160
|
+
modelSelect: cfg.writesEnabled !== false
|
|
161
|
+
? { current: issue.model ?? "", options: (await listCachedModels()).map((m) => ({ id: m.id, label: m.label })) }
|
|
162
|
+
: null,
|
|
159
163
|
},
|
|
160
164
|
safeJsonEmbed(payload),
|
|
161
165
|
displayViews.map((v) => renderCommentCard(v, cfg)).join("")
|
package/src/webhooks.ts
CHANGED
|
@@ -667,7 +667,7 @@ export async function emitIssueEvent(
|
|
|
667
667
|
// override (daemon omits --model, lets opencode pick). globalDefault
|
|
668
668
|
// comes from the config table via loadConfig() — cheap DB read.
|
|
669
669
|
const globalDefault = (await loadConfig()).defaultModel;
|
|
670
|
-
const model = resolveModel(project.model, globalDefault);
|
|
670
|
+
const model = resolveModel(project.model, globalDefault, issue.model);
|
|
671
671
|
const labels = await toPayloadLabels(issueId);
|
|
672
672
|
const payload = buildIssuePayload(issue, project, commentCount, action, origin, model, labels);
|
|
673
673
|
(payload as IssueEventPayload).event_id = randomUUID();
|
|
@@ -707,7 +707,7 @@ export async function emitCommentEvent(
|
|
|
707
707
|
const authorUser = await getUserByLogin(comment.author);
|
|
708
708
|
const commentCount = await countCommentsSafe(issueId);
|
|
709
709
|
const globalDefault = (await loadConfig()).defaultModel;
|
|
710
|
-
const model = resolveModel(project.model, globalDefault);
|
|
710
|
+
const model = resolveModel(project.model, globalDefault, issue.model);
|
|
711
711
|
const labels = await toPayloadLabels(issueId);
|
|
712
712
|
const payload = buildCommentPayload(issue, comment, project, commentCount, origin, model, labels);
|
|
713
713
|
(payload as CommentEventPayload).event_id = randomUUID();
|