glad-web 1.0.44 → 1.0.46
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/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/lib/claude/structured-session.js +10 -5
- package/lib/codex/image-store.js +1 -2
- package/lib/codex/structured-session.js +39 -76
- package/lib/commands/web.js +64 -9
- package/lib/config/manager.js +19 -0
- package/lib/server/routes/providers.js +0 -12
- package/lib/server/routes/skillhub.js +104 -0
- package/lib/session/file-attachment-store.js +168 -0
- package/lib/session/session-manager.js +143 -60
- package/lib/skillhub/client.js +121 -0
- package/lib/skillhub/settings-store.js +168 -0
- package/lib/skillhub/skill-installer.js +320 -0
- package/lib/web/bootstrap.js +34 -0
- package/lib/web/claude.js +44 -16
- package/lib/web/codex.js +51 -22
- package/lib/web/composer.js +293 -28
- package/lib/web/core.js +56 -38
- package/lib/web/git.js +53 -51
- package/lib/web/gitgraph.js +8 -8
- package/lib/web/index.html +61 -26
- package/lib/web/layout.js +4 -7
- package/lib/web/notifications.js +1 -0
- package/lib/web/session.js +7 -21
- package/lib/web/shell.js +17 -2
- package/lib/web/skillhub.js +197 -0
- package/lib/web/styles.css +138 -16
- package/lib/web/timed-inputs.js +6 -6
- package/package.json +6 -3
package/lib/web/claude.js
CHANGED
|
@@ -7,29 +7,34 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function setClaudeModeEnabled(enabled) {
|
|
10
|
-
const codexChat = isCodexSession()
|
|
10
|
+
const codexChat = isCodexSession();
|
|
11
11
|
const structured = enabled || codexChat;
|
|
12
12
|
const actionRail = enabled
|
|
13
13
|
? document.querySelector('.claude-control-rail')
|
|
14
14
|
: codexChat
|
|
15
15
|
? document.getElementById('codex-control-rail')
|
|
16
16
|
: document.getElementById('shortcut-rail');
|
|
17
|
-
const
|
|
17
|
+
const attachmentButton = document.getElementById('attachment-btn');
|
|
18
18
|
const scheduleButton = document.getElementById('schedule-send-btn');
|
|
19
|
-
if (actionRail &&
|
|
19
|
+
if (actionRail && attachmentButton && scheduleButton) {
|
|
20
20
|
actionRail.prepend(scheduleButton);
|
|
21
|
-
actionRail.prepend(
|
|
21
|
+
actionRail.prepend(attachmentButton);
|
|
22
22
|
}
|
|
23
23
|
document.getElementById('terminal-container').style.display = structured ? 'none' : '';
|
|
24
24
|
document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
|
|
25
25
|
document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
|
|
26
26
|
document.getElementById('claude-control-panel').style.display = enabled ? 'flex' : 'none';
|
|
27
27
|
document.getElementById('codex-control-panel').style.display = codexChat ? 'flex' : 'none';
|
|
28
|
-
document.getElementById('
|
|
29
|
-
document.getElementById('attach-image-btn').style.display = structured ? '' : 'none';
|
|
28
|
+
document.getElementById('attachment-btn').style.display = '';
|
|
30
29
|
document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
|
|
31
30
|
document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
|
|
32
31
|
document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
|
|
32
|
+
if (!codexChat) {
|
|
33
|
+
const skillPrefix = document.getElementById('composer-skill-prefix');
|
|
34
|
+
skillPrefix.classList.remove('active');
|
|
35
|
+
skillPrefix.innerHTML = '';
|
|
36
|
+
}
|
|
37
|
+
if (!structured) renderSessionAttention();
|
|
33
38
|
updateTerminalControlsHeight();
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -242,9 +247,12 @@
|
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
function applyClaudeState(state = {}) {
|
|
250
|
+
function applyClaudeState(state = {}, options = {}) {
|
|
246
251
|
claudeState = { ...claudeState, ...state };
|
|
247
252
|
claudeStatus = claudeState.status || claudeStatus;
|
|
253
|
+
if (typeof syncComposerSendState === 'function') {
|
|
254
|
+
syncComposerSendState({ acknowledgeProviderState: Boolean(options.providerStateReceived) });
|
|
255
|
+
}
|
|
248
256
|
const permissionEl = document.getElementById('claude-permission-select');
|
|
249
257
|
const modelEl = document.getElementById('claude-model-select');
|
|
250
258
|
const effortEl = document.getElementById('claude-effort-select');
|
|
@@ -314,8 +322,9 @@
|
|
|
314
322
|
const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
315
323
|
const mode = claudeState.permissionMode || 'default';
|
|
316
324
|
const parts = [];
|
|
325
|
+
const attention = [];
|
|
317
326
|
if (pending) {
|
|
318
|
-
|
|
327
|
+
attention.push(`<button type="button" class="session-attention-pill approval claude-approval-jump" onclick="jumpToClaudeApproval()" title="Jump to pending approval" aria-label="Jump to pending Claude approval"><span aria-hidden="true">!</span>${pending} approval${pending > 1 ? 's' : ''}<span aria-hidden="true">↓</span></button>`);
|
|
319
328
|
} else if (claudeStatus && !['idle', 'stopped', 'thinking'].includes(claudeStatus)) {
|
|
320
329
|
parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
|
|
321
330
|
}
|
|
@@ -324,6 +333,7 @@
|
|
|
324
333
|
}
|
|
325
334
|
el.innerHTML = parts.join('');
|
|
326
335
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
336
|
+
renderSessionAttention(attention);
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
function jumpToClaudeApproval() {
|
|
@@ -841,7 +851,7 @@
|
|
|
841
851
|
const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
|
|
842
852
|
if (parsed.kind === 'hidden') return '';
|
|
843
853
|
const attachments = Array.isArray(message.attachments) && message.attachments.length
|
|
844
|
-
? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || '
|
|
854
|
+
? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${item.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(item.name || 'Attachment')}</span>`).join('')}</div>` : '';
|
|
845
855
|
if (parsed.kind === 'command') {
|
|
846
856
|
const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
|
|
847
857
|
return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span>${attachments}</div>`;
|
|
@@ -1078,13 +1088,13 @@
|
|
|
1078
1088
|
} else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
|
|
1079
1089
|
claudeMessages = event.messages;
|
|
1080
1090
|
} else if (event.type === 'state' && event.state) {
|
|
1081
|
-
applyClaudeState(event.state);
|
|
1091
|
+
applyClaudeState(event.state, { providerStateReceived: true });
|
|
1082
1092
|
}
|
|
1083
1093
|
if (event.type !== 'state') applyClaudeState({
|
|
1084
1094
|
status: claudeStatus,
|
|
1085
1095
|
pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
|
|
1086
1096
|
canAbort: claudeStatus === 'thinking'
|
|
1087
|
-
});
|
|
1097
|
+
}, { providerStateReceived: event.type === 'status' });
|
|
1088
1098
|
renderClaudeChat();
|
|
1089
1099
|
}
|
|
1090
1100
|
|
|
@@ -1102,6 +1112,7 @@
|
|
|
1102
1112
|
}
|
|
1103
1113
|
|
|
1104
1114
|
async function createSession(toolKey, sessionName) {
|
|
1115
|
+
const selectedSkill = window.pendingSkillHubSkill || null;
|
|
1105
1116
|
try {
|
|
1106
1117
|
const workingDirectory = document.getElementById('cwd-field').value;
|
|
1107
1118
|
const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
|
|
@@ -1109,14 +1120,31 @@
|
|
|
1109
1120
|
model: runtimeConfig.defaultModel || 'default',
|
|
1110
1121
|
effort: runtimeConfig.defaultEffort || 'medium'
|
|
1111
1122
|
} : undefined;
|
|
1112
|
-
const
|
|
1123
|
+
const endpoint = selectedSkill ? '/api/skillhub/sessions' : '/api/sessions';
|
|
1124
|
+
const body = selectedSkill ? {
|
|
1125
|
+
toolKey,
|
|
1126
|
+
workingDirectory,
|
|
1127
|
+
skill: {
|
|
1128
|
+
id: selectedSkill.id,
|
|
1129
|
+
version: selectedSkill.version,
|
|
1130
|
+
digest: selectedSkill.digest
|
|
1131
|
+
}
|
|
1132
|
+
} : { toolKey, workingDirectory, claudeOptions };
|
|
1133
|
+
const list = document.getElementById('tools-list');
|
|
1134
|
+
if (selectedSkill) list.innerHTML = '<p class="skill-hall-status">Downloading and verifying Skill…</p>';
|
|
1135
|
+
const res = await fetchWithTimeout(endpoint, {
|
|
1113
1136
|
method: 'POST',
|
|
1114
1137
|
headers: { 'Content-Type': 'application/json' },
|
|
1115
|
-
body: JSON.stringify(
|
|
1116
|
-
});
|
|
1138
|
+
body: JSON.stringify(body)
|
|
1139
|
+
}, selectedSkill ? 70000 : 30000);
|
|
1117
1140
|
const data = await res.json();
|
|
1118
1141
|
if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
|
|
1119
1142
|
document.getElementById('modal-overlay').style.display = 'none';
|
|
1120
|
-
|
|
1121
|
-
|
|
1143
|
+
window.pendingSkillHubSkill = null;
|
|
1144
|
+
if (selectedSkill) joinSession(data.id, data.name || selectedSkill.name, 'codex');
|
|
1145
|
+
else refreshSessionsNow();
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
alert('Failed to create session: ' + e.message);
|
|
1148
|
+
if (selectedSkill) await showToolModal(selectedSkill);
|
|
1149
|
+
}
|
|
1122
1150
|
}
|
package/lib/web/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
|
|
2
2
|
function codexReadyForInput() {
|
|
3
|
-
return codexState.
|
|
3
|
+
return codexState.status === 'idle'
|
|
4
4
|
&& !codexState.aborting && !codexState.resuming && !codexResumeInFlight;
|
|
5
5
|
}
|
|
6
6
|
function codexJson(value) {
|
|
@@ -219,13 +219,30 @@
|
|
|
219
219
|
}
|
|
220
220
|
function renderCodexMessageSkills(item) {
|
|
221
221
|
const skills = Array.isArray(item?.skills) ? item.skills : [];
|
|
222
|
-
|
|
222
|
+
if (!skills.length) return '';
|
|
223
|
+
return `<div class="codex-message-skills">${skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('')}</div>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderComposerSkillPrefix() {
|
|
227
|
+
const container = document.getElementById('composer-skill-prefix');
|
|
228
|
+
if (!container) return;
|
|
229
|
+
const visible = selectedCodexSkill && codexReadyForInput();
|
|
230
|
+
const wasVisible = container.classList.contains('active');
|
|
231
|
+
container.classList.toggle('active', Boolean(visible));
|
|
232
|
+
container.innerHTML = visible
|
|
233
|
+
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
234
|
+
: '';
|
|
235
|
+
if (wasVisible !== Boolean(visible)) updateTerminalControlsHeight();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderCodexMessageAttachments(item) {
|
|
239
|
+
if (!Array.isArray(item.attachments) || !item.attachments.length) return '';
|
|
240
|
+
return `<div class="claude-message-attachments">${item.attachments.map(attachment => `<span class="claude-message-attachment" title="${escapeHtml(attachment.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${attachment.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(attachment.name || 'Attachment')}</span>`).join('')}</div>`;
|
|
223
241
|
}
|
|
224
242
|
function renderCodexMessageMeta(item, finalOnly = false, context = null) {
|
|
225
243
|
const time = renderCodexMessageTime(item, finalOnly);
|
|
226
|
-
const skills = renderCodexMessageSkills(item);
|
|
227
244
|
const meter = renderCodexContextMeter(context);
|
|
228
|
-
return time ||
|
|
245
|
+
return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
|
|
229
246
|
}
|
|
230
247
|
function syncCodexDom(current, next) {
|
|
231
248
|
if (!current || !next) return;
|
|
@@ -320,7 +337,7 @@
|
|
|
320
337
|
const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
|
|
321
338
|
parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item, true, context)}</div>`);
|
|
322
339
|
}
|
|
323
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"
|
|
340
|
+
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}">${renderCodexMessageSkills(item)}<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}${renderCodexMessageAttachments(item)}</div>${renderCodexMessageMeta(item)}</div>`);
|
|
324
341
|
else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
|
|
325
342
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
326
343
|
else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
|
|
@@ -328,16 +345,14 @@
|
|
|
328
345
|
i += 1;
|
|
329
346
|
}
|
|
330
347
|
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
331
|
-
const skillBubble = selectedCodexSkill && codexReadyForInput()
|
|
332
|
-
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
333
|
-
: '';
|
|
334
348
|
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
335
349
|
const template = document.createElement('template');
|
|
336
|
-
template.innerHTML = `<div class="codex-conversation">${
|
|
350
|
+
template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
|
|
337
351
|
const next = template.content.firstElementChild;
|
|
338
352
|
const current = container.firstElementChild;
|
|
339
353
|
if (!current) container.appendChild(next);
|
|
340
354
|
else syncCodexDom(current, next);
|
|
355
|
+
renderComposerSkillPrefix();
|
|
341
356
|
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
342
357
|
}
|
|
343
358
|
|
|
@@ -423,9 +438,11 @@
|
|
|
423
438
|
}, 300);
|
|
424
439
|
}
|
|
425
440
|
|
|
426
|
-
function applyCodexState(state = {}) {
|
|
427
|
-
const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
|
|
441
|
+
function applyCodexState(state = {}, options = {}) {
|
|
428
442
|
codexState = { ...codexState, ...state };
|
|
443
|
+
if (typeof syncComposerSendState === 'function') {
|
|
444
|
+
syncComposerSendState({ acknowledgeProviderState: Boolean(options.providerStateReceived) });
|
|
445
|
+
}
|
|
429
446
|
const permission = document.getElementById('codex-permission-select');
|
|
430
447
|
if (permission) {
|
|
431
448
|
const defaultOption = permission.querySelector('option[value="default"]');
|
|
@@ -459,11 +476,8 @@
|
|
|
459
476
|
if (resume) resume.disabled = !codexReadyForInput();
|
|
460
477
|
const fork = document.getElementById('codex-fork-btn');
|
|
461
478
|
if (fork) fork.disabled = !codexReadyForInput();
|
|
462
|
-
const terminal = document.getElementById('codex-terminal-switch');
|
|
463
|
-
if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
|
|
464
479
|
renderCodexStateBar();
|
|
465
480
|
renderCodexModelPanel();
|
|
466
|
-
if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
|
|
467
481
|
renderCodexChat();
|
|
468
482
|
}
|
|
469
483
|
|
|
@@ -473,12 +487,32 @@
|
|
|
473
487
|
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
474
488
|
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
475
489
|
const parts = [];
|
|
490
|
+
const attention = [];
|
|
476
491
|
if (codexState.aborting) parts.push('<span class="claude-state-pill warn">Stopping Codex…</span>');
|
|
477
492
|
else if (codexState.resuming || codexResumeInFlight) parts.push('<span class="claude-state-pill">Resuming conversation…</span>');
|
|
478
|
-
if (pending || codexState.status === 'waiting_approval')
|
|
479
|
-
if (subagents)
|
|
493
|
+
if (pending || codexState.status === 'waiting_approval') attention.push(`<button type="button" class="session-attention-pill approval codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval"><span aria-hidden="true">!</span>${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
|
|
494
|
+
if (subagents) attention.push(`<button type="button" class="session-attention-pill subagent codex-subagent-jump" onclick="jumpToActiveCodexSubagent()" title="Jump to active subagent" aria-label="Jump to active subagent"><span aria-hidden="true">↳</span>${subagents} subagent${subagents === 1 ? '' : 's'} running</button>`);
|
|
480
495
|
el.innerHTML = parts.join('');
|
|
481
496
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
497
|
+
renderSessionAttention(attention);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function jumpToActiveCodexSubagent(retry = true) {
|
|
501
|
+
const groups = Array.from(document.querySelectorAll('.codex-subagent-group'));
|
|
502
|
+
const target = groups[groups.length - 1];
|
|
503
|
+
if (!target) {
|
|
504
|
+
if (!retry) return false;
|
|
505
|
+
commitCodexChatRender();
|
|
506
|
+
requestAnimationFrame(() => jumpToActiveCodexSubagent(false));
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
target.open = true;
|
|
510
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
511
|
+
target.classList.remove('codex-approval-focus');
|
|
512
|
+
void target.offsetWidth;
|
|
513
|
+
target.classList.add('codex-approval-focus');
|
|
514
|
+
setTimeout(() => target.classList.remove('codex-approval-focus'), 1800);
|
|
515
|
+
return true;
|
|
482
516
|
}
|
|
483
517
|
|
|
484
518
|
function jumpToCodexApproval() {
|
|
@@ -590,7 +624,7 @@
|
|
|
590
624
|
}
|
|
591
625
|
else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
|
|
592
626
|
else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
|
|
593
|
-
if (event.state) applyCodexState(event.state); else renderCodexChat();
|
|
627
|
+
if (event.state) applyCodexState(event.state, { providerStateReceived: true }); else renderCodexChat();
|
|
594
628
|
}
|
|
595
629
|
|
|
596
630
|
function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
|
|
@@ -1009,8 +1043,3 @@
|
|
|
1009
1043
|
panel.innerHTML = '';
|
|
1010
1044
|
updateTerminalControlsHeight();
|
|
1011
1045
|
}
|
|
1012
|
-
async function toggleCodexPresentation() {
|
|
1013
|
-
const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
|
|
1014
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
|
|
1015
|
-
if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
|
|
1016
|
-
}
|