@poncho-ai/cli 0.30.0 → 0.30.2
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +28 -0
- package/dist/{chunk-5OLH7U3C.js → chunk-FA546WPW.js} +703 -208
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-EMTC7MK7.js → run-interactive-ink-FUMHN6DS.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +368 -110
- package/src/web-ui-client.ts +241 -9
- package/src/web-ui-styles.ts +50 -0
package/src/web-ui-client.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
30
30
|
parentConversationId: null,
|
|
31
31
|
todos: [],
|
|
32
32
|
todoPanelCollapsed: false,
|
|
33
|
+
cronSectionCollapsed: true,
|
|
34
|
+
cronShowAll: false,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
const agentInitial = document.body.dataset.agentInitial || "A";
|
|
@@ -773,11 +775,86 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
773
775
|
}
|
|
774
776
|
};
|
|
775
777
|
|
|
778
|
+
const cronCaretSvg = '<svg viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
|
|
779
|
+
|
|
780
|
+
const parseCronTitle = (title) => {
|
|
781
|
+
const rest = title.replace(/^\[cron\]\s*/, "");
|
|
782
|
+
const isoMatch = rest.match(/\s(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)$/);
|
|
783
|
+
if (isoMatch) {
|
|
784
|
+
return { jobName: rest.slice(0, isoMatch.index).trim(), timestamp: isoMatch[1] };
|
|
785
|
+
}
|
|
786
|
+
return { jobName: rest, timestamp: "" };
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const formatCronTimestamp = (isoStr) => {
|
|
790
|
+
if (!isoStr) return "";
|
|
791
|
+
try {
|
|
792
|
+
const d = new Date(isoStr);
|
|
793
|
+
if (isNaN(d.getTime())) return isoStr;
|
|
794
|
+
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
795
|
+
} catch { return isoStr; }
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const CRON_PAGE_SIZE = 20;
|
|
799
|
+
|
|
800
|
+
const appendCronSection = (cronConvs, needsDivider) => {
|
|
801
|
+
if (needsDivider) {
|
|
802
|
+
const divider = document.createElement("div");
|
|
803
|
+
divider.className = "sidebar-section-divider";
|
|
804
|
+
elements.list.appendChild(divider);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
cronConvs.sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0));
|
|
808
|
+
|
|
809
|
+
const isOpen = !state.cronSectionCollapsed;
|
|
810
|
+
const header = document.createElement("div");
|
|
811
|
+
header.className = "cron-section-header";
|
|
812
|
+
header.innerHTML =
|
|
813
|
+
'<span class="cron-section-caret' + (isOpen ? ' open' : '') + '">' + cronCaretSvg + '</span>' +
|
|
814
|
+
'<span>Cron jobs</span>' +
|
|
815
|
+
'<span class="cron-section-count">' + cronConvs.length + '</span>';
|
|
816
|
+
header.onclick = () => {
|
|
817
|
+
state.cronSectionCollapsed = !state.cronSectionCollapsed;
|
|
818
|
+
state.cronShowAll = false;
|
|
819
|
+
renderConversationList();
|
|
820
|
+
};
|
|
821
|
+
elements.list.appendChild(header);
|
|
822
|
+
|
|
823
|
+
if (state.cronSectionCollapsed) return;
|
|
824
|
+
|
|
825
|
+
const limit = state.cronShowAll ? cronConvs.length : CRON_PAGE_SIZE;
|
|
826
|
+
const visible = cronConvs.slice(0, limit);
|
|
827
|
+
|
|
828
|
+
for (const c of visible) {
|
|
829
|
+
const { jobName, timestamp } = parseCronTitle(c.title);
|
|
830
|
+
const fmtTime = formatCronTimestamp(timestamp);
|
|
831
|
+
const displayTitle = fmtTime ? jobName + " \\u00b7 " + fmtTime : c.title;
|
|
832
|
+
elements.list.appendChild(buildConversationItem(Object.assign({}, c, { title: displayTitle })));
|
|
833
|
+
appendSubagentsIfActive(c.conversationId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!state.cronShowAll && cronConvs.length > CRON_PAGE_SIZE) {
|
|
837
|
+
const remaining = cronConvs.length - CRON_PAGE_SIZE;
|
|
838
|
+
const viewMore = document.createElement("div");
|
|
839
|
+
viewMore.className = "cron-view-more";
|
|
840
|
+
viewMore.textContent = "View " + remaining + " more\\u2026";
|
|
841
|
+
viewMore.onclick = () => {
|
|
842
|
+
state.cronShowAll = true;
|
|
843
|
+
renderConversationList();
|
|
844
|
+
};
|
|
845
|
+
elements.list.appendChild(viewMore);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
776
849
|
const renderConversationList = () => {
|
|
777
850
|
elements.list.innerHTML = "";
|
|
778
851
|
const pending = state.conversations.filter(c => c.hasPendingApprovals);
|
|
779
852
|
const rest = state.conversations.filter(c => !c.hasPendingApprovals);
|
|
780
853
|
|
|
854
|
+
const isCron = (c) => c.title && c.title.startsWith("[cron]");
|
|
855
|
+
const cronConvs = rest.filter(isCron);
|
|
856
|
+
const nonCron = rest.filter(c => !isCron(c));
|
|
857
|
+
|
|
781
858
|
if (pending.length > 0) {
|
|
782
859
|
const label = document.createElement("div");
|
|
783
860
|
label.className = "sidebar-section-label";
|
|
@@ -796,7 +873,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
796
873
|
const latest = [];
|
|
797
874
|
const previous7 = [];
|
|
798
875
|
const older = [];
|
|
799
|
-
for (const c of
|
|
876
|
+
for (const c of nonCron) {
|
|
800
877
|
const ts = c.updatedAt || c.createdAt || 0;
|
|
801
878
|
if (ts >= startOfToday) {
|
|
802
879
|
latest.push(c);
|
|
@@ -808,6 +885,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
808
885
|
}
|
|
809
886
|
|
|
810
887
|
let sectionRendered = pending.length > 0;
|
|
888
|
+
|
|
889
|
+
if (cronConvs.length > 0) {
|
|
890
|
+
appendCronSection(cronConvs, sectionRendered);
|
|
891
|
+
sectionRendered = true;
|
|
892
|
+
}
|
|
893
|
+
|
|
811
894
|
const appendSection = (items, labelText) => {
|
|
812
895
|
if (items.length === 0) return;
|
|
813
896
|
if (sectionRendered) {
|
|
@@ -1261,6 +1344,101 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
1261
1344
|
});
|
|
1262
1345
|
} else if (willStream) {
|
|
1263
1346
|
setStreaming(true);
|
|
1347
|
+
} else if (payload.needsContinuation && !payload.conversation.parentConversationId) {
|
|
1348
|
+
console.log("[poncho] Detected orphaned continuation for", conversationId, "— auto-resuming");
|
|
1349
|
+
(async () => {
|
|
1350
|
+
try {
|
|
1351
|
+
setStreaming(true);
|
|
1352
|
+
var localMsgs = state.activeMessages || [];
|
|
1353
|
+
var contAssistant = {
|
|
1354
|
+
role: "assistant",
|
|
1355
|
+
content: "",
|
|
1356
|
+
_sections: [],
|
|
1357
|
+
_currentText: "",
|
|
1358
|
+
_currentTools: [],
|
|
1359
|
+
_toolImages: [],
|
|
1360
|
+
_activeActivities: [],
|
|
1361
|
+
_pendingApprovals: [],
|
|
1362
|
+
metadata: { toolActivity: [] }
|
|
1363
|
+
};
|
|
1364
|
+
localMsgs.push(contAssistant);
|
|
1365
|
+
state.activeMessages = localMsgs;
|
|
1366
|
+
state._activeStreamMessages = localMsgs;
|
|
1367
|
+
renderMessages(localMsgs, true);
|
|
1368
|
+
var contResp = await fetch(
|
|
1369
|
+
"/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
|
|
1370
|
+
{
|
|
1371
|
+
method: "POST",
|
|
1372
|
+
credentials: "include",
|
|
1373
|
+
headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
|
|
1374
|
+
body: JSON.stringify({ continuation: true }),
|
|
1375
|
+
},
|
|
1376
|
+
);
|
|
1377
|
+
if (!contResp.ok || !contResp.body) {
|
|
1378
|
+
contAssistant._error = "Failed to resume — reload to retry";
|
|
1379
|
+
setStreaming(false);
|
|
1380
|
+
renderMessages(localMsgs, false);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
state.activeStreamConversationId = conversationId;
|
|
1384
|
+
var contReader = contResp.body.getReader();
|
|
1385
|
+
var contDecoder = new TextDecoder();
|
|
1386
|
+
var contBuffer = "";
|
|
1387
|
+
while (true) {
|
|
1388
|
+
var chunk = await contReader.read();
|
|
1389
|
+
if (chunk.done) break;
|
|
1390
|
+
contBuffer += contDecoder.decode(chunk.value, { stream: true });
|
|
1391
|
+
contBuffer = parseSseChunk(contBuffer, function(evtName, evtPayload) {
|
|
1392
|
+
if (evtName === "model:chunk" && evtPayload.content) {
|
|
1393
|
+
contAssistant.content = (contAssistant.content || "") + evtPayload.content;
|
|
1394
|
+
contAssistant._currentText += evtPayload.content;
|
|
1395
|
+
}
|
|
1396
|
+
if (evtName === "tool:started") {
|
|
1397
|
+
if (contAssistant._currentText) {
|
|
1398
|
+
contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
|
|
1399
|
+
contAssistant._currentText = "";
|
|
1400
|
+
}
|
|
1401
|
+
contAssistant._currentTools.push("- start \`" + evtPayload.tool + "\`");
|
|
1402
|
+
}
|
|
1403
|
+
if (evtName === "tool:completed") {
|
|
1404
|
+
contAssistant._currentTools.push("- done \`" + evtPayload.tool + "\` (" + evtPayload.duration + "ms)");
|
|
1405
|
+
}
|
|
1406
|
+
if (evtName === "tool:error") {
|
|
1407
|
+
contAssistant._currentTools.push("- error \`" + evtPayload.tool + "\`: " + evtPayload.error);
|
|
1408
|
+
}
|
|
1409
|
+
if (evtName === "run:completed" || evtName === "run:error" || evtName === "run:cancelled") {
|
|
1410
|
+
if (contAssistant._currentTools.length > 0) {
|
|
1411
|
+
contAssistant._sections.push({ type: "tools", content: contAssistant._currentTools });
|
|
1412
|
+
contAssistant._currentTools = [];
|
|
1413
|
+
}
|
|
1414
|
+
if (contAssistant._currentText) {
|
|
1415
|
+
contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
|
|
1416
|
+
contAssistant._currentText = "";
|
|
1417
|
+
}
|
|
1418
|
+
contAssistant._activeActivities = [];
|
|
1419
|
+
if (evtName === "run:error") {
|
|
1420
|
+
contAssistant._error = evtPayload.error?.message || "Something went wrong";
|
|
1421
|
+
}
|
|
1422
|
+
if (evtName === "run:completed" && evtPayload.result?.continuation === true) {
|
|
1423
|
+
// Another continuation needed — reload to pick it up
|
|
1424
|
+
loadConversation(conversationId).catch(function() {});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
renderMessages(localMsgs, true);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
setStreaming(false);
|
|
1431
|
+
renderMessages(localMsgs, false);
|
|
1432
|
+
await loadConversations();
|
|
1433
|
+
} catch (contErr) {
|
|
1434
|
+
console.error("[poncho] Auto-continuation failed:", contErr);
|
|
1435
|
+
setStreaming(false);
|
|
1436
|
+
await loadConversation(conversationId).catch(function() {});
|
|
1437
|
+
} finally {
|
|
1438
|
+
state.activeStreamConversationId = null;
|
|
1439
|
+
state._activeStreamMessages = null;
|
|
1440
|
+
}
|
|
1441
|
+
})();
|
|
1264
1442
|
}
|
|
1265
1443
|
};
|
|
1266
1444
|
|
|
@@ -2344,6 +2522,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2344
2522
|
let _totalSteps = 0;
|
|
2345
2523
|
let _maxSteps = 0;
|
|
2346
2524
|
let _isContinuation = false;
|
|
2525
|
+
let _receivedTerminalEvent = false;
|
|
2347
2526
|
while (true) {
|
|
2348
2527
|
let _shouldContinue = false;
|
|
2349
2528
|
let fetchOpts;
|
|
@@ -2673,6 +2852,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2673
2852
|
}
|
|
2674
2853
|
}
|
|
2675
2854
|
if (eventName === "run:completed") {
|
|
2855
|
+
_receivedTerminalEvent = true;
|
|
2676
2856
|
_totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
|
|
2677
2857
|
if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
|
|
2678
2858
|
if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
|
|
@@ -2689,10 +2869,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2689
2869
|
}
|
|
2690
2870
|
}
|
|
2691
2871
|
if (eventName === "run:cancelled") {
|
|
2872
|
+
_receivedTerminalEvent = true;
|
|
2692
2873
|
finalizeAssistantMessage();
|
|
2693
2874
|
renderIfActiveConversation(false);
|
|
2694
2875
|
}
|
|
2695
2876
|
if (eventName === "run:error") {
|
|
2877
|
+
_receivedTerminalEvent = true;
|
|
2696
2878
|
finalizeAssistantMessage();
|
|
2697
2879
|
const errMsg = payload.error?.message || "Something went wrong";
|
|
2698
2880
|
assistantMessage._error = errMsg;
|
|
@@ -2703,7 +2885,19 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2703
2885
|
}
|
|
2704
2886
|
});
|
|
2705
2887
|
}
|
|
2888
|
+
if (!_shouldContinue && !_receivedTerminalEvent) {
|
|
2889
|
+
try {
|
|
2890
|
+
const recoveryPayload = await api("/api/conversations/" + encodeURIComponent(conversationId));
|
|
2891
|
+
if (recoveryPayload.needsContinuation) {
|
|
2892
|
+
_shouldContinue = true;
|
|
2893
|
+
console.log("[poncho] Stream ended without terminal event, server has continuation — resuming");
|
|
2894
|
+
}
|
|
2895
|
+
} catch (_recoverErr) {
|
|
2896
|
+
console.warn("[poncho] Recovery check failed after abrupt stream end");
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2706
2899
|
if (!_shouldContinue) break;
|
|
2900
|
+
_receivedTerminalEvent = false;
|
|
2707
2901
|
_isContinuation = true;
|
|
2708
2902
|
}
|
|
2709
2903
|
// Update active state only if user is still on this conversation.
|
|
@@ -2977,7 +3171,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2977
3171
|
state: "resolved",
|
|
2978
3172
|
resolvedDecision: decision,
|
|
2979
3173
|
}));
|
|
2980
|
-
api("/api/approvals/" + encodeURIComponent(approvalId), {
|
|
3174
|
+
return api("/api/approvals/" + encodeURIComponent(approvalId), {
|
|
2981
3175
|
method: "POST",
|
|
2982
3176
|
body: JSON.stringify({ approved: decision === "approve" }),
|
|
2983
3177
|
}).catch((error) => {
|
|
@@ -3025,16 +3219,54 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
3025
3219
|
if (pending.length === 0) return;
|
|
3026
3220
|
const wasStreaming = state.isStreaming;
|
|
3027
3221
|
if (!wasStreaming) setStreaming(true);
|
|
3028
|
-
|
|
3222
|
+
// Mark all items as resolved in the UI immediately
|
|
3223
|
+
for (const aid of pending) {
|
|
3224
|
+
state.approvalRequestsInFlight[aid] = true;
|
|
3225
|
+
updatePendingApproval(aid, (request) => ({
|
|
3226
|
+
...request,
|
|
3227
|
+
state: "resolved",
|
|
3228
|
+
resolvedDecision: decision,
|
|
3229
|
+
}));
|
|
3230
|
+
}
|
|
3029
3231
|
renderMessages(state.activeMessages, state.isStreaming);
|
|
3030
3232
|
loadConversations();
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3233
|
+
const streamCid = !wasStreaming && state.activeConversationId
|
|
3234
|
+
? state.activeConversationId
|
|
3235
|
+
: null;
|
|
3236
|
+
if (streamCid) {
|
|
3237
|
+
streamConversationEvents(streamCid, { liveOnly: true }).finally(() => {
|
|
3238
|
+
if (state.activeConversationId === streamCid) {
|
|
3239
|
+
pollUntilRunIdle(streamCid);
|
|
3240
|
+
}
|
|
3241
|
+
});
|
|
3037
3242
|
}
|
|
3243
|
+
// Send API calls sequentially so each store write completes
|
|
3244
|
+
// before the next read (avoids last-writer-wins in serverless).
|
|
3245
|
+
void (async () => {
|
|
3246
|
+
for (const aid of pending) {
|
|
3247
|
+
await api("/api/approvals/" + encodeURIComponent(aid), {
|
|
3248
|
+
method: "POST",
|
|
3249
|
+
body: JSON.stringify({ approved: decision === "approve" }),
|
|
3250
|
+
}).catch((error) => {
|
|
3251
|
+
const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
|
|
3252
|
+
if (isStale) {
|
|
3253
|
+
updatePendingApproval(aid, () => null);
|
|
3254
|
+
} else {
|
|
3255
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
3256
|
+
updatePendingApproval(aid, (request) => ({
|
|
3257
|
+
...request,
|
|
3258
|
+
state: "pending",
|
|
3259
|
+
pendingDecision: null,
|
|
3260
|
+
resolvedDecision: null,
|
|
3261
|
+
_error: errMsg,
|
|
3262
|
+
}));
|
|
3263
|
+
}
|
|
3264
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
3265
|
+
}).finally(() => {
|
|
3266
|
+
delete state.approvalRequestsInFlight[aid];
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
})();
|
|
3038
3270
|
return;
|
|
3039
3271
|
}
|
|
3040
3272
|
|
package/src/web-ui-styles.ts
CHANGED
|
@@ -401,6 +401,41 @@ export const WEB_UI_STYLES = `
|
|
|
401
401
|
.conversation-item .delete-btn.confirming:hover {
|
|
402
402
|
color: var(--error-alt);
|
|
403
403
|
}
|
|
404
|
+
.cron-section-header {
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 6px;
|
|
408
|
+
padding: 8px 10px 4px;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
font-size: 11px;
|
|
411
|
+
font-weight: 600;
|
|
412
|
+
color: var(--fg-7);
|
|
413
|
+
text-transform: uppercase;
|
|
414
|
+
letter-spacing: 0.04em;
|
|
415
|
+
user-select: none;
|
|
416
|
+
transition: color 0.15s;
|
|
417
|
+
}
|
|
418
|
+
.cron-section-header:hover { color: var(--fg-5); }
|
|
419
|
+
.cron-section-caret {
|
|
420
|
+
display: inline-flex;
|
|
421
|
+
transition: transform 0.15s;
|
|
422
|
+
}
|
|
423
|
+
.cron-section-caret.open { transform: rotate(90deg); }
|
|
424
|
+
.cron-section-count {
|
|
425
|
+
font-weight: 400;
|
|
426
|
+
color: var(--fg-8);
|
|
427
|
+
font-size: 11px;
|
|
428
|
+
}
|
|
429
|
+
.cron-view-more {
|
|
430
|
+
padding: 6px 10px;
|
|
431
|
+
font-size: 12px;
|
|
432
|
+
color: var(--fg-7);
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
text-align: center;
|
|
435
|
+
transition: color 0.15s;
|
|
436
|
+
user-select: none;
|
|
437
|
+
}
|
|
438
|
+
.cron-view-more:hover { color: var(--fg-3); }
|
|
404
439
|
.sidebar-footer {
|
|
405
440
|
margin-top: auto;
|
|
406
441
|
padding-top: 8px;
|
|
@@ -1572,6 +1607,8 @@ export const WEB_UI_STYLES = `
|
|
|
1572
1607
|
line-height: 1.45;
|
|
1573
1608
|
color: var(--fg-tool-code);
|
|
1574
1609
|
width: 100%;
|
|
1610
|
+
min-width: 0;
|
|
1611
|
+
overflow: hidden;
|
|
1575
1612
|
}
|
|
1576
1613
|
.subagent-result-summary {
|
|
1577
1614
|
list-style: none;
|
|
@@ -1616,6 +1653,19 @@ export const WEB_UI_STYLES = `
|
|
|
1616
1653
|
display: grid;
|
|
1617
1654
|
gap: 6px;
|
|
1618
1655
|
padding: 0 12px 10px;
|
|
1656
|
+
min-width: 0;
|
|
1657
|
+
overflow-x: auto;
|
|
1658
|
+
overflow-wrap: break-word;
|
|
1659
|
+
word-break: break-word;
|
|
1660
|
+
}
|
|
1661
|
+
.subagent-result-body pre {
|
|
1662
|
+
max-width: 100%;
|
|
1663
|
+
overflow-x: auto;
|
|
1664
|
+
}
|
|
1665
|
+
.subagent-result-body table {
|
|
1666
|
+
max-width: 100%;
|
|
1667
|
+
overflow-x: auto;
|
|
1668
|
+
display: block;
|
|
1619
1669
|
}
|
|
1620
1670
|
|
|
1621
1671
|
/* Todo panel — inside composer-inner, above the input shell */
|