mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-19 11:20:48 +08:00
feat(control-pane): add agent+human JIT assignment view to the work-items board
The kanban board tracked lanes (ready/running/blocked/done) but not WHO owns
each card, which is the missing piece for agent+human just-in-time team workflows.
- state.js: classifyAssignee() labels each work item agent | human | unassigned
(session-linked or agent-pattern owners = agent; named owners = human; ownerless
= unassigned), with an explicit metadata.assigneeKind override.
- summarizeWorkItems(): adds an assignment summary {agent,human,unassigned} over
OPEN cards plus a priority-sorted needsAssignment queue — the JIT pickup list.
- ui.js: cards show an [agent]/[human]/[unassigned] badge; the board header shows
agent/human split and 'N need owner'.
- Tests: assignment classification + JIT queue coverage in control-pane-state.
Full suite 2839/2839; lint green.
This commit is contained in:
parent
b3268fef80
commit
1efc399ab4
@ -26,11 +26,7 @@ function defaultStateDbPath(env = process.env) {
|
||||
|
||||
function defaultConfigPaths(cwd = process.cwd(), env = process.env) {
|
||||
const home = homeDir(env);
|
||||
const paths = [
|
||||
path.join(home, 'Library', 'Application Support', 'ecc2', 'config.toml'),
|
||||
path.join(home, '.config', 'ecc2', 'config.toml'),
|
||||
path.join(home, '.claude', 'ecc2.toml'),
|
||||
];
|
||||
const paths = [path.join(home, 'Library', 'Application Support', 'ecc2', 'config.toml'), path.join(home, '.config', 'ecc2', 'config.toml'), path.join(home, '.claude', 'ecc2.toml')];
|
||||
|
||||
let current = path.resolve(cwd);
|
||||
while (current && current !== path.dirname(current)) {
|
||||
@ -66,9 +62,7 @@ function normalizeObjectKeys(value) {
|
||||
if (Array.isArray(value)) return value.map(normalizeObjectKeys);
|
||||
if (!isPlainObject(value)) return value;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [toCamelCase(key), normalizeObjectKeys(item)])
|
||||
);
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [toCamelCase(key), normalizeObjectKeys(item)]));
|
||||
}
|
||||
|
||||
function normalizeMemoryConnectors(connectors = {}) {
|
||||
@ -80,23 +74,13 @@ function normalizeMemoryConnectors(connectors = {}) {
|
||||
}
|
||||
|
||||
function normalizeConfig(rawConfig = {}, options = {}) {
|
||||
const {
|
||||
memory_connectors: snakeMemoryConnectors,
|
||||
memoryConnectors,
|
||||
state_db_path: snakeStateDbPath,
|
||||
stateDbPath: camelStateDbPath,
|
||||
...rest
|
||||
} = rawConfig;
|
||||
const { memory_connectors: snakeMemoryConnectors, memoryConnectors, state_db_path: snakeStateDbPath, stateDbPath: camelStateDbPath, ...rest } = rawConfig;
|
||||
const normalized = normalizeObjectKeys(rest);
|
||||
const connectorConfig = memoryConnectors || snakeMemoryConnectors || normalized.memoryConnectors;
|
||||
return {
|
||||
dbPath: options.dbPath || normalized.dbPath || defaultDbPath(options.env),
|
||||
stateDbPath: options.stateDbPath
|
||||
|| camelStateDbPath
|
||||
|| snakeStateDbPath
|
||||
|| normalized.stateDbPath
|
||||
|| defaultStateDbPath(options.env),
|
||||
memoryConnectors: normalizeMemoryConnectors(connectorConfig),
|
||||
stateDbPath: options.stateDbPath || camelStateDbPath || snakeStateDbPath || normalized.stateDbPath || defaultStateDbPath(options.env),
|
||||
memoryConnectors: normalizeMemoryConnectors(connectorConfig)
|
||||
};
|
||||
}
|
||||
|
||||
@ -108,9 +92,7 @@ function readTomlConfig(configPath) {
|
||||
function resolveControlPaneConfig(options = {}) {
|
||||
const env = options.env || process.env;
|
||||
const cwd = options.cwd || process.cwd();
|
||||
const configPaths = options.configPath
|
||||
? [path.resolve(options.configPath)]
|
||||
: defaultConfigPaths(cwd, env);
|
||||
const configPaths = options.configPath ? [path.resolve(options.configPath)] : defaultConfigPaths(cwd, env);
|
||||
let merged = {};
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
@ -123,9 +105,9 @@ function resolveControlPaneConfig(options = {}) {
|
||||
...normalizeConfig(merged, {
|
||||
env,
|
||||
dbPath: options.dbPath || env.ECC2_DB_PATH || null,
|
||||
stateDbPath: options.stateDbPath || env.ECC_STATE_DB_PATH || null,
|
||||
stateDbPath: options.stateDbPath || env.ECC_STATE_DB_PATH || null
|
||||
}),
|
||||
configPaths: configPaths.filter(configPath => fs.existsSync(configPath)),
|
||||
configPaths: configPaths.filter(configPath => fs.existsSync(configPath))
|
||||
};
|
||||
}
|
||||
|
||||
@ -149,11 +131,7 @@ function execRows(db, sql, params = []) {
|
||||
}
|
||||
|
||||
function tableExists(db, tableName) {
|
||||
const rows = execRows(
|
||||
db,
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1",
|
||||
[tableName]
|
||||
);
|
||||
const rows = execRows(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", [tableName]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
@ -188,7 +166,7 @@ function normalizeSession(row, unreadMessages) {
|
||||
? {
|
||||
path: String(row.worktree_path),
|
||||
branch: row.worktree_branch ? String(row.worktree_branch) : null,
|
||||
base: row.worktree_base ? String(row.worktree_base) : null,
|
||||
base: row.worktree_base ? String(row.worktree_base) : null
|
||||
}
|
||||
: null,
|
||||
metrics: {
|
||||
@ -198,23 +176,18 @@ function normalizeSession(row, unreadMessages) {
|
||||
toolCalls: toNumber(row.tool_calls),
|
||||
filesChanged: toNumber(row.files_changed),
|
||||
durationSecs: toNumber(row.duration_secs),
|
||||
costUsd: toNumber(row.cost_usd),
|
||||
costUsd: toNumber(row.cost_usd)
|
||||
},
|
||||
unreadMessages: unreadMessages.get(id) || 0,
|
||||
createdAt: String(row.created_at || ''),
|
||||
updatedAt: String(row.updated_at || ''),
|
||||
lastHeartbeatAt: String(row.last_heartbeat_at || ''),
|
||||
lastHeartbeatAt: String(row.last_heartbeat_at || '')
|
||||
};
|
||||
}
|
||||
|
||||
function readUnreadMessageCounts(db) {
|
||||
if (!tableExists(db, 'messages')) return new Map();
|
||||
return new Map(
|
||||
execRows(
|
||||
db,
|
||||
'SELECT to_session, COUNT(*) AS unread_count FROM messages WHERE read = 0 GROUP BY to_session'
|
||||
).map(row => [String(row.to_session), toNumber(row.unread_count)])
|
||||
);
|
||||
return new Map(execRows(db, 'SELECT to_session, COUNT(*) AS unread_count FROM messages WHERE read = 0 GROUP BY to_session').map(row => [String(row.to_session), toNumber(row.unread_count)]));
|
||||
}
|
||||
|
||||
function readSessions(db) {
|
||||
@ -241,7 +214,7 @@ function summarizeSessions(sessions) {
|
||||
unreadMessages: 0,
|
||||
activeWorktrees: 0,
|
||||
totalTokens: 0,
|
||||
totalCostUsd: 0,
|
||||
totalCostUsd: 0
|
||||
};
|
||||
|
||||
for (const session of sessions) {
|
||||
@ -278,7 +251,7 @@ function readEntities(db) {
|
||||
summary: String(row.summary || ''),
|
||||
metadata: parseJson(row.metadata_json, {}),
|
||||
createdAt: String(row.created_at || ''),
|
||||
updatedAt: String(row.updated_at || ''),
|
||||
updatedAt: String(row.updated_at || '')
|
||||
}));
|
||||
}
|
||||
|
||||
@ -299,7 +272,7 @@ function readObservations(db) {
|
||||
pinned: toNumber(row.pinned) === 1,
|
||||
summary: String(row.summary || ''),
|
||||
details: parseJson(row.details_json, {}),
|
||||
createdAt: String(row.created_at || ''),
|
||||
createdAt: String(row.created_at || '')
|
||||
}));
|
||||
}
|
||||
|
||||
@ -341,7 +314,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) {
|
||||
{ text: entity.path || '', weight: 6 },
|
||||
{ text: entity.summary, weight: 8 },
|
||||
{ text: metadataText, weight: 5 },
|
||||
{ text: observationText, weight: 10 },
|
||||
{ text: observationText, weight: 10 }
|
||||
].map(item => ({ ...item, text: item.text.toLowerCase() }));
|
||||
const matchedTerms = [];
|
||||
let score = 0;
|
||||
@ -357,10 +330,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) {
|
||||
if (matched) matchedTerms.push(term);
|
||||
}
|
||||
|
||||
const maxPriority = observations.reduce(
|
||||
(highest, observation) => Math.max(highest, observation.priority),
|
||||
0
|
||||
);
|
||||
const maxPriority = observations.reduce((highest, observation) => Math.max(highest, observation.priority), 0);
|
||||
const hasPinnedObservation = observations.some(observation => observation.pinned);
|
||||
score += Math.min(relationCount, 8);
|
||||
score += maxPriority * 3;
|
||||
@ -372,7 +342,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) {
|
||||
observationCount: observations.length,
|
||||
relationCount,
|
||||
maxObservationPriority: maxPriority,
|
||||
hasPinnedObservation,
|
||||
hasPinnedObservation
|
||||
};
|
||||
}
|
||||
|
||||
@ -388,23 +358,21 @@ function recallKnowledgeEntries({ entities, observations, relationCounts, query,
|
||||
return entities
|
||||
.map(entity => {
|
||||
const entityObservations = observationsByEntity.get(entity.id) || [];
|
||||
const score = queryTerms.length > 0
|
||||
? scoreEntity(entity, entityObservations, relationCounts.get(entity.id) || 0, queryTerms)
|
||||
: {
|
||||
score: entityObservations.some(observation => observation.pinned) ? 10 : 1,
|
||||
matchedTerms: [],
|
||||
observationCount: entityObservations.length,
|
||||
relationCount: relationCounts.get(entity.id) || 0,
|
||||
maxObservationPriority: entityObservations.reduce(
|
||||
(highest, observation) => Math.max(highest, observation.priority),
|
||||
0
|
||||
),
|
||||
hasPinnedObservation: entityObservations.some(observation => observation.pinned),
|
||||
};
|
||||
const score =
|
||||
queryTerms.length > 0
|
||||
? scoreEntity(entity, entityObservations, relationCounts.get(entity.id) || 0, queryTerms)
|
||||
: {
|
||||
score: entityObservations.some(observation => observation.pinned) ? 10 : 1,
|
||||
matchedTerms: [],
|
||||
observationCount: entityObservations.length,
|
||||
relationCount: relationCounts.get(entity.id) || 0,
|
||||
maxObservationPriority: entityObservations.reduce((highest, observation) => Math.max(highest, observation.priority), 0),
|
||||
hasPinnedObservation: entityObservations.some(observation => observation.pinned)
|
||||
};
|
||||
return {
|
||||
entity,
|
||||
...score,
|
||||
latestObservation: entityObservations[0] || null,
|
||||
latestObservation: entityObservations[0] || null
|
||||
};
|
||||
})
|
||||
.filter(entry => queryTerms.length === 0 || entry.matchedTerms.length > 0)
|
||||
@ -431,8 +399,8 @@ function connectorStatus(config, db) {
|
||||
String(row.connector_name),
|
||||
{
|
||||
syncedSources: toNumber(row.synced_sources),
|
||||
lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null,
|
||||
},
|
||||
lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@ -449,23 +417,56 @@ function connectorStatus(config, db) {
|
||||
defaultObservationType: connector.defaultObservationType || null,
|
||||
includeSafeValues: Boolean(connector.includeSafeValues),
|
||||
syncedSources: checkpoint.syncedSources,
|
||||
lastSyncedAt: checkpoint.lastSyncedAt,
|
||||
lastSyncedAt: checkpoint.lastSyncedAt
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeWorkItemStatus(status) {
|
||||
const normalized = String(status || 'open').trim().toLowerCase();
|
||||
const normalized = String(status || 'open')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (['done', 'closed', 'resolved', 'merged', 'cancelled'].includes(normalized)) return 'done';
|
||||
if (['blocked', 'needs-review', 'failed', 'stalled'].includes(normalized)) return 'blocked';
|
||||
if (['running', 'in-progress', 'active', 'working'].includes(normalized)) return 'running';
|
||||
return 'ready';
|
||||
}
|
||||
|
||||
// Heuristics for whether a work item's owner is an autonomous agent or a human.
|
||||
// Agent signals win when present so the board reflects who is *actively* on a card.
|
||||
const AGENT_OWNER_RE = /(agent|claude|codex|hermes|gemini|opencode|qwen|joycode|codebuddy|\bbot\b|gpt|sonnet|opus|haiku|fable)/i;
|
||||
const SESSION_ID_RE = /^(sid-|tx-|proj-|sess|session|run-|wt-)/i;
|
||||
|
||||
/**
|
||||
* Classify the assignment of a work item for agent+human JIT team workflows.
|
||||
* Returns the assignee kind ('agent' | 'human' | 'unassigned') and the resolved
|
||||
* assignee label, so the board can show who owns each card and which cards are
|
||||
* waiting for a just-in-time pickup.
|
||||
*/
|
||||
function classifyAssignee({ owner, sessionId, metadata = {} }) {
|
||||
const explicitKind = String(metadata.assigneeKind || metadata.ownerKind || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (explicitKind === 'agent' || explicitKind === 'human') {
|
||||
return { assigneeKind: explicitKind, assignee: owner || sessionId || metadata.assignee || null };
|
||||
}
|
||||
const ownerStr = owner ? String(owner) : '';
|
||||
if (sessionId || (ownerStr && (AGENT_OWNER_RE.test(ownerStr) || SESSION_ID_RE.test(ownerStr)))) {
|
||||
return { assigneeKind: 'agent', assignee: ownerStr || String(sessionId) };
|
||||
}
|
||||
if (ownerStr) {
|
||||
return { assigneeKind: 'human', assignee: ownerStr };
|
||||
}
|
||||
return { assigneeKind: 'unassigned', assignee: null };
|
||||
}
|
||||
|
||||
function normalizeWorkItem(row) {
|
||||
const parsedMetadata = parseJson(row.metadata, {});
|
||||
const metadata = isPlainObject(parsedMetadata) ? normalizeObjectKeys(parsedMetadata) : {};
|
||||
const kanbanState = normalizeWorkItemStatus(row.status);
|
||||
const owner = row.owner ? String(row.owner) : null;
|
||||
const sessionId = row.session_id ? String(row.session_id) : null;
|
||||
const { assigneeKind, assignee } = classifyAssignee({ owner, sessionId, metadata });
|
||||
return {
|
||||
id: String(row.id || ''),
|
||||
source: String(row.source || ''),
|
||||
@ -475,16 +476,18 @@ function normalizeWorkItem(row) {
|
||||
kanbanState,
|
||||
priority: row.priority ? String(row.priority) : null,
|
||||
url: row.url ? String(row.url) : null,
|
||||
owner: row.owner ? String(row.owner) : null,
|
||||
owner,
|
||||
assigneeKind,
|
||||
assignee,
|
||||
repoRoot: row.repo_root ? String(row.repo_root) : null,
|
||||
sessionId: row.session_id ? String(row.session_id) : null,
|
||||
sessionId,
|
||||
branch: metadata.branch || metadata.headRefName || null,
|
||||
mergeGate: metadata.mergeGate || metadata.mergeGateStatus || metadata.mergeStateStatus || null,
|
||||
blocker: metadata.blocker || null,
|
||||
acceptance: Array.isArray(metadata.acceptance) ? metadata.acceptance.map(String) : [],
|
||||
metadata,
|
||||
createdAt: String(row.created_at || ''),
|
||||
updatedAt: String(row.updated_at || ''),
|
||||
updatedAt: String(row.updated_at || '')
|
||||
};
|
||||
}
|
||||
|
||||
@ -509,22 +512,54 @@ function summarizeWorkItems(items) {
|
||||
ready: 0,
|
||||
running: 0,
|
||||
blocked: 0,
|
||||
done: 0,
|
||||
done: 0
|
||||
},
|
||||
items,
|
||||
// Agent + human JIT team-workflow view: who owns the open work, and which
|
||||
// open cards are waiting for a just-in-time pickup.
|
||||
assignment: {
|
||||
agent: 0,
|
||||
human: 0,
|
||||
unassigned: 0
|
||||
},
|
||||
needsAssignment: [],
|
||||
items
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
const kanbanState = normalizeWorkItemStatus(item.kanbanState || item.status);
|
||||
summary.kanban[kanbanState] += 1;
|
||||
const isOpen = kanbanState !== 'done';
|
||||
if (kanbanState === 'done') {
|
||||
summary.doneCount += 1;
|
||||
} else {
|
||||
summary.openCount += 1;
|
||||
}
|
||||
if (kanbanState === 'blocked') summary.blockedCount += 1;
|
||||
|
||||
// Assignment is only meaningful for open work; done cards don't need an owner.
|
||||
if (isOpen) {
|
||||
const kind = item.assigneeKind || classifyAssignee(item).assigneeKind;
|
||||
summary.assignment[kind] = (summary.assignment[kind] || 0) + 1;
|
||||
if (kind === 'unassigned') {
|
||||
summary.needsAssignment.push({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
kanbanState,
|
||||
priority: item.priority || null,
|
||||
url: item.url || null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Surface the highest-priority unclaimed work first for JIT pickup.
|
||||
const priorityRank = { critical: 0, high: 1, urgent: 1, medium: 2, normal: 2, low: 3 };
|
||||
summary.needsAssignment.sort((a, b) => {
|
||||
const ra = priorityRank[String(a.priority || '').toLowerCase()] ?? 2;
|
||||
const rb = priorityRank[String(b.priority || '').toLowerCase()] ?? 2;
|
||||
return ra - rb;
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
@ -547,7 +582,7 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
? normalizeConfig(options.config, {
|
||||
env: options.env || process.env,
|
||||
dbPath: options.dbPath || options.config.dbPath || null,
|
||||
stateDbPath: options.stateDbPath || options.config.stateDbPath || null,
|
||||
stateDbPath: options.stateDbPath || options.config.stateDbPath || null
|
||||
})
|
||||
: resolveControlPaneConfig(options);
|
||||
const dbPath = options.dbPath || config.dbPath;
|
||||
@ -563,17 +598,17 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
dbPath,
|
||||
stateDbPath,
|
||||
database: {
|
||||
exists: Boolean(dbPath && fs.existsSync(dbPath)),
|
||||
exists: Boolean(dbPath && fs.existsSync(dbPath))
|
||||
},
|
||||
stateDatabase: {
|
||||
exists: Boolean(stateDbPath && fs.existsSync(stateDbPath)),
|
||||
exists: Boolean(stateDbPath && fs.existsSync(stateDbPath))
|
||||
},
|
||||
config: {
|
||||
configPaths: config.configPaths || [],
|
||||
memoryConnectorCount: Object.keys(config.memoryConnectors || {}).length,
|
||||
memoryConnectorCount: Object.keys(config.memoryConnectors || {}).length
|
||||
},
|
||||
execution: {
|
||||
allowActions: options.allowActions !== false,
|
||||
allowActions: options.allowActions !== false
|
||||
},
|
||||
summary: summarizeSessions([]),
|
||||
sessions: [],
|
||||
@ -581,11 +616,11 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
query,
|
||||
entityCount: 0,
|
||||
observationCount: 0,
|
||||
results: [],
|
||||
results: []
|
||||
},
|
||||
connectors: connectorStatus(config, null),
|
||||
workItems,
|
||||
actions: buildControlPaneActions({ repoRoot, query, limit }),
|
||||
actions: buildControlPaneActions({ repoRoot, query, limit })
|
||||
};
|
||||
|
||||
const db = await openSqlDatabase(dbPath);
|
||||
@ -611,10 +646,10 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
observations,
|
||||
relationCounts,
|
||||
query,
|
||||
limit,
|
||||
}),
|
||||
limit
|
||||
})
|
||||
},
|
||||
connectors: connectorStatus(config, db),
|
||||
connectors: connectorStatus(config, db)
|
||||
};
|
||||
} finally {
|
||||
db.close();
|
||||
@ -627,5 +662,5 @@ module.exports = {
|
||||
defaultConfigPaths,
|
||||
defaultStateDbPath,
|
||||
recallKnowledgeEntries,
|
||||
resolveControlPaneConfig,
|
||||
resolveControlPaneConfig
|
||||
};
|
||||
|
||||
@ -497,7 +497,11 @@ function renderControlPaneHtml() {
|
||||
const summary = workItems || { totalCount: 0, openCount: 0, blockedCount: 0, doneCount: 0, kanban: {}, items: [] };
|
||||
const items = Array.isArray(summary.items) ? summary.items : [];
|
||||
const kanban = summary.kanban || {};
|
||||
$('#work-item-count').textContent = summary.openCount + ' open / ' + summary.blockedCount + ' blocked';
|
||||
const needsAssignment = Array.isArray(summary.needsAssignment) ? summary.needsAssignment : [];
|
||||
const assignment = summary.assignment || { agent: 0, human: 0, unassigned: 0 };
|
||||
$('#work-item-count').textContent = summary.openCount + ' open / ' + summary.blockedCount + ' blocked'
|
||||
+ ' / ' + (assignment.agent || 0) + ' agent / ' + (assignment.human || 0) + ' human'
|
||||
+ (needsAssignment.length ? ' / ' + needsAssignment.length + ' need owner' : '');
|
||||
|
||||
const lanes = ['ready', 'running', 'blocked', 'done'];
|
||||
const laneHtml = '<div class="kanban">' + lanes.map(lane =>
|
||||
@ -513,10 +517,11 @@ function renderControlPaneHtml() {
|
||||
const branch = item.branch || (item.metadata && item.metadata.branch) || '';
|
||||
const mergeGate = item.mergeGate || (item.metadata && item.metadata.mergeGate) || '';
|
||||
const blocker = item.blocker || (item.metadata && item.metadata.blocker) || '';
|
||||
const owner = item.owner || item.source || 'unassigned';
|
||||
const assigneeKind = item.assigneeKind || 'unassigned';
|
||||
const owner = item.assignee || item.owner || (assigneeKind === 'unassigned' ? 'unassigned (JIT)' : item.source) || 'unassigned';
|
||||
return '<div class="work-item">' +
|
||||
'<div class="row"><strong>' + escapeHtml(item.title || item.id) + '</strong>' + statePill(item.kanbanState || item.status) + '</div>' +
|
||||
'<div class="subtle">' + escapeHtml(owner) + ' - ' + escapeHtml(item.source || 'manual') + (item.priority ? ' - ' + escapeHtml(item.priority) : '') + '</div>' +
|
||||
'<div class="subtle">[' + escapeHtml(assigneeKind) + '] ' + escapeHtml(owner) + ' - ' + escapeHtml(item.source || 'manual') + (item.priority ? ' - ' + escapeHtml(item.priority) : '') + '</div>' +
|
||||
(branch ? '<div class="subtle">branch: ' + escapeHtml(branch) + '</div>' : '') +
|
||||
(mergeGate ? '<div class="subtle">merge gate: ' + escapeHtml(mergeGate) + '</div>' : '') +
|
||||
(blocker ? '<div class="subtle">blocker: ' + escapeHtml(blocker) + '</div>' : '') +
|
||||
@ -629,5 +634,5 @@ function renderControlPaneHtml() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderControlPaneHtml,
|
||||
renderControlPaneHtml
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user