Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ designs/

.codegraph/
.mcp.json
.claude/
.claude/
.planning/
5 changes: 5 additions & 0 deletions src/services/builtinSerialMonitorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface SerialReadResult {
export interface SerialMonitorSettings {
showTimestamp: boolean;
renderAnsi: boolean;
localEcho: boolean;
logBaudRate: number;
}

Expand Down Expand Up @@ -770,6 +771,9 @@ export class BuiltinSerialMonitorService {
if (typeof settings.renderAnsi === 'boolean') {
await this.workspaceStateService.setSerialMonitorRenderAnsi(settings.renderAnsi);
}
if (typeof settings.localEcho === 'boolean') {
await this.workspaceStateService.setSerialMonitorLocalEcho(settings.localEcho);
}

const nextBaudRate = this.resolveLogBaudRate(settings.logBaudRate);
if (nextBaudRate === undefined) {
Expand Down Expand Up @@ -956,6 +960,7 @@ export class BuiltinSerialMonitorService {
return {
showTimestamp: this.workspaceStateService.getSerialMonitorShowTimestamp(),
renderAnsi: this.workspaceStateService.getSerialMonitorRenderAnsi(),
localEcho: this.workspaceStateService.getSerialMonitorLocalEcho(),
logBaudRate: this.workspaceStateService.getMonitorBaudRate(),
};
}
Expand Down
14 changes: 14 additions & 0 deletions src/services/workspaceStateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface WorkspaceState {
serialMonitorShowTimestamp?: boolean;
// 串口监视器是否渲染 VT-100/ANSI 颜色
serialMonitorRenderAnsi?: boolean;
// 串口监视器终端模式是否本地回显输入
serialMonitorLocalEcho?: boolean;
}

// 工作区状态的 key 常量
Expand All @@ -43,6 +45,7 @@ export const WORKSPACE_STATE_KEYS = {
WORKFLOW_SHELL_APPROVALS: 'workflowShellApprovals',
SERIAL_MONITOR_SHOW_TIMESTAMP: 'serialMonitorShowTimestamp',
SERIAL_MONITOR_RENDER_ANSI: 'serialMonitorRenderAnsi',
SERIAL_MONITOR_LOCAL_ECHO: 'serialMonitorLocalEcho',
} as const;

// 默认值
Expand All @@ -58,6 +61,7 @@ const DEFAULT_VALUES: Required<WorkspaceState> = {
workflowShellApprovals: {},
serialMonitorShowTimestamp: true,
serialMonitorRenderAnsi: true,
serialMonitorLocalEcho: false,
};

/**
Expand Down Expand Up @@ -134,6 +138,7 @@ export class WorkspaceStateService {
workflowShellApprovals: this.get('workflowShellApprovals'),
serialMonitorShowTimestamp: this.get('serialMonitorShowTimestamp'),
serialMonitorRenderAnsi: this.get('serialMonitorRenderAnsi'),
serialMonitorLocalEcho: this.get('serialMonitorLocalEcho'),
};
}

Expand Down Expand Up @@ -277,4 +282,13 @@ export class WorkspaceStateService {
public async setSerialMonitorRenderAnsi(value: boolean): Promise<void> {
await this.set('serialMonitorRenderAnsi', value);
}

// serialMonitorLocalEcho
public getSerialMonitorLocalEcho(): boolean {
return this.get('serialMonitorLocalEcho') ?? DEFAULT_VALUES.serialMonitorLocalEcho;
}

public async setSerialMonitorLocalEcho(value: boolean): Promise<void> {
await this.set('serialMonitorLocalEcho', value);
}
}
10 changes: 8 additions & 2 deletions src/test/vueWebviewAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ describe('vueWebviewAssets', () => {
return dir;
}

it('loads only the default entry css for the main Vue webview', () => {
it('loads the default entry css and shared xterm css for the main Vue webview', () => {
const distPath = createDistWithCssFiles(['index.css', 'buildLogs.css', 'xterm.css']);

assert.deepStrictEqual(resolveVueWebviewCssFiles(distPath), ['assets/index.css']);
assert.deepStrictEqual(resolveVueWebviewCssFiles(distPath), ['assets/index.css', 'assets/xterm.css']);
});

it('keeps unrelated entry css out of the main Vue webview', () => {
const distPath = createDistWithCssFiles(['index.css', 'buildLogs.css', 'memoryMap.css', 'xterm.css']);

assert.deepStrictEqual(resolveVueWebviewCssFiles(distPath), ['assets/index.css', 'assets/xterm.css']);
});

it('uses explicit css files for the build logs webview', () => {
Expand Down
5 changes: 4 additions & 1 deletion src/utils/vueWebviewAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export function resolveVueWebviewCssFiles(
const scriptName = path.posix.basename(normalizedScriptFile, path.posix.extname(normalizedScriptFile));
const entryCssFile = path.posix.join(scriptDir, `${scriptName}.css`);
const entryCssPath = path.join(vueDistPath, ...entryCssFile.split('/'));
const sharedCssFiles = ['xterm.css']
.map(file => path.posix.join(scriptDir, file))
.filter(file => file !== entryCssFile && fs.existsSync(path.join(vueDistPath, ...file.split('/'))));

return fs.existsSync(entryCssPath) ? [entryCssFile] : [];
return fs.existsSync(entryCssPath) ? [entryCssFile, ...sharedCssFiles] : sharedCssFiles;
}
2 changes: 1 addition & 1 deletion webview-vue/src/components/layout/AppShell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const isStandaloneRoute = computed(
const showBackButton = computed(() => route.path !== '/' && !isStandaloneRoute.value);
const rootClass = computed(() =>
isViewportLockedRoute.value
? 'h-screen min-h-0 overflow-hidden p-0 text-vscode-foreground'
? 'h-full min-h-0 overflow-hidden p-0 text-vscode-foreground'
: 'min-h-screen p-0 text-vscode-foreground'
);
const shellClass = computed(() =>
Expand Down
2 changes: 2 additions & 0 deletions webview-vue/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@
"description": "These preferences are saved in the current workspace and apply when the monitor is reopened.",
"logBaudRate": "Log baud rate",
"logBaudRateDescription": "Used by serial logs and MCP sessions. It does not change the status bar COM download baud rate.",
"localEcho": "Local echo terminal input",
"localEchoDescription": "Show typed characters immediately in terminal mode. Keep this off when the device echoes input itself.",
"renderAnsi": "Render VT-100 colors",
"renderAnsiDescription": "Parse ANSI SGR color sequences. Turn it off to show logs as plain text.",
"showTimestamp": "Show timestamps",
Expand Down
2 changes: 2 additions & 0 deletions webview-vue/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@
"description": "这些偏好会保存在当前工作区,重新打开监视器后仍会生效。",
"logBaudRate": "日志波特率",
"logBaudRateDescription": "用于串口日志和 MCP 会话,不影响状态栏 COM 下载波特率。",
"localEcho": "终端输入本地回显",
"localEchoDescription": "开启后,在终端模式中输入的字符会立即显示;如果设备自身会回显,请保持关闭。",
"renderAnsi": "渲染 VT-100 颜色",
"renderAnsiDescription": "解析 ANSI SGR 颜色序列,关闭后会以普通文本显示日志。",
"showTimestamp": "显示时间戳",
Expand Down
1 change: 1 addition & 0 deletions webview-vue/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export interface SerialMonitorSnapshot {
settings: {
showTimestamp: boolean;
renderAnsi: boolean;
localEcho: boolean;
logBaudRate: number;
};
}
Expand Down
44 changes: 43 additions & 1 deletion webview-vue/src/views/SerialMonitorPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@
<input v-model="settings.renderAnsi" type="checkbox" class="h-4 w-4" @change="updateSettings" />
</label>

<label
class="mt-4 flex cursor-pointer items-center justify-between gap-4 border-t border-vscode-panel-border pt-4"
>
<span>
<span class="block text-sm">{{ t('serialMonitor.settings.localEcho') }}</span>
<span class="mt-1 block text-xs text-vscode-input-placeholder">
{{ t('serialMonitor.settings.localEchoDescription') }}
</span>
</span>
<input v-model="settings.localEcho" type="checkbox" class="h-4 w-4" @change="updateSettings" />
</label>

<label class="mt-4 block border-t border-vscode-panel-border pt-4">
<span class="block text-sm">{{ t('serialMonitor.settings.logBaudRate') }}</span>
<span class="mt-1 block text-xs text-vscode-input-placeholder">
Expand Down Expand Up @@ -325,7 +337,7 @@ const lineEnding = ref<SerialLineEnding>('crlf');
const showHex = ref(false);
const terminalMode = ref(false);
const settingsOpen = ref(false);
const settings = ref({ showTimestamp: true, renderAnsi: true, logBaudRate: 1000000 });
const settings = ref({ showTimestamp: true, renderAnsi: true, localEcho: false, logBaudRate: 1000000 });
const input = ref('');
const errorMessage = ref('');
const logContainer = ref<HTMLElement | null>(null);
Expand Down Expand Up @@ -394,7 +406,9 @@ const disposables: Array<() => void> = [];

onMounted(() => {
window.addEventListener('keydown', handleGlobalKeydown);
window.addEventListener('resize', handleWindowResize);
disposables.push(() => window.removeEventListener('keydown', handleGlobalKeydown));
disposables.push(() => window.removeEventListener('resize', handleWindowResize));
disposables.push(
onMessage<{ snapshot: SerialMonitorSnapshot }>('serialMonitorSnapshot', payload => {
applySnapshot(payload.snapshot);
Expand Down Expand Up @@ -513,6 +527,7 @@ function applySnapshot(snapshot: SerialMonitorSnapshot) {
settings.value = {
showTimestamp: snapshot.settings?.showTimestamp ?? true,
renderAnsi: snapshot.settings?.renderAnsi ?? true,
localEcho: snapshot.settings?.localEcho ?? false,
logBaudRate: snapshot.settings?.logBaudRate ?? snapshot.status.baudRate ?? 1000000,
};
if (terminalMode.value) {
Expand Down Expand Up @@ -581,6 +596,7 @@ function updateSettings() {
settings: {
showTimestamp: settings.value.showTimestamp,
renderAnsi: settings.value.renderAnsi,
localEcho: settings.value.localEcho,
logBaudRate: settings.value.logBaudRate,
},
});
Expand Down Expand Up @@ -684,6 +700,7 @@ async function initializeTerminal() {
allowProposedApi: true,
convertEol: true,
cursorBlink: true,
cursorStyle: 'bar',
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
fontSize: 13,
scrollback: 5000,
Expand Down Expand Up @@ -716,6 +733,9 @@ async function initializeTerminal() {
}

function sendTerminalText(payload: string) {
if (settings.value.localEcho && terminal) {
terminal.write(toLocalEchoText(payload));
}
postMessage({
command: 'serialMonitorSend',
payload,
Expand All @@ -724,6 +744,10 @@ function sendTerminalText(payload: string) {
});
}

function toLocalEchoText(payload: string): string {
return payload.replace(/\x7f/g, '\b \b').replace(/\r\n|\r|\n/g, '\r\n');
}

function runTerminalSearch(direction: SearchDirection, incremental = false) {
if (!terminalSearchAddon) {
return;
Expand Down Expand Up @@ -834,6 +858,13 @@ function fitTerminal() {
}
}

function handleWindowResize() {
if (!terminalMode.value || !terminal) {
return;
}
window.requestAnimationFrame(() => fitTerminal());
}

function disposeTerminal() {
if (terminalSearchRefreshHandle !== undefined) {
window.clearTimeout(terminalSearchRefreshHandle);
Expand Down Expand Up @@ -1160,6 +1191,7 @@ async function scrollToBottom() {
.serial-monitor-shell {
height: 100%;
max-height: 100%;
min-width: 0;
}

.tool-button,
Expand Down Expand Up @@ -1281,28 +1313,38 @@ async function scrollToBottom() {
background: var(--vscode-terminal-background, var(--vscode-background));
color: var(--vscode-terminal-foreground, var(--vscode-foreground));
outline: none;
min-width: 0;
}

.xterm-host {
width: 100%;
height: 100%;
min-height: 0;
min-width: 0;
overflow: hidden;
}

.xterm-host :deep(.xterm) {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 8px 10px;
}

.xterm-host :deep(.xterm-screen) {
width: 100%;
min-height: 100%;
}

.xterm-host :deep(.xterm-viewport) {
background: transparent;
}

.xterm-host :deep(.xterm-helpers),
.xterm-host :deep(.xterm-helper-textarea) {
max-width: 100%;
}

.send-input {
overflow-y: auto;
}
Expand Down
Loading