fix(v02): persist Web refresh state (#859)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -4,6 +4,8 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { commandInputRows } from "./CommandBar";
|
||||
|
||||
const sourcePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "CommandBar.tsx");
|
||||
|
||||
test("CommandBar releases the send lock on the next tick instead of waiting for the full agent turn", () => {
|
||||
@@ -13,3 +15,10 @@ test("CommandBar releases the send lock on the next tick instead of waiting for
|
||||
assert.doesNotMatch(source, /await props\.onSubmit\(next\)/u);
|
||||
assert.doesNotMatch(source, /\.finally\(\(\) => setSubmitting\(false\)\)/u);
|
||||
});
|
||||
|
||||
test("CommandBar textarea grows to five physical lines then becomes scrollable", () => {
|
||||
assert.deepEqual(commandInputRows(""), { rows: 1, scrollable: false });
|
||||
assert.deepEqual(commandInputRows("one\ntwo\nthree"), { rows: 3, scrollable: false });
|
||||
assert.deepEqual(commandInputRows("1\n2\n3\n4\n5"), { rows: 5, scrollable: false });
|
||||
assert.deepEqual(commandInputRows("1\n2\n3\n4\n5\n6"), { rows: 5, scrollable: true });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { FormEvent, KeyboardEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, KeyboardEvent, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { ProviderProfile } from "../../types/domain";
|
||||
import { DraftsList } from "./DraftsList";
|
||||
@@ -31,6 +31,7 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
|
||||
const [value, setValue] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [draftsOpen, setDraftsOpen] = useState(false);
|
||||
const inputRows = useMemo(() => commandInputRows(value), [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pickedDraft) {
|
||||
@@ -68,7 +69,7 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
|
||||
</label>
|
||||
<div className="input-shell">
|
||||
<span className="prompt-mark">hwlab</span>
|
||||
<textarea id="command-input" value={value} placeholder="输入任务,Enter 发送,Shift+Enter 换行" rows={1} onChange={(event) => { setValue(event.currentTarget.value); onTyping(); }} onKeyDown={keyDown} />
|
||||
<textarea id="command-input" className={inputRows.scrollable ? "is-scrollable" : undefined} value={value} placeholder="输入任务,Enter 发送,Shift+Enter 换行" rows={inputRows.rows} onChange={(event) => { setValue(event.currentTarget.value); onTyping(); }} onKeyDown={keyDown} />
|
||||
</div>
|
||||
<button className="command-button secondary command-drafts-toggle" id="command-drafts-toggle" type="button" aria-expanded={draftsOpen} aria-controls="command-drafts-panel" onClick={() => setDraftsOpen((open) => !open)}>最近输入</button>
|
||||
<button className="command-button" id="command-send" type="submit" disabled={props.disabled || submitting}>{sendLabel(props.submitMode, submitting)}</button>
|
||||
@@ -78,6 +79,11 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
|
||||
);
|
||||
}
|
||||
|
||||
export function commandInputRows(value: string): { rows: number; scrollable: boolean } {
|
||||
const physicalLines = Math.max(1, value.split("\n").length);
|
||||
return { rows: Math.min(5, physicalLines), scrollable: physicalLines > 5 };
|
||||
}
|
||||
|
||||
function sendLabel(submitMode: "turn" | "steer", submitting: boolean): string {
|
||||
if (submitting) return submitMode === "steer" ? "引导中" : "发送中";
|
||||
return submitMode === "steer" ? "引导" : "发送";
|
||||
|
||||
@@ -24,9 +24,7 @@ export function MessageTracePanel({ trace, defaultOpen, storageKey }: MessageTra
|
||||
useEffect(() => {
|
||||
if (traceRunning) {
|
||||
setOpen(true);
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
}, [traceRunning, trace.traceId]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function ensureWorkspace(current: WorkspaceRecord | null): Promise<
|
||||
return response.ok ? response.data?.workspace ?? null : null;
|
||||
}
|
||||
|
||||
export async function persistConversation(input: { workspace: WorkspaceRecord | null; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<void> {
|
||||
export async function persistConversation(input: { workspace: WorkspaceRecord | null; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<WorkspaceRecord | null> {
|
||||
await api.saveConversation(input.conversationId, {
|
||||
projectId: WORKBENCH_PROJECT_ID,
|
||||
conversationId: input.conversationId,
|
||||
@@ -107,7 +107,7 @@ export async function persistConversation(input: { workspace: WorkspaceRecord |
|
||||
snapshot: { source: "cloud-web-react-account-sync", updatedAt: new Date().toISOString(), sessionStatus: input.messages.at(-1)?.status ?? "source" }
|
||||
});
|
||||
if (input.workspace?.workspaceId) {
|
||||
await api.updateWorkspace(input.workspace.workspaceId, {
|
||||
const response = await api.updateWorkspace(input.workspace.workspaceId, {
|
||||
projectId: WORKBENCH_PROJECT_ID,
|
||||
expectedRevision: input.workspace.revision,
|
||||
selectedConversationId: input.conversationId,
|
||||
@@ -118,7 +118,9 @@ export async function persistConversation(input: { workspace: WorkspaceRecord |
|
||||
workspace: { source: "cloud-web-react-workspace-sync", updatedAt: new Date().toISOString() },
|
||||
updatedByClient: "cloud-web-react"
|
||||
});
|
||||
return response.ok ? response.data?.workspace ?? input.workspace : input.workspace;
|
||||
}
|
||||
return input.workspace;
|
||||
}
|
||||
|
||||
export function conversationsToTabs(conversations: ConversationRecord[], activeConversationId: string | null): SessionTab[] {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "bun:test";
|
||||
|
||||
const srcRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
test("issue 853 persists submitted user and running agent messages before long Code Agent polling", () => {
|
||||
const source = fs.readFileSync(path.join(srcRoot, "state/workbench.ts"), "utf8");
|
||||
const pendingIndex = source.indexOf('dispatch({ type: "message:pending"');
|
||||
const persistIndex = source.indexOf("let currentWorkspace = await persistConversation", pendingIndex);
|
||||
const routeIndex = source.indexOf("const route = steerMode ? api.steerAgentMessage : api.sendAgentMessage", pendingIndex);
|
||||
assert.ok(pendingIndex > 0);
|
||||
assert.ok(persistIndex > pendingIndex);
|
||||
assert.ok(routeIndex > persistIndex);
|
||||
assert.match(source.slice(persistIndex, routeIndex), /messages: \[\.\.\.state\.messages, user, pendingWithRetry\]/u);
|
||||
});
|
||||
|
||||
test("issue 853 left sidebar collapse only removes the left columns", () => {
|
||||
const css = fs.readFileSync(path.join(srcRoot, "styles/workbench.css"), "utf8");
|
||||
assert.match(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(0, 1fr\) var\(--resize-handle-width\) var\(--right-sidebar-width\); \}/u);
|
||||
assert.doesNotMatch(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(420px, 1fr\)/u);
|
||||
});
|
||||
|
||||
test("issue 853 completed trace panels keep stored open state instead of closing on refresh", () => {
|
||||
const source = fs.readFileSync(path.join(srcRoot, "components/conversation/MessageTracePanel.tsx"), "utf8");
|
||||
assert.doesNotMatch(source, /setOpen\(false\);/u);
|
||||
assert.match(source, /if \(traceRunning\) \{\n\s+setOpen\(true\);\n\s+\}/u);
|
||||
});
|
||||
@@ -48,6 +48,35 @@ test("mergeTraceResults keeps trace assistant markdown ahead of flattened termin
|
||||
assert.match(merged.assistantText ?? "", /\n\n\| devicePodId \| debug \|/u);
|
||||
});
|
||||
|
||||
test("mergeTraceResults keeps full polled trace events when terminal runnerTrace is compact", () => {
|
||||
const terminal: AgentChatResultResponse = {
|
||||
status: "completed",
|
||||
traceId: "trc_compact_terminal",
|
||||
assistantText: "最终回复",
|
||||
runnerTrace: {
|
||||
traceId: "trc_compact_terminal",
|
||||
status: "completed",
|
||||
eventCount: 97,
|
||||
eventsCompacted: true,
|
||||
fullTraceLoaded: false
|
||||
}
|
||||
};
|
||||
const events: TraceEvent[] = [
|
||||
{ seq: 1, label: "agentrun:backend:turn-started", status: "running" },
|
||||
{ seq: 42, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "中间回复" },
|
||||
{ seq: 97, label: "agentrun:result:completed", type: "result", status: "completed" }
|
||||
];
|
||||
const trace: TraceSnapshot = { traceId: "trc_compact_terminal", status: "completed", events, eventCount: 97, eventsCompacted: false, lastEventLabel: "agentrun:result:completed" };
|
||||
|
||||
const merged = mergeTraceResults(terminal, trace);
|
||||
|
||||
assert.equal(merged.runnerTrace?.events?.length, 3);
|
||||
assert.equal(merged.runnerTrace?.eventCount, 97);
|
||||
assert.equal(merged.runnerTrace?.eventsCompacted, false);
|
||||
assert.equal(merged.runnerTrace?.fullTraceLoaded, true);
|
||||
assert.equal(merged.traceEvents?.length, 3);
|
||||
});
|
||||
|
||||
test("subscribeToTrace keeps polling result when trace terminal lacks final response text", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalSetTimeout = globalThis.window?.setTimeout;
|
||||
|
||||
@@ -76,7 +76,9 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
if (!trace) return terminal;
|
||||
const traceEvents = Array.isArray(trace.events) ? trace.events : [];
|
||||
const terminalEvents = Array.isArray((terminal as { events?: TraceEvent[] }).events) ? ((terminal as { events?: TraceEvent[] }).events ?? []) : [];
|
||||
const events = traceEvents.length >= terminalEvents.length ? traceEvents : terminalEvents;
|
||||
const terminalRunnerTrace = terminal.runnerTrace && typeof terminal.runnerTrace === "object" ? terminal.runnerTrace : null;
|
||||
const terminalRunnerEvents = Array.isArray(terminalRunnerTrace?.events) ? terminalRunnerTrace.events : [];
|
||||
const events = longestTraceEvents(traceEvents, terminalEvents, terminalRunnerEvents);
|
||||
const mergedTrace = { ...trace, events, eventCount: trace.eventCount ?? events.length };
|
||||
const assistantText = assistantTextFromTraceEvents(events) ?? firstNonEmptyResultText(terminal);
|
||||
return {
|
||||
@@ -86,7 +88,7 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
threadId: trace.threadId ?? terminal.threadId,
|
||||
agentRun: trace.agentRun ?? terminal.agentRun,
|
||||
assistantText: assistantText ?? terminal.assistantText,
|
||||
runnerTrace: terminal.runnerTrace ?? snapshotToRunnerTrace(mergedTrace),
|
||||
runnerTrace: mergeTerminalRunnerTrace(terminalRunnerTrace, mergedTrace),
|
||||
traceEvents: terminal.traceEvents ?? events,
|
||||
events,
|
||||
eventCount: trace.eventCount ?? events.length,
|
||||
@@ -94,6 +96,26 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
} as AgentChatResultResponse;
|
||||
}
|
||||
|
||||
function longestTraceEvents(...candidates: TraceEvent[][]): TraceEvent[] {
|
||||
return candidates.reduce((best, item) => item.length > best.length ? item : best, [] as TraceEvent[]);
|
||||
}
|
||||
|
||||
function mergeTerminalRunnerTrace(terminalTrace: AgentChatResultResponse["runnerTrace"] | null, trace: TraceSnapshot): AgentChatResultResponse["runnerTrace"] {
|
||||
const restored = snapshotToRunnerTrace(trace);
|
||||
if (!terminalTrace) return restored;
|
||||
const terminalEvents = Array.isArray(terminalTrace.events) ? terminalTrace.events : [];
|
||||
const restoredEvents = Array.isArray(restored.events) ? restored.events : [];
|
||||
const events = restoredEvents.length >= terminalEvents.length ? restoredEvents : terminalEvents;
|
||||
return {
|
||||
...terminalTrace,
|
||||
...restored,
|
||||
events,
|
||||
eventCount: Math.max(Number(terminalTrace.eventCount ?? 0) || 0, Number(restored.eventCount ?? 0) || 0, events.length),
|
||||
eventsCompacted: false,
|
||||
fullTraceLoaded: events.length > 0 || restored.fullTraceLoaded === true || terminalTrace.fullTraceLoaded === true
|
||||
};
|
||||
}
|
||||
|
||||
function firstNonEmptyResultText(result: AgentChatResultResponse): string | null {
|
||||
const reply = result.reply;
|
||||
const replyText = typeof reply === "string" ? reply : typeof reply?.content === "string" ? reply.content : null;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ComposerState, WorkbenchState } from "./workbench-state";
|
||||
export type Action =
|
||||
| { type: "hydrate:start" }
|
||||
| { type: "hydrate:done"; workspace: WorkbenchState["workspace"]; conversations: ConversationRecord[]; messages: ChatMessage[] }
|
||||
| { type: "workspace:sync"; workspace: WorkbenchState["workspace"] }
|
||||
| { type: "hydrate:error"; error: string }
|
||||
| { type: "live:set"; live: WorkbenchState["live"]; availability: CodeAgentAvailability | null; selectedDevicePodId: string }
|
||||
| { type: "device:selected"; devicePodId: string }
|
||||
@@ -27,6 +28,7 @@ export function workbenchReducer(state: WorkbenchState, action: Action): Workben
|
||||
switch (action.type) {
|
||||
case "hydrate:start": return { ...state, loading: true, error: null };
|
||||
case "hydrate:done": return { ...state, loading: false, workspace: action.workspace, conversations: action.conversations, messages: action.messages, error: null };
|
||||
case "workspace:sync": return { ...state, workspace: action.workspace ?? state.workspace };
|
||||
case "hydrate:error": return { ...state, loading: false, error: action.error };
|
||||
case "live:set": return { ...state, live: action.live, codeAgentAvailability: action.availability, selectedDevicePodId: action.selectedDevicePodId };
|
||||
case "device:selected": return { ...state, selectedDevicePodId: action.devicePodId };
|
||||
|
||||
@@ -244,9 +244,9 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
||||
const pending = makeMessage("agent", "正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent 处理中" });
|
||||
const pendingWithRetry: ChatMessage = { ...pending, retryInput: value };
|
||||
dispatch({ type: "message:pending", user, pending: pendingWithRetry, request: { traceId, conversationId, sessionId, threadId } });
|
||||
let currentWorkspace = await persistConversation({ workspace: state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, pendingWithRetry] });
|
||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||
const route = steerMode ? api.steerAgentMessage : api.sendAgentMessage;
|
||||
// The submit call itself is a "kicked off" event; treat it as activity
|
||||
// so the initial POST has a fresh inactivity window.
|
||||
updateActivity();
|
||||
const submitActivityRef = buildActivityRef("code-agent-submit", null);
|
||||
const response = await route({
|
||||
@@ -259,23 +259,20 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
||||
traceId,
|
||||
providerProfile: state.providerProfile,
|
||||
gatewayShellTimeoutMs: state.gatewayShellTimeoutMs,
|
||||
workspaceId: state.workspace?.workspaceId,
|
||||
workspaceRevision: state.workspace?.revision,
|
||||
workspaceId: currentWorkspace?.workspaceId ?? state.workspace?.workspaceId,
|
||||
workspaceRevision: currentWorkspace?.revision ?? state.workspace?.revision,
|
||||
targetTraceId: steerMode ? traceId : null,
|
||||
steerTraceId
|
||||
}, state.codeAgentTimeoutMs, submitActivityRef);
|
||||
const finalize = async (result: AgentChatResultResponse, availability: CodeAgentAvailability | null): Promise<void> => {
|
||||
const completed = messageFromAgentResponse(pending.id, pending, result);
|
||||
dispatch({ type: "message:complete", messageId: pending.id, message: completed, availability: availability ?? result.availability ?? null });
|
||||
await persistConversation({ workspace: state.workspace, conversationId, sessionId: completed.sessionId ?? sessionId, threadId: completed.threadId ?? threadId, messages: [...state.messages, user, completed] });
|
||||
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId: completed.sessionId ?? sessionId, threadId: completed.threadId ?? threadId, messages: [...state.messages, user, completed] });
|
||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||
const conversations = await api.conversations(WORKBENCH_PROJECT_ID);
|
||||
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
||||
};
|
||||
if (response.ok && response.data) {
|
||||
// HWLAB #802: single unified path. onActivity on every successful
|
||||
// poll keeps the per-poll inactivity window from firing on a
|
||||
// healthy loop. Persist on both onComplete AND onInfrastructureError
|
||||
// so cloud-api records the fail state too.
|
||||
const subscriptionController = new AbortController();
|
||||
let lastSnapshot: TraceSnapshot | null = null;
|
||||
let terminalResult: AgentChatResultResponse | null = null;
|
||||
@@ -298,7 +295,8 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
||||
// the frontend-reported failure, not a silent "running" state.
|
||||
const failed = makeMessage("agent", `Code Agent 在 ${state.codeAgentTimeoutMs}ms 无活动后中断(${error});可以继续 steer 或查看 trace。`, "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
||||
dispatch({ type: "message:fail", messageId: pending.id, message: failed });
|
||||
void persistConversation({ workspace: state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, failed] });
|
||||
void persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, failed] })
|
||||
.then((workspace) => { if (workspace) dispatch({ type: "workspace:sync", workspace }); });
|
||||
subscriptionController.abort();
|
||||
}
|
||||
});
|
||||
@@ -315,6 +313,8 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
||||
} else {
|
||||
const failed = makeMessage("agent", response.error ?? "Code Agent 请求失败", "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 请求失败" });
|
||||
dispatch({ type: "message:fail", messageId: pending.id, message: failed });
|
||||
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, failed] });
|
||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||
}
|
||||
dispatch({ type: "chat:done" });
|
||||
}, [activeConversationId, composer.conversationId, composer.sessionId, composer.submitMode, composer.targetTraceId, composer.threadId, state.codeAgentTimeoutMs, state.gatewayShellTimeoutMs, state.messages, state.providerProfile, state.workspace]);
|
||||
|
||||
@@ -55,9 +55,9 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.status-chip-label { flex: 0 0 auto; }
|
||||
.status-chip-meta { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
|
||||
.workspace-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); grid-template-rows: minmax(0, 1fr); overflow: hidden; }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(0, 1fr) var(--resize-handle-width) var(--right-sidebar-width); }
|
||||
.workbench-shell.is-right-sidebar-collapsed .workspace-body { grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) 0 0; }
|
||||
.workbench-shell.is-left-sidebar-collapsed.is-right-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(420px, 1fr) 0 0; }
|
||||
.workbench-shell.is-left-sidebar-collapsed.is-right-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(0, 1fr) 0 0; }
|
||||
.workspace-body.is-route-without-sidebars { grid-template-columns: minmax(0, 1fr); }
|
||||
.activity-rail { min-width: 0; display: grid; grid-template-rows: repeat(4, minmax(54px, auto)) 1fr minmax(54px, auto); gap: 6px; padding: 10px 8px; background: var(--rail); overflow-x: hidden; }
|
||||
.rail-button { min-width: 0; min-height: 44px; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 8px 6px; background: transparent; color: #eef4ed; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
@@ -274,7 +274,8 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.agent-timeout-control select, .device-pod-picker select { border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; background: var(--surface); }
|
||||
.input-shell { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: end; gap: 8px; border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; background: #fff; }
|
||||
.prompt-mark { color: var(--accent); font-weight: 900; }
|
||||
#command-input { min-width: 0; width: 100%; min-height: 30px; max-height: 110px; resize: none; border: 0; outline: none; }
|
||||
#command-input { min-width: 0; width: 100%; min-height: 30px; max-height: calc(1.45em * 5 + 8px); line-height: 1.45; resize: none; border: 0; outline: none; overflow-y: hidden; }
|
||||
#command-input.is-scrollable { overflow-y: auto; }
|
||||
|
||||
.command-bar .command-button { min-height: 32px; padding: 6px 10px; }
|
||||
.command-drafts-toggle { white-space: nowrap; }
|
||||
|
||||
Reference in New Issue
Block a user